728x90

문제

<choose>
    <when test = ”isLiked == ‘t’”>
        UPDATE t_book_like
        SET is_liked = 'f'
        WHERE identity_id = #{userId} AND book_id = #{bookId}
    </when>

<when test = ”isLiked == ‘t’”> 여기서 난 문제였다.

 

 

해결

<when test='isLiked == "t"'> 이렇게 ‘ ‘ 로 해주니까 해결 되었다.

728x90

헥사고날 아키텍처를 처음 접해보았을 때는 인턴때였습니다. 프로젝트 베이스가 깔려지고 사수님한테 설명을 듣고 "나도 저런 아키텍처를 내 코드에 사용해보고싶다.. 더 나아가서 아키텍처를 개발해보고 싶다" 라는 생각이 들었습니다. 마침F-lab을 하며 멘토님이 헥사고날 아키텍처를 사용하여 코드 작성하는 모습을 보여주었습니다. 인턴 때 예습이 되어있어서 그런지 정말정말 이해가 잘 되는 느낌이었습니다. 

 

 

 

헥사고날 아키텍처

  • 내부 영역 - 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계
  • 외부 영역 - 내부 영역에서 기술을 분리하여 구성한 영역이고 내부 영역 설계 이후 설계

이상 대략적인 설명이었고

자세한 설명은 https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749 여기에 가서 보시길 바랍니다.. 괜히 잘못된 정보가 전해지면 안되니까요..

 

비즈니스 로직에 집중

내/외부를 분리하고 내부에는 비즈니스 로직들만 존재하고 외부에는 내부의 비즈니스 로직의 관심사가 아닌 것들(database, logging, etc...)이 존재하였습니다. 내/외부를 adaptor와 port를 이용하여 내부의 비즈니스 로직이 필요한 것들을 꽂아서 사용할 수 있는 구조였습니다. 다음 그림으로 설명이 될 것 같습니다.

포트(port)

 

어뎁터(adaptor)

USB를 옆의 그림처럼 USB 포트에 꼽아서 사용하는 것과 같다고 생각합니다. USB는 우리가 원하는 것들을 꼽아서 사용하지 않나요? 그런거와 같이 우리가 비즈니스로직에 MySql을 사용하고싶으면 MySql에 해당하는 어뎁터를 포트에 꼽고 H2를 사용하고 싶으면 그에 해당하는 어뎁터를 꼽아 사용할 수 있습니다. 

 

 

 

 

어뎁터는 포트에다가 꽂을 수 있는 것들을 골라서 꼽을 수 있습니다. 비즈니스 로직에 사용하고 싶은 어뎁터로 포트에 꼽으면 되는 구조였습니다.

 

 

 

 

 

 

 

 

 

어떻게?

인터페이스가 가능하게 해줍니다. 

 

원래 이런 구조였습니다. CommandHandler가 바로 MybatisRepositoryRepository를 의존하고 있었습니다. 그런데 사실상 CommandHandler는 MyBatis, JPA, JDBCtemplate 등 무슨 ORM을 사용하는지 비즈니스 로직의 관심사가 아닙니다. 그리고 바로 구현 객체를 의존하면 변경에 유연하지 않다고 생각합니다. 이런 상황을 풀어줄 친구는 인터페이스 입니다. 다음 사진으로 가보겠습니다.

 

이렇게 인터페이스인 IBookRepository를 의존하는 방법입니다. 여기서 인터페이스는 port 역할을 합니다. 그리고 BookMybatisRepository는 adaptor 역할을 합니다.

 확실하게 관심사가 분리될 수 있었습니다. 비즈니스 로직은 비즈니스로직이 중심이 되고 비즈니스 로직이 필요한 것들은 포트를 게이트로 삼아 끼워넣는 방식은 변경에 유연하다고 생각했습니다. 

 

 

 

 

프로젝트 구조 바뀌기 전 후

구조 바뀌기 전
구조 바뀐 후

 

 

훨씬 깔끔해졌습니다..!!!!

 

https://github.com/Taewoongjung/Doseoro_Java/pull/37

 

728x90

이와 같이 패키지 전체를 포인트 컷으로 잡아서 구현한 AOP를 Custom AOP로 바꾸어보았습니다.  

