@Builder 조심하세요!

alsdl0629·2023년 12월 2일
0
post-thumbnail

이번 글에서는 @Builder 사용 시 발생했던 에러에 대해 다뤄보려고 합니다.

@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 ClassUserBuilder가 생긴 것을 확인할 수 있습니다.

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의 장점은 반복되는 코드의 작성을 줄여 가독성을 높여주지만,
제대로 학습하고 사용하지 않는다면 사이드 이펙트가 생긴다는 것을 알게 되었습니다.

profile
https://alsdl0629.tistory.com 블로그 이전

0개의 댓글