-
@Builder 제대로 알고 사용해보기프로젝트/게시판 프로젝트 2022. 4. 30. 02:27728x90
Lombok의 @Builder를 사용하게 되면 빌더 패턴을 매우 간단하게 적용할 수 있습니다.
빌더 패턴이란?
빌더 패턴이란 인스턴스(객체)를 생성할 때 생성자만을 통해서 생성하는데 어려움이 있기 때문에 고안된 생성 패턴입니다.
예를 들어 클래스에 생성자 인자가 많으면 각 인자들이 어떠한 값을 나타내는지 알기 어렵기 때문입니다.
빌더 패턴에 대해서 잘 모르신다면 다음 글을 보고 오시면 좋을 것 같습니다.
https://junuuu.tistory.com/247?category=968252
LombokTest라는 클래스를 만들고 @Builder를 적용해보겠습니다.
정말 간단하게 다음과 같은 코드를 작성해 보았습니다.
@Entity @Builder public class LombokTest { @Id @GeneratedValue Long lombokTest_id; }
LombokTest 클래스에 PK를 잡아주고 @Builder를 적용해 보았습니다.
그러면 다음과 같은 컴파일 에러가 발생합니다.
Class 'LombokTest' Sholud have [public, protected] no-arg constructor
@Entity만 사용하는 경우 에러가 발생하지 않고, @Builder만 사용하는 경우에도 에러가 발생하지 않습니다.
하지만 둘을 같이 사용하는 경우에는 컴파일 에러가 발생하게 됩니다.
음.. 일단 기본 생성자가 없다고 하니까 만들어 보겠습니다.
@NoArgsConstructor는 파라미터가 존재하지 않는 기본 생성자를 생성해주는 어노테이션입니다.
@Entity @Builder @NoArgsConstructor public class LombokTest { @Id @GeneratedValue Long lombokTest_id; }
여전히 에러가 발생합니다.
Lombok @Builder needs a proper constructor for this class
이번에는 @Builder를 사용하려면 클래스에 적절한 생성자가 있어야 한다고 합니다.
@AllArgsConstructor를 추가하여 해결해보겠습니다.
@AllArgsConstructor 어노테이션은 모든 필드 값을 파라미터로 받는 생성자를 만들어줍니다.
드디어 모든 것이 해결되었습니다
왜 그럴까요?!!
둘의 존재 자체가 양립하기 때문입니다.
JPA의 Entity객체에는 기본 생성자가 있어야 합니다.
@Builder를 사용하면 생성자의 유무에 따라 아래와 같이 동작합니다.
- 생성자가 없는 경우 : 모든 멤버 변수를 파라미터로 받는 기본 생성자 생성(기본 생성자는 사라짐)
- 생성자가 있을 경우 : 따로 생성자 생성 X
즉 @Entity와 @Builder를 같이 사용하는 순간 @Entity는 기본 생성자가 필요한데 @Builder 어노테이션 때문에 기본 생성자가 사라져서 class Sholud have no-arg constructor라는 기본 생성자가 필요하다는 컴파일 에러가 나오게 된 것입니다.
이후에 @NoArgsConstructor 어노테이션을 명시해주면 기본 생성자가 생기기 때문에 생성자가 존재하므로 @Builder에서 따로 생성자가 생기지 않습니다.
여기에서 클래스에 적절한 생성자가 필요하다고 컴파일 에러가 나오는 이유입니다.
따라서 @AllArgsConstructor 어노테이션을 통해 모든 필드 값을 파라미터로 받는 생성자를 만들어주면 모든 것이 해결됩니다!
@Entity는 기본 생성자가 왜 필요할까요?
Spring Data JPA가 동적으로 객체 생성 시 Reflection API를 활용하기 때문입니다.
우선 java Reflection에 대해서 조금 알아야 하는데 간단하게 소개하자면 구체적인 클래스 타입을 알지 못해도 그 클래스의 메서드, 타입 변수에 접근할 수 있도록 해주는 API입니다.
하지만 이때 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이기 때문에 기본 생성자가 꼭 필요합니다.
@Builder는 모든 멤버 변수를 파라미터로 받는 기본 생성자가 왜 필요할까요?
빌더 패턴에 대해 떠올려보면 좋습니다.
마지막에 build()를 호출하면서 모든 파라미터를 받는 생성자로 객체를 build 하는 과정이 필요합니다.
이때 알맞은 생성자를 찾을 수 없게 되므로 당연하게도 모든 멤버 변수를 파라미터로 받는 기본 생성자가 필요합니다.
한발 더 나아가서
NoargsConstructor(AccessLevel.PROTECTED)를 사용하자
기본 생성자의 접근 제어를 PROTECTED로 설정해놓게 되면 무분별한 객체 생성에 대해 한번 더 체크할 수 있는 수단이 되기 때문입니다.
즉, 객체 생성 시 안정성을 어느 정도 보장받을 수 있습니다.
어 그러면 왜 PRIVATE으로 사용하지 않나요?
만약 AccessLevel의 PRIVATE로 설정한다면 다음과 같은 에러가 발생합니다.
Class 'LombokTest' should have [public, protected] no-arg constructor
아까 봤던 에러네요..
public 또는 protected만 사용하라고 합니다.
이 또한 리플렉션과 관련 있어 보입니다.
기본 생성자가 PRIVATE으로 막히게 되면 외부에서 접근할 수 없기 때문에 제일 안전한 protected를 사용합니다.
@AllArgsConstructor(access = AccessLevel.PRIVATE)를 사용하자
@AllArgsConstructor는 AccessLevel을 PRIVATE으로 설정해도 문제가 없기 때문에 PRIVATE로 설정함으로써 외부에서 접근할 수 없게 됩니다.
@Builder. default와 @NonNull
특정 필드를 특정 값으로 초기화하고 싶다면 @Builder.Default를 사용하면 됩니다.
특정 필드에 Null을 허용하고 싶지 않다면 @NonNull을 사용하면 됩니다.
@NonNull의 아쉬운점은 컴파일 시점에 에러를 뱉어주지 않습니다.
@NoArgsConstructor @Entity @ToString @Builder @AllArgsConstructor public class LombokTest { @Id @GeneratedValue Long lombokTest_id; @NonNull String 필수1; @NonNull String 필수2; //@NonNull String 필수3; @Builder.Default String 기본값 = "기본값입니다"; public static void main(String[] args) { LombokTest lombokTest = LombokTest.builder() .필수1("") .필수2("필수2") .build(); System.out.println(lombokTest); } }
결과
LombokTest(lombokTest_id=null, 필수 1=, 필수 2=필수 2, 기본값=기본값입니다)
여기에서 만약 필수3의 주석을 풀면 어떻게 될까요?!
컴파일 타임에서는 해결해주지 않고 실행 시에 런타임으로 NPE를 발생시킵니다.
@Builder를 클래스 선언부에 @Builder를 사용하지 말자
클래스 선언부에 @Builder 사용 예시
@Builder public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; }
생성자에 @Builder 사용 예시
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; @Builder public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; // ... } }
@Builder를 클래스에 달아주면 @AllArgsConstructor도 같이 달아주는 것과 같기 때문에 바람직하지 않습니다.
가급적 직접 만든 생성자에 달아주는 것이 더 좋습니다.
결론
빌더 패턴을 쉽게 적용할 수 있는 @Builder에 대해서 알아보았습니다.
하지만 @NonNull 어노테이션이 컴파일 타임에서 에러를 발생시키지 않는 것이 조금 아쉽습니다.
경우에 따라 직접 빌더패턴을 작성해서 사용하는 것도 생각해 봐야 할 것 같습니다.
출처
https://cobbybb.tistory.com/14
https://erjuer.tistory.com/106
https://velog.io/@hsbang_thom/Lombok-Builder.Default
'프로젝트 > 게시판 프로젝트' 카테고리의 다른 글
RestController에서 클라이언트에게 어떤 값을 반환해야 할까? (0) 2022.05.05 Entity와 DTO를 분리하자! (0) 2022.05.04 JPA로 CRUD 해보기 + 테스트코드 (0) 2022.04.28 Spring Boot + MySQL 연동(feat. Spring initializr) (0) 2022.04.26 테이블의 PK는 어떤값으로 잡아야 할까? (0) 2022.04.21