@Aspect
@Component
public class LogAspect {
    private static final Loggerlogger= LoggerFactory.getLogger(LogAspect.class);

    @Around("within(com.myproject.doseoro..*)")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {

        String params = getRequestParams();

        long startAt = System.currentTimeMillis();

logger.info("----------> REQUEST : {}({}) = {}", pjp.getSignature().getDeclaringTypeName(),
                pjp.getSignature().getName(), params);

        Object result = pjp.proceed();

        long endAt = System.currentTimeMillis();

logger.info("----------> RESPONSE : {}({}) = {} ({}ms)", pjp.getSignature().getDeclaringTypeName(),
                pjp.getSignature().getName(), result, endAt-startAt);

        return result;
    }

    private String getRequestParams() {

        String params = "";

        RequestAttributes requestAttribute = RequestContextHolder.getRequestAttributes();

        if (requestAttribute != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes()).getRequest();

            Map<String, String[]> paramMap = request.getParameterMap();

            if (!paramMap.isEmpty()) {
                params = " [" + paramMapToString(paramMap) + "]";
            }
        }
        return params;
    }

    private String paramMapToString(Map<String, String[]> paramMap) {
        StringJoiner sj = new StringJoiner(",", "[", "]");
        return paramMap.entrySet().stream()
                .map(entry -> String.format("$s -> ($s)",
                        entry.getKey(), sj,join(entry.getValue())))
                .collect(Collectors.joining(", "));
    }
}

 

바꾼 이유는 공부 목적도 있었고 멘토님이 말하시길 "코드상 눈에 보이는 표식을 하지 않았기 때문에 모르는 사람이 볼때는 Aspect 로직이 왜 실행되는지 이해를 하지 못하는 경우도 많다"고 하셨다. 듣고 보니 정말 그럴 것 같았다.

 

어떻게 바뀌었냐면 

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {}
@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(Logging)") // Custom AOP
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
    
    	// ... 이하 위 로직과 같음

 

이렇게 바꿈으로써 전에 없었던 어노테이션이 생겼고, 그 어노테이션으로 무엇을 하려고 하는지 정확히 알 수 있었습니다.

 

 

이 과정에서 @Transactional, @Cacheable, @Async 에너테이션도 AOP가 적용된 사례라는 것을 알게되었습니다.

 

 

참고

https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

 

AOP와 @Transactional의 동작 원리

오늘은 @Transactional의 동작 원리를 AOP와 함께 좀 더 자세하게 조사해보려고 한다.여기서 다루는 내용은 다음과 같다.AOP란 무엇이며 왜 사용하는가Spring AOP는 왜 프록시를 사용하는가@Transactional은

velog.io

https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/

 

AOP 입문자를 위한 개념 이해하기

이 글은 AOP 개념이 생소한 입문자들을 위한 포스팅입니다. 1. OOP의 한계 image…

tecoble.techcourse.co.kr

https://private-space.tistory.com/98

 

Spring에서 AOP를 구현하는 방법과 Transactional

Spring에서 AOP를 구현하는 방법과 Transactional AOP에 관한 간략한 개념이 필요하다면 다른 글을 참조한다. 이 내용은 공식 문서를 참조하여 작성하였다. Spring Framework Document AOP 구현 방식 Spring에서 A..

private-space.tistory.com

 

728x90

버스를 타고 집에 오는 길에 생각이 나서 적어본다...

앞으로 얘기할 주제는 seq를 1,2,3,... 으로 하고 PK를 주고 id를 uuid를 넣어 UK를 주는 방식을 말한다.  

 

이 방법의 핵심은 이벤트 드리븐 형식에 있다고 생각한다. 아직 카프카 같은 것을 써보진 않았지만... (얼핏 봤을 때) 이벤트가 발생하고 그 이벤트가 큐에 들어갔을 때 이벤트의 고유번호를 테이블의 컬럼의 id를 주면 되겠다는 생각이 들었다.

 

그러면 나중에 모니터링 할때도 쉬워진다고 생각했다. 왜냐하면 그 이벤트가 발생하고 생겨난 row를 추적할 때 그 id를 가진 컬럼만 찾으면 되기 때문이라고 생각한다.

 

