Design Pattern

Design Pattern | 빌더 패턴(Builder Pattern)

이진유진 2024. 4. 26. 10:25
반응형

https://idl.uw.edu/papers/infovis-design-patterns

빌더 패턴이란?

빌더 패턴(Builder Pattern)은 객체 생성을 단순화하고 유연성을 높이기 위한 디자인 패턴입니다. 

복잡한 객체의 생성 과정을 추상화하고 객체의 구성 요소를 조립하여 객체를 생성하는 데 사용됩니다.

 

빌더 패턴 예시 코드 

// Person 클래스: 생성할 객체의 일부인 간단한 사람 클래스
public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String address;

    // 생성자는 private으로 설정하고, 빌더를 통해서만 객체를 생성할 수 있도록 합니다.
    private Person(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.address = builder.address;
    }

    // Builder 클래스: Person 객체를 생성하는 빌더 클래스
    public static class Builder {
        private String firstName;
        private String lastName;
        private int age;
        private String address;

        public Builder() {}

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

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

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

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

        // Person 객체를 생성하여 반환하는 메서드
        public Person build() {
            return new Person(this);
        }
    }

    // Person 객체의 toString() 메서드 오버라이드
    @Override
    public String toString() {
        return "Person{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

// 빌더 패턴을 사용하는 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        // Builder 객체를 생성하여 필요한 속성을 설정하고, build() 메서드를 호출하여 Person 객체를 생성합니다.
        Person person = new Person.Builder()
                .firstName("John")
                .lastName("Doe")
                .age(30)
                .address("123 Main Street")
                .build();

        // 생성된 Person 객체 출력
        System.out.println(person);
    }
}

 

위 코드에서는 Person 클래스를 생성하고, 해당 클래스의 Builder 클래스를 정의합니다.

Builder 클래스는 필요한 속성을 설정할 수 있는 메서드를 제공하고, 

build() 메서드를 통해 실제로 Person 객체를 생성합니다. 

이를 통해 클라이언트 코드는 각 속성을 선택적으로 설정할 수 있고, 객체 생성 과정을 단순화할 수 있습니다. 

 

Person 클래스에는 직접적으로 출력할 내용을 지정하는 toString() 메서드가 없기에, 
Person클래스에 toString() 메서드를 오버라이드하여 객체의 정보를 문자열로 보기 편하게 형태를 재정의하였습니다. 

Person{firstName='John', lastName='Doe', age=30, address='123 Main Street'}

 

빌더 패턴에서 기본값 정의도 가능합니다. 

public static class Builder {
    private String firstName;
    private String lastName;
    private int age = 0; // 기본값 설정
    private String address = ""; // 기본값 설정

    public Builder() {}

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

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

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

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

    // Person 객체를 생성하여 반환하는 메서드
    public Person build() {
        return new Person(this);
    }
}

 

Builder 패턴에서는 클래스 내에 선택적인 값에 대한 설정 메서드를 구현하고, 

해당 값이 설정되지 않은 경우 기본값을 사용하도록 설정할 수 있습니다. 

 

위의 코드는 age와 address의 기본값을 설정한 코드입니다. 

 

Person person = new Person.Builder()
        .age(30) // 선택적으로 설정
        .address("123 Main Street") // 선택적으로 설정
        .build();

 

또한 위와같이, 기본값을 설정하지 않더라도, 
firstName과 lastName은 자동적으로 문자열 변수의 기본값인 null로 설정이 됩니다. 

Person{firstName='null', lastName='null', age=30, address='123 Main Street'}

 

프로그램 로직을 조심하기만한다면, 

자유롭게 사용할 수 있다는 장점이 있습니다. 

 

빌더 패턴의 장점

1. 객체 생성 과정의 유연성

빌더 패턴을 사용하면 객체의 생성 과정을 단계적으로 수행하고, 

각 단계에서 필요한 속성을 선택적으로 설정할 수 있습니다. 

이를 통해 다양한 객체를 생성하고, 객체 생성 과정을 유연하게 조정할 수 있습니다. 

2. 객체 생성 코드의 가독성 향상

빌더 패턴을 사용하면 객체 생성 코드가 간결해지고 가독성이 향상됩니다. 

필요한 속성을 명시적으로 설정하고, 객체를 생성하는 단계를 쉽게 이해할 수 있습니다. 

3. 객체 불변성 보장

빌더 패턴을 사용하면 생성된 객체의 불변성을 보장할 수 있습니다. 

빌더를 통해 객체를 생성하므로, 클라이언트 코드가 객체의 상태를 변경할 수 없습니다. 

이는 안정성을 높이고 예상치 못한 부작용을 방지할 수 있습니다. 

 

빌더 패턴의 단점

1. 코드 복잡성

빌더 패턴은 추가적인 클래스와 인터페이스를 도입하여 구현되므로, 

코드의 복잡성이 증가할 수 있습니다. 

특히 객체가 단순하거나 생성 과정이 간단한 경우, 빌더 패턴을 사용하는 것이 오히려 더 번거로울 수 있습니다. 

