📌 빌더 패턴(Builder Pattern) 이란?
빌더 패턴은 복잡한 객체를 생성하는 방법을 정의하는 클래스와 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴이다.
많은 선택적인 멤버 변수나 지속성 없는 상태 값들에 대해 처리해야 하는 문제들을 해결한다.
팩토리 패턴이나 추상 팩토리 패턴에서 생성해야 하는 클래스에 대한 속성 값이 많을 때는 아래와 같은 이슈가 있다.
- 클라이언트 프로그램은 팩토리 클래스로 많은 파라미터를 넘겨줄 때 타입, 순서 등에 대한 관리가 어려워 에러가 발생할 확률이 높아진다.
- 경우에 따라 필요 없는 파라미터들에 대해 팩토리 클래스에 null 값을 넘겨주어야 한다.
- 생성해야 하는 서브클래스가 무거워지고 복잡해짐에 따라 팩토리 클래스 또한 복잡해진다.
빌더 패턴은 위의 문제를 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값은 생성자를 통해, 선택 값은 메서드를 통해 값을 입력받고 build() 메서드를 통해 최종적으로 하나의 인스턴스를 리턴하는 방식이다.
📌 빌더 패턴의 구조
[ Director ]
Builder를 이용하여 객체를 생성(반환).
[ Builder ]
객체를 생성하는 방법과 형태를 가진 인터페이스.
[ ConcreteBuilder ]
Builder 인터페이스의 구현체로 구현하고자하는 각 객체의 특징을 정의.
[ Product ]
Director가 Builder를 이용해 생성하는 결과 객체.
📌 빌더 패턴의 탄생 배경
빌더 패턴의 등장 이전에 생성자의 복잡성을 해결하는데 사용한 기법들이 있었다.
[ 점층적 생성자 패턴 ]
생성자를 필수 매개변수부터 모든 매개변수 까지 하나씩 늘리는 방법이다.
public class Burger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
// 모든 필드들이 있는 생성자
public Burger(int bun, int patty, int cheese, int lettuce, int tomato) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.lettuce = lettuce;
this.tomato = tomato;
}
// 토마토 제외 생성자
public Burger(int bun, int patty, int cheese, int lettuce) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.lettuce = lettuce;
}
// 토마토, 양상추 제외 생성자
public Burger(int bun, int patty, int cheese) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
}
// 필수 생성자
public Burger(int bun, int patty) {
this.bun = bun;
this.patty = patty;
}
}
점층적 생성자 패턴은 클래스 인스턴스 필드들이 많으면 많을수록 생성자에 들어갈 인자의 수가 늘어나 클라이언트 코드를 작성하거나 읽기에 어려운 문제가 있다.
또한 타입이 다양할 수록 생성자 메서드 수가 기하급수적으로 늘어남에 따라 가독성이나 유지보수 측면에서 좋지 않다.
[ 자바 빈즈(Java Beans) 패턴 ]
썬 마이크로시스템즈에서 정의한 패턴으로, 매개변수가 없는 생성자로 객체를 만든 후 setter 메서드를 호출해 매개변수의 값을 설정하는 방식이다.
public class Burger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
public Burger() {}
public void setBun(int bun) {
this.bun = bun;
}
public void setPatty(int patty) {
this.patty = patty;
}
public void setCheese(int cheese) {
this.cheese = cheese;
}
public void setLettuce(int lettuce) {
this.lettuce = lettuce;
}
public void setTomato(int tomato) {
this.tomato = tomato;
}
}
점층적 생성자 패턴에서 나타난 생성자 오버로딩으로 인한 가독성 문제점이 사라지고 선택적인 파라미터에 대해 해당되는 setter 메서드를 호출함으로써 유연한 객체 생성이 가능해졌지만, 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체 생성 시점에 모든 값들을 주입하지 않아 일관성이 무너진 상태에 놓이게 된다.
일관성이 무너지는 문제 때문에 자바 빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안정성을 얻을 수 없다.
[ 빌더 패턴 ]
위 패턴들의 문제점들을 해결하기 위해 별도의 Builder 클래스를 만들어 메서드 체이닝 형태로 호출함으로써 자연스럽게 인스턴스를 구성하고 마지막에 build() 메서드를 통해 최종적으로 객체를 생성하는 방식이다.
public class Burger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
// Builder 클래스 정의
public static class Builder {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수에 대한 기본값 설정
private int cheese = 1;
private int lettuce = 1;
private int tomato = 1;
// 필수 매개변수를 받는 생성자
public Builder(int bun, int patty) {
this.bun = bun;
this.patty = patty;
}
// 선택 매개변수에 대한 메서드
public Builder cheese(int cheese) {
this.cheese = cheese;
return this; // 자기 자신을 리턴하여 메서드 체이닝 구현
}
public Builder lettuce(int lettuce) {
this.lettuce = lettuce;
return this;
}
public Builder tomato(int tomato) {
this.tomato = tomato;
return this;
}
public Burger build() {
return new Burger(this);
}
}
// Burger 클래스의 private 생성자
private Burger(BurgerBuilder builder) {
this.bun = builder.bun;
this.patty = builder.patty;
this.cheese = builder.cheese;
this.lettuce = builder.lettuce;
this.tomato = builder.tomato;
}
}
빌더 패턴을 이용하면 데이터의 순서에 상관없이 객체를 만들어내 생성자 인자 순서를 파악할 필요도 없고, 잘못된 값을 넣는 실수도 하지 않게 된다.
점층적 생성자 패턴과 자바 빈즈 패턴 두 가지의 장점만을 취하였다고 볼 수 있다.
📌 빌더 패턴의 장 / 단점
[ 장점 ]
- 객체들을 단계별로 생성하거나 생성 단계들을 연기하거나 재귀적으로 단계들을 실행할 수 있다.
- 제품들의 다양한 표현을 만들 때 같은 생성 코드를 재사용할 수 있다.
- 자바에서 지원하지 않는 디폴트 매개변수를 간접적으로 지원할 수 있다.
- 객체 인스턴스의 목적에 따른 초기화가 필수인 멤버 변수와 선택적 멤버 변수를 분리할 수 있다.
[ 단점 ]
- 코드의 전반적인 복잡성이 증가한다.
- 매번 메서드를 호출하여 빌더를 거쳐 인스턴스화 하기 때문에 생성자보다 성능이 떨어진다.
📌 Lombok 의 @Builder
보일러 플레이트 코드를 줄여주는 롬복 라이브러리에서는 편하게 빌더 패턴을 이용하기 위해 별도의 어노테이션을 지원한다.
클래스에 @Builder 어노테이션만 붙여주면 클래스를 컴파일할 때 자동으로 클래스 내부에 빌더 API가 만들어진다.
@AllArgsConstructor
@Builder
public class Burger {
// 필수 매개변수
@NonNull
private int bun;
@NonNull
private int patty;
// 선택 매개변수
@Builder.Default
private int cheese = 1;
@Builder.Default
private int lettuce = 1;
@Builder.Default
private int tomato = 1;
}
[ @AllArgsConstructor ]
위의 빌더 패턴 구현에서 보았듯이 마지막에 build() 메서드로 모든 매개변수를 받는 생성자가 있어야 하기 때문에 지정한다. @Builder 만 있으면 컴파일 에러가 난다.
[ @NonNull ]
아쉽게도 롬복 @Builder에서는 필수값을 지정하는 방법이 없다. validation을 통해 검증하거나, @Builder 메서드의 이름을 바꾸고 직접 메서드를 작성해야 한다.
[ @Builder.Default ]
선택 매개변수들의 초기화를 담당한다.
📌 자바 속 빌더 패턴
'java.lang.StringBuilder' 에서 이름에서도 알 수 있듯 빌더 패턴을 사용한다.
StringBuilder 클래스는 문자열을 동적으로 조작하는 데 사용되며, 내부적으로 빌더 패턴을 적용한다.
StringBuilder strBuilder = new StringBuilder();
strBuilder.append("Hello");
strBuilder.append(" ");
strBuilder.append("World!");
String result = strBuilder.toString();
각각의 append() 메서드를 통해 문자열을 덧붙일 수 있고, 최종적으로 toString() 메서드를 호출하여 문자열을 얻을 수 있다.
📌 정리
빌더 패턴은 객체 생성 과정을 추상화하여 복잡한 객체를 생성하고 조립하는 데 사용되는 디자인 패턴이다. 다음과 같을 때 사용하면 좋다
- 객체가 많은 속성을 가지고, 이를 설정하고 초기화하는 과정이 복잡한 경우 (복잡한 객체 생성)
- 객체의 여러 부분이 서로 다른 방식으로 조합될 수 있을 때 (다양한 객체 구성)
- 일부 옵션이 선택적일 때 (선택적 매개변수의 문제)
빌더 패턴을 사용하면 아래와 같은 이점을 얻을 수 있다.
- 객체 생성을 위한 각 단계를 명시적으로 나타내기에 가독성이 향상된다.
- 객체 생성 방법을 수정하지 않고도 새로운 객체 유형을 도입하거나 새로운 매개변수를 추가하는데 용이하기에 유연성 및 확장성이 증가된다.
- 객체 생성 시 필수 매개변수만 설정하고 나머지는 기본값을 사용할 수 있다.
'OOP > Design Pattern' 카테고리의 다른 글
[Design Pattern] 프로토타입 패턴(Prototype Pattern)이란? (0) | 2024.03.07 |
---|---|
[Design Pattern] 추상 팩토리 패턴(Abstract Factory Pattern)이란? (0) | 2024.03.06 |
[Design Pattern] 팩토리 메서드 패턴(Factory Method Pattern)이란? (0) | 2024.03.05 |
[Design Pattern] 싱글턴 패턴(Singleton Pattern)이란? (2) | 2024.02.28 |
[Design Pattern] 생성 패턴이란? (0) | 2024.02.26 |