단지 생각만 나서 기록을 해둔다... 나중에 카프카나 RabbitMQ같은 메시징 서비스를 사용해볼 예정이라 그때 드는 생각을 또 적어보겠다.

728x90

깃헙 액션 스크립트는 별로 특별한게 없다.. 별로 달라진것도 없고.. 

name: PR test CI

# 하기 내용에 해당하는 이벤트 발생 시 github action 동작
on:
  push: # feature/*와 pre-production와 main 브랜치에서 push가 일어났을 때 github action 동작
    branches:
      - 'feature/*'
      - 'main'
      - 'pre-production'
  pull_request: # feature/*와 pre-production와 main 브랜치에서 PR이 일어났을 때 github action 동작
    branches:
      - 'feature/*'
      - 'main'
      - 'pre-production'

# 참고사항
# push가 일어난 브랜치에 PR이 존재하면, push에 대한 이벤트와 PR에 대한 이벤트 모두 발생합니다.

jobs:
  build: 
    runs-on: ubuntu-latest # 실행 환경 지정

    steps:
      - uses: actions/checkout@v2 # github action 버전 지정(major version)

      - name: Set up JDK 11 # JAVA 버전 지정
        uses: actions/setup-java@v3
        with:
          java-version: 11
          distribution: 'temurin'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
        run: ./gradlew build
#         run: ./gradlew build -x test
        
#       - name: Test with Gradle # test application build
#         run: ./gradlew test

      - name: Publish Unit Test Results # test 후 result를 보기 위해 추가
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: ${{ always() }} # test가 실패해도 report를 남기기 위해 설정
        with:
          files: build/test-results/**/*.xml

 

 

그런데....

 

 

 

이 화면의 연속이었다. 어제 오늘...순수 해결하려고 노력한 시간만 대략 20시간 동안 꼬박 이것만 한 것 같다.

 

DoseoroApplicationTests > contextLoads() FAILED
 java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
 

이 오류에 대한 원인을 살펴봤었다.

  • MySql의 문제
  • DB가 물리지 않았기 때문
    • 디비가 물렸어도 테이블이 없기 때문
      • 현재 MyBatis를 사용하고 있는데 JPA나 Node js에 Sequelize 같은 ORM은 자동으로 테이블을 만들어 주지만 MyBatis에는 그런게 없는데 어떡하지? 의 문제

등등 긴 시간동안 많은 고민이 있었던 것 같다...

 

 

1.  처음에 MySql의 문제로 다가갔다.

테스트 코드가 돌려면 어쨌든 DB에 갔다 와야하는데 깃헙 액션 스크립트를 봤는데 MySql을 세팅하는 로직이 없었다. 그래서 

 

 - name: Setup MySQL
        uses: samin/mysql-action@v1
        with:
          mysql user: 'root'
          mysql password: ${{ secrets.MYSQL_PASSWORD }}

 

이 코드를 추가했다. 그런데 예상 외로 실행되지  않았다... 그렇다면 무슨 문제가 있을까 하며 다른 문제를 찾았다. MySql을 세팅을 했지만 거기에 테이블이 없네??? 그러면 만들어야지. 라며 생각했다. 그런데 H2가 요즘 현업에서 테스트용도로만 쓴다고 했다.

 

2.  H2의 적용

그래서 찾아보니 H2는 정말정말 구미가 당길만한 능력이 있는 친구였다. In-Memory 형식 디비를 구축할 수 있었다. H2 DB를 메인 메모리(JVM) 위에서 돌아가며 애플리케이션이 종료되면 없어지는 휘발성 디비였다. 그러면 완전 좋은거네??? 이득!! 하면서 바로 적용해보기로 했다. 그리고 메인 메모리에서 돌아가는 DB라 데이터 캐싱 DB로도 사용할 수 있는 장점을 느꼈다. 다시 사용할거라는 기약을 했다.

 

