빌더 패턴 (Builder Pattern) 정리


1. 개념

빌더 패턴은 객체 생성 과정에서 생성자를 사용하지 않고, 단계적으로 객체를 구성하여 마지막에 완성된 객체를 반환하는 방식입니다. 이 패턴은 가독성을 높이고, 객체를 유연하고 안정적으로 생성할 수 있는 방법을 제공합니다. 특히, 파라미터가 많거나 다양한 조합의 파라미터를 사용할 때 매우 유용합니다.

빌더 패턴은 다음과 같은 특징을 가지고 있습니다

  • 유연한 객체 생성: 선택적인 필드나 필수 필드를 구분하여 객체를 쉽게 만들 수 있습니다.

  • 가독성 향상: 메서드 체이닝 방식으로 필드 값을 설정할 수 있어 코드의 가독성이 좋아집니다.



2. 탄생 배경

빌더 패턴은 복잡한 객체 생성 과정을 단순화하고 가독성을 높이기 위해 등장한 디자인 패턴입니다. 특히, 다음과 같은 문제를 해결하기 위해 고안되었습니다

  • 복잡한 객체 생성: 객체가 생성될 때 많은 파라미터가 필요하거나, 객체의 필수 및 선택적인 필드가 많은 경우 생성자를 통해 객체를 생성하는 것은 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다.

  • 가독성 저하: 생성자에 많은 인자가 포함되면, 호출 시 어떤 값이 어떤 파라미터에 해당하는지 명확하지 않아 코드의 가독성이 떨어집니다.

  • 점층적 생성자 패턴의 한계: 파라미터가 늘어날수록, 점층적 생성자 패턴 (Telescoping Constructor Pattern)을 사용할 때 여러 개의 생성자를 정의해야 하고, 이는 코드 복잡성을 증가시킵니다.


2-1 점층적 생성자 패턴 (Telescoping Constructor Pattern) with Lombok


import lombok.AllArgsConstructor;

@AllArgsConstructor
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;

    public static void main(String[] args) {
        // 모든 필드를 지정하는 생성자 사용
        NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
        System.out.println(cocaCola);
    }

// 위의 객체를 생성할 때 각 파라미터가 어떤 속성에 해당하는지 알기 어려움.

    @Override
    public String toString() {
        return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings +
                ", calories=" + calories + ", fat=" + fat + 
                ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
    }
}

Lombok의 @AllArgsConstructor 어노테이션은 모든 필드를 초기화하는 생성자를 자동으로 생성해줍니다. 하지만 점층적 생성자 패턴의 경우, 필요에 따라 추가적인 생성자를 수동으로 작성해야 하는 불편함이 여전히 존재합니다.

2-2 빌더 패턴 (Builder Pattern) with Lombok


import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
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; // 선택적

    public static void main(String[] args) {
        // 빌더 패턴으로 객체 생성
        NutritionFacts cocaCola = NutritionFacts.builder()
                                    .servingSize(240)
                                    .servings(8)
                                    .calories(100)
                                    .sodium(35)
                                    .carbohydrate(27)
                                    .build();

// 위의 객체 생성시 파라미터와 속성을 함께 작성하여 어떤 파라미터인지 알기 쉽게 만듦

        System.out.println(cocaCola);
    }
}

Lombok의 @Builder 어노테이션을 사용하면 자동으로 빌더 패턴을 구현할 수 있습니다. 필드들을 체이닝 방식으로 설정할 수 있으며, 필요하지 않은 필드는 생략할 수 있습니다.



3. lombok 의 @Builder 와 @ToString

3-1 (lombok 사용 x)

public class Person {

    private String firstName;
    private String lastName;
    private int age;
    private String address;

    // Person 클래스의 private 생성자
    private Person(PersonBuilder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.address = builder.address;
    }

    // PersonBuilder 내부 클래스
    public static class PersonBuilder {
        private String firstName;
        private String lastName;
        private int age;
        private String address;

        public PersonBuilder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public PersonBuilder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

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

        public PersonBuilder address(String address) {
            this.address = address;
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }


	// 생성시킨 객체를 더 편하게 보기위해서(테스트를 용이하게 하려고 작성)
    // lombok 사용시 @ToString 으로 대체능
    @Override
    public String toString() {
        return "Person [firstName=" + firstName + ", lastName=" + lastName + ", age=" + age + ", address=" + address + "]";
    }

    public static void main(String[] args) {
        // 빌더 패턴으로 객체 생성
        Person person = new Person.PersonBuilder()
                                .firstName("John")
                                .lastName("Doe")
                                .age(30)
                                .address("123 Main St")
                                .build();

        System.out.println(person);
    }
}

3-2 (lombok 사용)

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class Person {

    private String firstName;
    private String lastName;
    private int age;
    private String address;

    public static void main(String[] args) {
        // @Builder 어노테이션으로 객체 생성
        Person person = Person.builder()
                              .firstName("John")
                              .lastName("Doe")
                              .age(30)
                              .address("123 Main St")
                              .build();

        System.out.println(person);
    }
}