이번 글에서는
@Builder
사용 시 발생했던 에러에 대해 다뤄보려고 합니다.
반복되는 코드를 줄여주는 라이브러리인 Lombok에서 제공하는 어노테이션으로
빌더 패턴을 구현하지 않더라도 사용할 수 있게 해줍니다.
보통 클래스, 생성자 위에 붙여 사용합니다.
개발 환경은 다음과 같습니다.
IntelliJ
Spring Boot 2.7.4
먼저 예시를 들어보겠습니다!
@ToString
@Builder
public class User {
private String name = "***님";
private String password;
private List<String> books = new ArrayList<>();
}
public class Main {
public static void main(String[] args) {
User user = User.builder()
.password("1234")
.build();
System.out.println(user.toString());
}
}
name, feeds는 초깃값이 지정되어 있기 때문에 passowrd만 설정하는 상황입니다.
User(name=null, password=1234, books=null)
이 출력됩니다.
메인 함수에서 생성할 때 명시하지 않은 필드는
초깃값으로 초기화되기를 기대했지만 null로 초기화되었습니다.
이것을 모르고 사용하다가 NullPointerexception
이 발생했습니다...
public class User {
private String name = "***님";
private String password;
private List<String> books = new ArrayList();
...
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder { // <--- @Builder가 생성해줌
// User에서는 초깃값을 명시해도 UserBuilder는 명시되지 않음
private String name;
private String password;
private List<String> books;
UserBuilder() {
}
public UserBuilder name(final String name) {
this.name = name;
return this;
}
public UserBuilder password(final String password) {
this.password = password;
return this;
}
public UserBuilder books(final List<String> books) {
this.books = books;
return this;
}
public User build() {
return new User(this.name, this.password, this.books);
}
}
우리 눈엔 보이지 않지만 빌드 후 생성된 User.class
입니다.
@Builder
로 인해 Inner Class로 UserBuilder
가 생긴 것을 확인할 수 있습니다.
User에서는 초깃값을 명시해도 UserBuilder는 명시되지 않습니다.
그래서 .password("1234")
를 제외한 나머지 필드는 초기화하지 않았기 때문에
wrapper class default value인 null로 초기화됩니다.
@Builder.Default
사용하기@ToString
@Builder
public class User {
@Builder.Default
private String name = "***님";
private String password;
@Builder.Default
private List<String> books = new ArrayList<>();
}
@Builder.Default
를 사용하면
User(name=***님, password=1234, books=[])
출력이 잘 됩니다!
public class User {
private String name;
private String password;
private List<String> books;
private static String $default$name() {
return "***님";
}
private static List<String> $default$books() {
return new ArrayList();
}
...
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
private boolean name$set;
private String name$value;
private String password;
private boolean books$set;
private List<String> books$value;
...
public UserBuilder name(final String name) {
this.name$value = name;
this.name$set = true;
return this;
}
public UserBuilder password(final String password) {
this.password = password;
return this;
}
public UserBuilder books(final List<String> books) {
this.books$value = books;
this.books$set = true;
return this;
}
public User build() {
String name$value = this.name$value;
if (!this.name$set) {
name$value = User.$default$name();
}
List<String> books$value = this.books$value;
if (!this.books$set) {
books$value = User.$default$books();
}
return new User(name$value, this.password, books$value);
}
}
}
@Builder.Default
사용 후 생긴 User.class
입니다.
이제 .name()
, .books()
를 호출하지 않는다면
$default$name()
, $default$books()
로 초깃값을 지정해 줍니다.
초기화와 관련된 로직이 생겼기 때문에
더 이상 wrapper class default value인 null이 안 들어가게 됩니다!
자식 클래스 생성자에서 @Builder
를 사용한 부모 클래스에 값을 넘기고 싶어
super
를 사용했지만, 패키지 위치가 달라 컴파일 에러가 발생했습니다.
생성자를 명시하지 않으면 어디서든 접근할 수 있는
@AllArgsConstructor(access = AccessLevel.PUBLIC)
이 생성될 줄 알았지만,
// 컴파일된 User.class에서는 default 생성자가 생성됨
User(final String name, final String password, final List<String> books) {
this.name = name;
this.password = password;
this.books = books;
}
같은 패키지에서만 접근할 수 있는
@AllArgsConstructor(access = AccessLevel.PACKAGE)
가 생성됩니다.
따라서 @Builder
와 모든 필드를 가지는 생성자를 같이 사용하고 싶다면
직접 생성자 만들기
(추천)@RequiredArgsConstructor
(추천)@AllArgsConstructor
(비추천)세 가지 방법 중 하나를 선택해면 됩니다.
@Builder
관련 자료를 찾아보면서 @Builder.Default
,
@builder(tobuilder = true)
등 다양한 기능이 있다는 것을 알게 되었습니다.
Lombok의 장점은 반복되는 코드의 작성을 줄여 가독성을 높여주지만,
제대로 학습하고 사용하지 않는다면 사이드 이펙트가 생긴다는 것을 알게 되었습니다.