2. 오버헤드

빌더 패턴은 객체 생성을 단순화하고 유연성을 높이지만, 

이에는 일정한 오버헤드가 발생할 수 있습니다. 

빌더 클래스의 추가 및 인스턴스 생성은 메모리와 성능에 부하를 줄 수 있습니다. 

3. 변경에 대한 유연성 제한 

빌더 패턴을 사용하여 객체를 생성하면, 객체의 구조를 변경하기 어려울 수 있습니다. 

새로운 속성이나 변경된 요구사항이 있을 때, 빌더 클래스를 수정해야 하므로, 변경에 대한 유연성이 제한될 수 있습니다. 

 

Lombok의 @Builder 

Lombok은 자바 언어의 보일러플레이트 코드를 줄여주는 라이브러리로, 

이 어노테이션을 사용하면 빌더 패턴을 자동으로 생성해 객체를 생성하는 코드를 간결하게 작성할 수 있습니다.

 

일반적으로 빌더 패턴은 복잡한 객체를 생성할 때 유용하며, 

객체의 각 필드를 설정하는데 필요한 복잡한 코드를 줄여줍니다. 

그러나 빌더 패턴을 구현하는 데는 많은 반복적인 코드가 필요할 수 있습니다. 

 

@Builder 어노테이션을 사용하면 해당 클래스의 빌더 클래스를 자동으로 생성합니다. 

 

사용법은 

@Builder 어노테이션을 추가하고, 필요한 필드에 대해서는 @Builder어노테이션의 access 속성을 통해 접근 제어 지정할 수 있습니다. 

 

@Builder 어노테이션 예시코드(Access 속성 사용)

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.AccessLevel;

@Getter
@Setter
@Builder(access = AccessLevel.PUBLIC)
@ToString
public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String address;
}

// 사용 예시
Person person = Person.builder()
    .firstName("John")
    .lastName("Doe")
    .age(30)
    .address("123 Main Street")
    .build();
AccessLevel.PUBLIC: 빌더 클래스와 생성자, 메서드를 모두 public으로 지정합니다. (기본값)
AccessLevel.PROTECTED: 빌더 클래스와 생성자를 public, 메서드를 protected로 지정합니다.
AccessLevel.PRIVATE: 빌더 클래스와 생성자, 메서드를 모두 private로 지정합니다.
AccessLevel.PACKAGE: 빌더 클래스와 생성자, 메서드를 동일한 패키지 내에서만 접근할 수 있도록 지정합니다.
AccessLevel.NONE: 빌더 클래스를 생성하지 않습니다. 직접 생성자를 이용하여 객체를 생성해야 합니다.

 

이렇게 @Builder 어노테이션을 사용하면 더 간편하게 빌더 패턴을 적용할 수 있습니다. 

 

여기서 @ToString 이라는 어노테이션도 확인할 수 있는데, 

toString() 메서드를 자동 생성해 주는 어노테이션입니다. 

 

필수 파라미터를 가지는 빌더 구현

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE) // 기본 생성자를 private로 설정
public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String address;

    @Builder(builderMethodName = "requiredBuilder") // 필수 파라미터를 받는 빌더 메서드 지정
    private Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // 사용 예시
    public static void main(String[] args) {
        Person person = Person.requiredBuilder()
                .firstName("John")
                .lastName("Doe")
                .age(30)
                .address("123 Main Street")
                .build();
    }
}

 

위의 코드에서는

@NoArgsConstructor(access = AccessLevel.PRIVATE)를 사용하여

기본 생성자를 private으로 설정하여 외부에서 객체를 직접 생성하는 것을 막습니다.

대신에 @Builder 어노테이션에 builderMethodName 속성을 사용하여 필수 파라미터를 받는 빌더 메서드의 이름을 지정합니다.

여기서는 requiredBuilder라는 이름을 사용하였습니다.

이렇게 함으로써 Person 객체를 생성할 때 반드시 firstNamelastName을 설정해주어야 합니다.

 

firstName과 lastName이 필수파라미터로 지정되어 있어, 이를 설정하지 않고 빌더를 호출 시,

컴파일 에러가 발생합니다. 

 

Error:(12, 26) java: no suitable method found for build() method Person.requiredBuilder(java.lang.String,java.lang.String) is not applicable (actual and formal argument lists differ in length)

 

이 에러는 필수 파라미터를 받는 빌더 메서드가 firstName과 lastName을 요구하는데,

설정하지 않고 build() 메서드를 호출하면 빌더 메서드의 시그니처와 일치하지 않아 컴파일 에러가 발생합니다. 


 

오늘은 디자인 패턴 스터디 5번째인 빌더 패턴에 대하여 공부하였습니다.
평소에도 자주 보던 패턴이였는데, 
자세하게 사용법을 보니, 더 잘 쓰고 싶다는 욕심이 생기네요 :>

모두 즐거운 개발공부되세요~ 
반응형