테스팅 용으로 H2를 적용은 했는데 이제 어떻게해?? 라는 의문점만 남았다. 그냥 돌려봐야지. 라는 생각만 들었다. 빨리 결과를 도출하고싶은 급한 마음이었다... 당연히 안될게 뻔했다. 내 자신이 부끄러울정도로 멍청하였다. 테이블이 없는데 어떻게 돌아가겠니!!!... 바로 테이블을 생성하는 시도로 넘어가자.

 

3.  테이블 생성

지금까지 MyBatis를 사용하면서 코드상으로 DDL을 한번도 한적이 없었다. 어떻게 하지..? 라는 생각이 들고 계속 검색만 했다. 정말 여러 방법들이 있었다. 이번에 자바 스프링을 처음 배우면서 MyBatis만 사용해봤으니 다른 것도 사용해볼까? 라는 생각이 들었다. JdbcTemplate이 pure 한 쿼리문을 넣을 수 있고 예전부터 사용해보고 싶었는데 이번 기회에 사용해보면 좋겠다 싶어서 사용해봤다. 정말정말 편했다...

 

JdbcTemplate으로 테이블을 생성하고 테스트코드 로직에 맞게 잘 수정한 뒤 로컬에서 테스트를 돌려보았다. 잘 돌아갔다. 그러면 깃헙 액션에서도 잘 돌아가겠지?? 라는 안일한 생각으로 기쁜 마음으로 돌려보았다. 결과는 아직이었다.. 계속 위와 같은 오류를 뿜어냈다. 그래도 고지가 코앞이라는 생각이 들었다. 이 문제를 해결할 자신이 있었다. 

 

4. application.yml 의 분리

테스트 용도랑 로컬용도로 분리하였다. main과 test 폴더 각각 resource 폴더가 있고 그 안에는 application.yml 파일이 각각 있었다. 그런데 main에는 application-test.yml을 추가하고 

spring:
  profiles:
    active: test

을 추가하며 test 용도로 분리했다.  그리고 test 폴더의 application.yml은 

spring:
  profiles:
    active: test
  config:
    import: application-test.yml

main의 application.yml을 불러오는 식으로 처리했다. 이제 완벽히 세팅이 끝났다고 생각하고 깃헙 액션을 돌려보았다. 잘 안됐다. 다 됐다고 생각했는데 왜 안되지???

 

+ ./bin에 있는 H2 라이브러리를 추가할 때 바로 위와 같은 상황일 때 main에만 추가 해주면 된다.

 

5. 커밋 히스토리를 보고 .gitignore를 확인하는 순간 해결

커밋 히스토리를 유심히 보니 테스트 폴더쪽에 application.yml이 들어가있지 않았다 .gitignore에 application.yml이 추가가 되어있었다. 정말 바보같은 실수였다. 테스트 폴더쪽의 application.yml과 함께 푸시를 하면서 모든 문제는 해결이 되었다. 안도의 한숨을 쉰다.

728x90

 

MySql 테이블에 컬럼 타입이 JSON인 컬럼을 MyBatis로 읽어오려고 했는데 처음에 오류가 나서 12시간 가량 삽질 끝에 빛이 보였다. 이 과정을 공유하려고 한다.

Select

문제

MySql 테이블은 다음과 같다.

CREATE TABLE `t_book`
(
		...
		
		`title` VARCHAR(100) NOT NULL,
		`about` VARCHAR(100) NOT NULL,
		`price` VARCHAR(100) NOT NULL,
		`img`   JSON             NULL,
		
		...
}
<select id="findHomeDisplayedBooks" resultMap="HomeDisplayedBookVO">
    SELECT
        title,
        about,
        price,
        img
    FROM t_book
    ORDER BY createdAt
    DESC  LIMIT 5
</select>

이것의 결과로 이렇게 나왔었다.

[HomeDisplayedBookVO{title='나는찐개발자', about='나.', price='777', images=null}]

해결

<resultMap id="HomeDisplayedBookVO" type="com.myproject.doseoro.packages.book.vo.HomeDisplayedBookVO">
    <result property="title" column="title"></result>
    <result property="about" column="about"></result>
    <result property="price" column="price"></result>
    <result property="images" column="img" typeHandler="com.myproject.doseoro.global.util.JsonTypeHandler"></result>
