Lombok의 @Builder 어노테이션

na.ram·2024년 8월 13일

Spring

목록 보기
1/13
post-thumbnail

@Builder

Lombok의 @Builder 어노테이션은 클래스 레벨에 붙이거나 생성자에 붙여주면 파라미터를 활용하여 빌더 패턴을 자동으로 생성해준다.

빌더 패턴
보통 생성자를 통해 객체를 생성하게 되는데 빌더를 사용하면, 생성할 때 필요한 데이터만 설정할 수 있고, 유연성을 확보할 수 있으며, 가독성을 높일 수 있으며, 불변성을 확보할 수 있다.


@Builder는 기본적으로 클래스, 생성자, 메서드에만 붙일 수 있는데 클래스 레벨에서 @Builder 어노테이션을 붙이면 모든 요소를 받는 package-private 생성자가 자동으로 생성되며, 이 생성자에 @Builder 어노테이션을 붙인 것과 동일하게 동작한다고 한다.

클래스 레벨 어노테이션

클래스 레벨은 fianl로 선언 및 초기화되어 수정이 불가능한 필드를 제외하고 가능한 모든 필드에 대한 빌더 패턴이 생성된다. (final로 선언되어 있지만 초기화 되어있지 않을 경우, 빌더 패턴이 생성된다.)

@Builder
public class Person {
	private String name;
    private int age;
}

클래스 레벨 어노테이션은 다른 생성자가 이미 있을 경우 제대로 동작하지 않을 수 있다.

해당 생성자가 클래스 레벨에서 요구하는 모든 필드를 주입하는 생성자가 아니더라도(ex. NoArgsConstructor) Lombok은 해당 생성자가 all-args 생성자라고 생각하고 빌더 코드를 생성하기 때문에 컴파일 시점에 오류가 발생할 수 있다.

그리고 생성자에서 파라미터를 주입받지 않고, 특정값으로 초기화하는 필드가 존재할 경우 정의되지 않은 생성자를 사용하기 때문에 컴파일 에러가 발생한다.


디컴파일된 Person 클래스

public class Person {
    private String name;
    private int age;

    @Generated
    Person(final String name, final int age) {
        this.name = name;
        this.age = age;
    }

    @Generated
    public static PersonBuilder builder() {
        return new PersonBuilder();
    }

    @Generated
    public static class PersonBuilder {
        @Generated
        private String name;
        @Generated
        private int age;

        @Generated
        PersonBuilder() {
        }

        @Generated
        public PersonBuilder name(final String name) {
            this.name = name;
            return this;
        }

        @Generated
        public PersonBuilder age(final int age) {
            this.age = age;
            return this;
        }

        @Generated
        public Person build() {
            return new Person(this.name, this.age);
        }

        @Generated
        public String toString() {
            return "Person.PersonBuilder(name=" + this.name + ", age=" + this.age + ")";
        }
    }
}

생성자 레벨 어노테이션

클래스 레벨과 생성자 레벨에는 한 가지 차이점이 있는데 클래스 레벨에서는 가능한 모든 필드에 대하여 빌더 메서드를 생성했다면 생성자 레벨에서는 생성자의 파라미터 필드에 대해서만 빌더 메서드를 생성한다는 점이다.

public class Person {
	private String name;
    private int age;
    
    @Builder
    public Person(String name) {
    	this.name = name;
    }
}

특히 JPA 엔티티 같은 경우 영속되기 전에는 식별자가 존재하지 않아 필연적으로 null 값을 가져야 하는 경우 생성자로 null 값을 전달하기보다는 아예 생성자에서 null 값을 받지 않도록 직접 구성하는 편이 좋을 것이다.

디컴파일된 Person 클래스

public class Person {
    private String name;
    private int age;

    public Person(String name) {
        this.name = name;
    }

    @Generated
    public static PersonBuilder builder() {
        return new PersonBuilder();
    }

    @Generated
    public static class PersonBuilder {
        @Generated
        private String name;

        @Generated
        PersonBuilder() {
        }

        @Generated
        public PersonBuilder name(final String name) {
            this.name = name;
            return this;
        }

        @Generated
        public Person build() {
            return new Person(this.name);
        }

        @Generated
        public String toString() {
            return "Person.PersonBuilder(name=" + this.name + ")";
        }
    }
}

@Singular

@Singular 어노테이션을 사용하면 리스트와 같은 컬렉션 객체를 빌더 패턴으로 다룰 때, 리스트 객체 자체를 넘기는게 아니라 Lombok은 해당 빌더 노드를 컬렉션으로 취급하고 'setter' 메서드 대신 2개의 'adder' 메서드를 생성한다.

@Builder
public class Person {
	private String name;
    private int age;
    @Singular private List<String> families;
}

// 객체 생성 시
Person person = Person.builder()
	.name("john")
    .age(11)
    .families("mother")
    .families("father")
    .build();

만약 필드가 일반 영어로 작성된 경우, Lombok은 @Singular가 있는 모든 컬렉션의 이름이 영어 복수형이라고 가정하고 자동으로 그 이름을 단수화하려고 시도한다. 만약 단수화가 가능하다면, 컬렉션에 한 가지 원소를 추가하는 방법(add-one 메서드)은 해당 단수를 사용한다.

예를 들어, 컬렉션 타입의 필드명이 statuses라면, add-one 메서드는 자동으로 status라고 불린다. 필드를 단수화할 수 없거나 모호한 경우, Lombok은 오류를 생성하고 단수 이름을 명시적으로 지정하도록 강요한다.

디컴파일된 Person 클래스

public class Person {
    private String name;
    private int age;
    private List<String> families;

    @Generated
    Person(final String name, final int age, final List<String> families) {
        this.name = name;
        this.age = age;
        this.families = families;
    }

    @Generated
    public static PersonBuilder builder() {
        return new PersonBuilder();
    }

    @Generated
    public static class PersonBuilder {
        @Generated
        private String name;
        @Generated
        private int age;
        @Generated
        private ArrayList<String> families;

        @Generated
        PersonBuilder() {
        }

        @Generated
        public PersonBuilder name(final String name) {
            this.name = name;
            return this;
        }

        @Generated
        public PersonBuilder age(final int age) {
            this.age = age;
            return this;
        }

        @Generated
        public PersonBuilder family(final String family) {
            if (this.families == null) {
                this.families = new ArrayList();
            }

            this.families.add(family);
            return this;
        }

        @Generated
        public PersonBuilder families(final Collection<? extends String> families) {
            if (families == null) {
                throw new NullPointerException("families cannot be null");
            } else {
                if (this.families == null) {
                    this.families = new ArrayList();
                }

                this.families.addAll(families);
                return this;
            }
        }

        @Generated
        public PersonBuilder clearFamilies() {
            if (this.families != null) {
                this.families.clear();
            }

            return this;
        }

        @Generated
        public Person build() {
            List families;
            switch (this.families == null ? 0 : this.families.size()) {
                case 0 -> families = Collections.emptyList();
                case 1 -> families = Collections.singletonList((String)this.families.get(0));
                default -> families = Collections.unmodifiableList(new ArrayList(this.families));
            }

            return new Person(this.name, this.age, families);
        }

        @Generated
        public String toString() {
            return "Person.PersonBuilder(name=" + this.name + ", age=" + this.age + ", families=" + this.families + ")";
        }
    }
}

0개의 댓글