Lombok @Builder 상속

MiHyun Park·2021년 9월 14일
0

Lombok의 @Builder

Lombok의 @Builder를 사용하면 객체를 생성할 때 빌더 패턴을 어노테이션 하나로 손쉽게 적용할 수 있다.

빌더 패턴을 적용하면, 생성자를 사용해 객체를 생성할 때보다 가독성이 좋아지고, 파라미터의 순서를 지키지 않아도 돼 보다 유연하다는 장점이 있다. 따라서 생성자에 파라미터가 많다면 빌더 패턴이 좋은 선택이 될 수 있다.

문제상황 : 제너릭을 사용한 중복 제거와 상속

제네릭을 사용해 목록 조회용 응답 DTO의 중복 문제를 해결하고자 했다.

@Getter
@NoArgsConstructor
public class ResponseListDto<T> {
    private int firstPage;
    private int lastPage;
    private List<T> list;

    @Builder
    public ResponseListDto(int firstPage, int lastPage, List<T> list) {
        this.firstPage = firstPage;
        this.lastPage = lastPage;
        this.list = list;
    }
}

대부분의 목록 조회 기능이 위의 DTO를 사용해도 충분했지만, 필드를 추가해 응답해줘야하는 상황이 발생했다. ResponseListDto의 필드는 그대로 사용하고, 필드를 추가해야하니 해결방법으로 상속을 떠올렸다.

@Getter
@NoArgsConstructor
public class ResponseMatchingResultListDto<MatchingVo> extends ResponseListDto<MatchingVo>{
    private int totalCount;
    private int matchedCount;
    private int canceledCount;

    @Builder
    public ResponseMatchingResultListDto(int firstPage, int lastPage, List<MatchingVo> list,
                                         int totalCount, int matchedCount, int canceledCount) {
        super(firstPage, lastPage, list);
        this.totalCount = totalCount;
        this.matchedCount = matchedCount;
        this.canceledCount = canceledCount;
    }
}

그 결과 이런 오류가 발생한다.

서브 클래스의 @Builder가 슈퍼 클래스의 @Builder를 상속했기 때문에 발생한 오류다.
build에서 .class 파일을 디컴파일 해 Lombok의 @Builder가 어떻게 코드로 구현되는지 살펴보기로 한다.

public class ResponseListDto<T> {
	...
	public static <T> ResponseListDto.ResponseListDtoBuilder<T> builder() {
		return new ResponseListDto.ResponseListDtoBuilder();
	}
   
	public static class ResponseListDtoBuilder<T> {...}

}

1. 오버라이딩이 불가능한 정적 메소드

Lombok의 @Builder는 정적 메소드(static method)를 만들어 빌더 패턴을 구현하고 있다.

정적 메소드는 컴파일 될때 메모리에 올라간다. 즉, 정적 메소드는 인스턴스 단위로 생성되는 것이 아니라, 클래스 단위로 생성되는 것이다.

public class Sports {
	public static void play() { System.out.println("Sports"); }
}

public class Soccer extends Sports {
	@Override
	public static void play() { System.out.println("Soccer"); }
}

Sports sports1 = new Sports();
sports1.play();    //출력결과 : Sports

Sports sports2 = new Soccer();
sports2.play();    //출력결과 : Sports

Soccer soccer = new Soccer();
soccer.play();     //출력결과 : Soccer

new 연산은 런타임 시점에 동적으로 메모리를 할당한다. 즉, 부모 클래스의 메소드를 재정의하는 오버라이딩은 런타임 시점에서 동적 디스패치가 이루어진다. static은 클래스 단위로 컴파일 시점에 메모리를 할당하기 때문에 오버라이딩(재정의)되지 않는 것이다.

2. 오버라이딩의 조건

오버라이딩을 하기 위해서는 부모와 자식 클래스의 메소드의 이름, 파라미터 수와 타입, 반환 타입이 정확하게 일치해야한다.

public class ResponseListDto<T> {
	...
	public static <T> ResponseListDto.ResponseListDtoBuilder<T> builder() {
		return new ResponseListDto.ResponseListDtoBuilder();
	}
   
	public static class ResponseListDtoBuilder<T> {...}

}

public class ResponseMatchingResultListDto<T> extends ResponseListDto<T> {
	...
	public static <T> ResponseMatchingResultListDto.ResponseMatchingResultListDtoBuilder<T> builder() {
		return new ResponseMatchingResultListDto.ResponseMatchingResultListDtoBuilder();
	}
   
	public static class ResponseMatchingResultListDtoBuilder<T> {...}

}

슈퍼 클래스 builder()의 반환 타입은 ResponseListDto.ResponseListDtoBuilder, 자식 클래스 builder()의 반환 타입은 ResponseMatchingResultListDto.ResponseMatchingResultListDtoBuilder로 서로 다르다.
그래서 애초에 오버라이딩이 성립될 수 없었던 것이다.

해결

1. 생성자 사용

정적 메소드 build()의 반환값 타입이 일치하지 않아 두 함수가 구분되지 않고, 오버라이딩도 되지 않아 발생한 오류이기 때문에 둘 중 하나의 @Builder를 사용하지 않고 생성자 그대로 사용한다.

2. @SuperBuilder

Lombok은 빌더 패턴이 상속의 경우에서 지원되지 않는 문제를 해결하기 위해 @SuperBuilder를 지원한다.

@SuperBuilder
@Getter
@NoArgsConstructor
public class ResponseListDto<T> {
  private int firstPage;
  private int lastPage;
  private List<T> list;

  public ResponseListDto(int firstPage, int lastPage, List<T> list) {
      this.firstPage = firstPage;
      this.lastPage = lastPage;
      this.list = list;
  }
}

@SuperBuilder
@Getter
@NoArgsConstructor
public class ResponseMatchingResultListDto<T> extends ResponseListDto<T>{
  private int totalCount;
  private int matchedCount;
  private int canceledCount;

  public ResponseMatchingResultListDto(int firstPage, int lastPage, List<T> list,
                                       int totalCount, int matchedCount, int canceledCount) {
      super(firstPage, lastPage, list);
      this.totalCount = totalCount;
      this.matchedCount = matchedCount;
      this.canceledCount = canceledCount;
  }
}

@SuperBuilder는 부모 클래스, 자식 클래스 모두에 클래스 범위에 붙여야한다.
@SuperBuilders는 접근 지정자가 protected인 생성자와 타입 안정성을 보장하기 위해 두개의 내부 빌더 클래스를 각각 만든다.

public Soccer(int game, int ball) {
    super(game);
    this.ball = ball;
}

protected Soccer(final Soccer.SoccerBuilder<?, ?> b) {
    super(b);
    this.ball = b.ball;
}
  
public static Soccer.SoccerBuilder<?, ?> builder() {
    return new Soccer.SoccerBuilderImpl();
}
  
private static final class SoccerBuilderImpl extends Soccer.SoccerBuilder<Soccer, Soccer.SoccerBuilderImpl> { ... }

public abstract static class SoccerBuilder<C extends Soccer, B extends Soccer.SoccerBuilder<C, B>> extends SportsBuilder<C, B> { ... }

빌더 패턴으로 객체를 생성하면
1. Soccer.builder()
2. Soccer.SoccerBuilderImpl
3. Soccer(Soccer.SoccerBuilderImpl)
4. Sports(Sports.SportsBuilder)
가 차례로 불린다.

참고

https://projectlombok.org/features/experimental/SuperBuilder
https://blog.naver.com/gngh0101/221206214829

profile
나는 박미현

0개의 댓글