</resultMap>
<select id="findHomeDisplayedBooks" resultMap="HomeDisplayedBookVO">
    SELECT
        title,
        about,
        price,
        img
    FROM t_book
    ORDER BY createdAt
    DESC  LIMIT 5
</select>

insert 처럼 typeHandler를 추가해주었더니 DB에서 JSON으로 데이터를 가져올 수 있었다.

typeHandler 코드는 다음과 같다.

 

 

Insert

문제

MySql에 테이블은 select 할때와 같고, img 컬럼을 insert 하고싶었다. 오류가 계속 났다... 시도 끝에 해결책을 알아냈다.

CREATE TABLE `t_book`
(
		...
		
		`title` VARCHAR(100) NOT NULL,
		`about` VARCHAR(100) NOT NULL,
		`price` VARCHAR(100) NOT NULL,
		`img`   JSON             NULL,
		
		...
}

 

해결

<resultMap id="BookVO" type="com.myproject.doseoro.packages.book.vo.BookVO">
        <result property="images" column="img" typeHandler="com.myproject.doseoro.global.util.JsonTypeHandler"></result>
</resultMap>
<select id="findBookByBookId" resultMap="BookVO" parameterType="String">
    SELECT
    	img,
    FROM t_book
    WHERE id = #{bookId}
</select>

이것 또한 typeHandler 를 넣었어야 했습니다. typeHandler의 코드는 위와(insert 때와) 같습니다.

 

마치며

해결하고 나니 너무 뿌듯했습니다. 고민하고 찾는 시간은 고됐지만 해결 후 뿌듯함은 더할나위 없이 기쁘네요. 

728x90

이런 오류가 나타났다.

 

이 오류는 dao.xml 에 중복되는 resultType이 있었기 때문이다. 처음에는 어차피 객체를 생성하는 틀이라고 생각해서 중복되는 VO나 DTO를 사용해도 될 것 같다고 생각했는데 아니었다. 그래서 다시 VO를 만들어주었다.

728x90

Mybatis로 select를 해서 그 결과 값을 dto 객체에 넣으려고 했는데 기본생성자가 없다고하면서 오류가 났었던 기억이 있다.

@Builder
@Getter
@NoArgsConstructor
public class HomeDisplayedBookVO {

    private String id;
    private String title;
    private String about;

