티스토리 뷰

Spring

@Builder 조심하세요!

alsdl0629 2024. 7. 29. 17:26

@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, books는 초깃값이 할당되어 있기 때문에 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의 장점으로 반복되는 코드를 줄여 가독성을 높여주지만, 제대로 학습하지 않고 사용하면 사이드 이펙트가 생길 수 있다는 것을 알게 되었습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함