빌더 패턴
접근
많은 필드와 중첩된 객체들을 단계별로 초기화해야 할 때 많은 매개 변수를 포함한 거대 생성자를 사용하면 코드가 복잡해진다.
반드시 필요하지 않은 값들도 매개 변수로 정의되므로 null을 입력해야만 하는데, 이러면 가독성이 안좋아진다.
객체 생성 코드를 추출해 빌더 클래스에 단계 별로 구성 요소를 생성하도록 해야 한다.
개념
클라이언트가 객체를 생성할 때 다양한 방법으로 생성할 수 있도록 빌더 클래스를 제공하는 패턴이다.
복합 객체 구조를 생성할 때 많이 쓰인다.
아래와 같이 Builder 인터페이스를 구현하여 다양한 생성 단계를 가진 빌더 객체를 만들 수 있다. 각 빌더 객체로부터 얻은 결과 객체는 같은 클래스 계층구조 또는 인터페이스에 속할 필요가 없다. 디렉터 클래스를 생성하면 빌더 객체로 객체를 생성하는 방법을 캡슐화할 수 있다.

아래와 같이 점층적 생성자로 인해 코드가 복잡해질 경우 빌더 패턴을 사용해 클라이언트가 필요한 단계들만 설정해 객체를 생성하도록 하면 된다.
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// …
PizzaBuilder pb = new PizzaBuilder();
pb.size("LARGE");
pb.crust("Cheese");
pb.topping("pineapple", "olive");
Pizza pizza = pb.getPizza();
빌더의 생성 단계들을 재귀적으로 동작하도록 하여 컴포지트 패턴의 트리를 생성할 수 있다.
장단점
장점
복합 객체 생성 과정을 캡슐화한다.
팩토리 패턴과 달리 여러 단계와 다양한 절차를 거쳐 객체를 만들 수 있다.
클라이언트는 추상 인터페이스만 볼 수 있으므로 실제 빌더 구현체를 쉽게 바꿀 수 있다.
단점
팩토리를 사용할 때 보다 클라이언트에 관해 많이 알아야 한다.
사용 방법
객체의 공통 생성 단계를 정의하고 빌더 인터페이스에서 이를 선언한다.
빌더 인터페이스를 구현하는 클래스를 만든다. 필요하다면 디렉터 클래스도 만든다.
클라이언트 코드에서 빌더 객체나 디렉터 객체를 사용해 원하는 객체를 생성한다.
예시
아래에서 소개할 두 예시는 사실 완벽한 GoF 빌더 패턴은 아니다. 보통 GoF에서 제시한 것 처럼 빌더 인터페이스와 구현체를 따로 두지 않고 간편하게 정적 내부 클래스로 빌더를 정의한다.
Spring Cacheable의 빌더
빌더 클래스를 정적 내부 클래스로 선언하고, 빌더 객체를 얻기 위해 builder 정적 메서드를 제공한다.
다양한 초기화 메서드를 사용해 원하는 값들을 설정하고 build 메서드를 호출하면 생성된 객체를 얻을 수 있다.
public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) {
Assert.notNull(cacheWriter, "CacheWriter must not be null");
return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter);
}
public static class RedisCacheManagerBuilder {
public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWriter) {
Assert.notNull(cacheWriter, "CacheWriter must not be null");
return new RedisCacheManagerBuilder(cacheWriter);
}
// ...
private final Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
private RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
private @Nullable RedisCacheWriter cacheWriter;
private RedisCacheManagerBuilder(RedisCacheWriter cacheWriter) {
this.cacheWriter = cacheWriter;
}
public RedisCacheManagerBuilder allowCreateOnMissingCache(boolean allowRuntimeCacheCreation) {
this.allowRuntimeCacheCreation = allowRuntimeCacheCreation;
return this;
}
// ...
public RedisCacheManager build() {
Assert.state(cacheWriter != null, "CacheWriter must not be null;"
+ " You can provide one via 'RedisCacheManagerBuilder#cacheWriter(RedisCacheWriter)'");
RedisCacheWriter resolvedCacheWriter = !CacheStatisticsCollector.none().equals(this.statisticsCollector)
? this.cacheWriter.withStatisticsCollector(this.statisticsCollector)
: this.cacheWriter;
RedisCacheManager cacheManager = newRedisCacheManager(resolvedCacheWriter);
cacheManager.setTransactionAware(this.enableTransactions);
return cacheManager;
}
}
롬복의 @Builder
아래와 같이 필요한 인자를 입력받는 생성자에
@Builder
어노테이션을 붙이면 컴파일 시 롬복이 자동으로 빌더 클래스를 추가해준다.
@Builder
public User(String username, String password, String email, String phoneNumber, Role role) {
this.username = username;
this.password = password;
this.email = email;
this.phoneNumber = phoneNumber;
this.role = role;
}
아래는 컴파일 후 class 파일을 디컴파일해본 결과이며, 자동으로 UserBuilder 클래스가 생성된 것을 확인할 수 있다.
public class User {
// ...
@Generated
public static UserBuilder builder() {
return new UserBuilder();
}
@Generated
public static class UserBuilder {
@Generated
private String username;
@Generated
private String password;
@Generated
private String email;
@Generated
private String phoneNumber;
@Generated
private Role role;
@Generated
UserBuilder() {
}
@Generated
public UserBuilder username(final String username) {
this.username = username;
return this;
}
@Generated
public UserBuilder password(final String password) {
this.password = password;
return this;
}
@Generated
public UserBuilder email(final String email) {
this.email = email;
return this;
}
@Generated
public UserBuilder phoneNumber(final String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
@Generated
public UserBuilder role(final Role role) {
this.role = role;
return this;
}
@Generated
public User build() {
return new User(this.username, this.password, this.email, this.phoneNumber, this.role);
}
@Generated
public String toString() {
String var10000 = this.username;
return "User.UserBuilder(username=" + var10000 + ", password=" + this.password + ", email=" + this.email + ", phoneNumber=" + this.phoneNumber + ", role=" + String.valueOf(this.role) + ")";
}
}
}
Last updated