	public HomeDisplayedBookVO(
String id, String title, String about, String price, List<String> images) 
	{
        this.id = id;
        this.title = title;
        this.about = about;
			
			...

분명 @Builder 패턴으로 객체를 생성하면 되는데 왜 굳이 기본생성자가 필요한지 궁금했다.

Mybatis에서 resultType, resultMapping은 ObjectFactory라는 것을 사용하고 있다고 한다. 이 과정에서 객체의 초기화가 필요한데, 이 때 기본생성자가 먼저 호출되어 사용되어진다. 기본 생성자를 이용하여 오브젝트를 생성 한 후에는, 리플렉션을 사용하여 객체에 데이터를 바인딩 한다고 한다. 

 

그래서  리플랙션이랑 기본생성자랑 무슨 관계가 있는데?? 그리고 Mybatis랑 무슨 관계가 있는데? 

리플랙션에서 객체를 만들 때 기본 생성자를 먼저 찾는다고한다. 왜냐하면 먼저 객체를 생성해야 필드값들을 가져와서 필드값에 값을 넣어줄 수 있지않는가?? 객체를 만들 때 필요한 것기본 생성자이기 때문이다. 그러므로 Mybatis 로 select 하고 select 된 값들로 객체를 생성해야 하는데, 생성하는 과정에서 리플랙션(reflection)이 기본 생성자를 가르켜 객체를 생성한 후, 객체의 필드에 접근하여 값을 저장합니다.

 

 

참조

https://da-nyee.github.io/posts/woowacourse-why-the-default-constructor-is-needed/

 

[우아한테크코스] 기본 생성자가 필요한 이유 (Why the default constructor is needed) (feat. Jackson ObjectMapper

개요

da-nyee.github.io

https://okky.kr/articles/725359

 

OKKY - Spring MyBatis & DTO 기본생성자 관련 질문 드립니다.

안녕하세요.SQL Mapper인 MyBatis를 사용하다가 궁금한점이 생겨 질문 드립니다.MyBatis에서 Select를 통해 데이터를 조회 후 객체에 데이터 삽입할 경우 기본 생성자가 없을 경우 데이터값 삽입 불가 [@N

okky.kr

https://stir.tistory.com/52

 

기본 생성자(Default Constructor)가 필요한 이유

서론 어느날 갑자기 이런 생각이 떠올랐다. 클래스에 매개변수가 존재하는 생성자가 있으면 기본 생성자가 굳이 필요한가? 왜 매개변수가 존재하는 생성자를 만들어주면 항상 기본 생성자를 만

stir.tistory.com

 

728x90

MySql 테이블에 컬럼 타입이 JSON인 컬럼을 MyBatis로 읽어오려고 했는데 처음에 오류가 나서 12시간 가량 삽질 끝에 빛이 보였다. 이 과정을 공유하려고 한다.

 

문제

MySql 테이블은 다음과 같다.

CREATE TABLE `t_book`
(
		...
		
		`title` VARCHAR(100) NOT NULL,
		`about` VARCHAR(100) NOT NULL,
		`price` VARCHAR(100) NOT NULL,
		`img`   JSON             NULL,
		
		...
}

그리고 문제의 Mybatis 쿼리문은 다음과 같다

<select id="findHomeDisplayedBooks" resultMap="HomeDisplayedBookVO">
    SELECT
        title,
        about,
        price,
        img
    FROM t_book
    ORDER BY createdAt
    DESC  LIMIT 5
</select>

이것의 결과로 이렇게 나왔었다.

[HomeDisplayedBookVO{title='헤헤헿', about='나.', price='777', images=null}]

 

해결

<resultMap id="HomeDisplayedBookVO" type="com.myproject.doseoro.packages.book.vo.HomeDisplayedBookVO">
    <result property="title" column="title"></result>
    <result property="about" column="about"></result>
    <result property="price" column="price"></result>
    <result property="images" column="img" typeHandler="com.myproject.doseoro.global.util.JsonTypeHandler"></result>
</resultMap>
<select id="findHomeDisplayedBooks" resultMap="HomeDisplayedBookVO">
    SELECT
        title,
        about,
        price,
        img
    FROM t_book
    ORDER BY createdAt
    DESC  LIMIT 5
</select>

 

insert 처럼 typeHandler를 추가해주었더니 DB에서 JSON으로 데이터를 가져올 수 있었다.

[HomeDisplayedBookVO{title='헤헤헿', about='나.', price='777', images=[7df847a1-6846-4f60-9a52-7aed8677d02e.jpeg, 7e14f0f2-647f-4eff-b673-e518b2e96bd3.jpeg]}]

 

 

typeHandler 코드는 다음과 같다.

@MappedTypes({Object.class})
public class JsonTypeHandler extends BaseTypeHandler<List<String>> {

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, List<String> s, JdbcType jdbcType) throws
            SQLException {
        preparedStatement.setObject(i, new Gson().toJson(s));

    }

    @Override
    public List<String> getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return convertToList(resultSet.getString(s));
    }

    @Override
    public List<String> getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return convertToList(resultSet.getString(i));
    }

    @Override
    public List<String> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return convertToList(callableStatement.getString(i));
    }

    private List<String> convertToList(String myJsonDataListAsString) {
        try {
            return new ObjectMapper().readValue(myJsonDataListAsString, new TypeReference<List<String>>() {
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }
}
728x90

아래와 같이 인터페이스를 정했을 때

public interface ICommandHandler<T, R> {

    public R handle(T t);
}

 

리턴타입을 void로 짜야하는 로직이 있었다. 이럴 때 Void 로 넣어주면 됐었다.

제네릭에는 Object 클래스를 상속받는 클래스들만 올 수 있고 객체만 올 수 있다. 어떻게 보면 당연한 소리다. 넘길 때는 주소값이 있는 객체를 넘겨야 하는 것이기 때문이다.

 

int, boolean 등등 primitive type 으로 할 때도 wrapper class 인 Integer, Boolean .. 으로 제네릭에 넣어야 한다.

 

https://stackoverflow.com/questions/5568409/java-generics-void-void-types

+ Recent posts