[Design Pattern] Builder Pattern
Builder Pattern에 대하여 알아보도록 하겠습니다.
빌더 패턴이란??
인스턴스를 생성할 때, 생성자(Constructor)만을 통해서 생성하는 데는 어려움이 있습니다.
빌더 패턴은 이 문제를 기반으로 고안된 패턴 중 하나입니다.
예를 들면, 생성자 인자로 너무 많은 인자가 넘겨지는 경우 어떠한 인자가 어떠한 값을 나타내는지 확인하기 힘듭니다.
또 어떠한 인스턴스의 경우에는 특정 인자만으로 생성해야 하는 경우가 발생합니다.
이럴 경우, 특정 인자에 해당하는 값을 null로 전달해줘야 하는데,
이는 코드의 가독성 측면에서 매우 좋지 않다는 것을 직감적으로 알 수 있습니다.
코드를 통해 봐 보겠습니다.
id, pw, name, address, phoneNumber 등의 필드를 생성자로 초기화하는 클래스입니다.
인스턴스를 생성할 때 생성자로 아래와 같이 파라미터들을 넘겨주게 됩니다.
만약 인스턴스를 생성할때 id와 pw 만 알고 있다면 나머지 값들을 null로 채워서 보내주는 방법으로 인스턴스를 생성하게 됩니다.
위 같은 방법은 특정 파라미터가 어떠한 값을 나타내는지 확인하기 힘듭니다.
그렇다고 해서 점층적 생성자 패턴(telescoping constructor pattern)을 사용하게 되면 해당 클래스의 코드가 지저분해지고,
한 인스턴스를 생성할 때 사용하지 없는 코드들도 같이 생성되는 문제가 발생합니다.
점층적 생성자 패턴이란 클래스 내에 오버 로딩(Overloading)을 통해서 생성자를 여러 개 작성하는 것을 말합니다.
그렇다면 setter 메서드를 사용한 자바 빈 패턴(Java Bean pattern)은 어떨까? 아래 코드를 봐보겠습니다.
NoUseBuilder beanNub =new NoUseBuilder();
beanNub.setId("qjatjr1108");
beanNub.setPw("1234");
beanNub.setName("bsjo");
beanNub.setAddress("Junghwa 1-dong, Jungnang-gu, Seoul, Korea");
beanNub.setPhoneNumber("010-1234-1234");
클라이언트에서는 이런 식으로 객체를 생성하게 됩니다.
하지만 아래와 같은 단점을 갖게 됩니다.
- 함수 호출 1회로 객체 생성을 끝낼 수 없으므로 객체 일관성(consistency)이 일시적으로 깨질 수 있습니다.
- immutable 객체를 생성할 수 없는 단점이 있습니다.
그래서 나온 게 점층적 생성자 패턴과
자바 빈 패턴의 장점을 결합한 것이 바로 빌더 패턴입니다.
클라이언트 코드에서 필요한 객체를 직접 생성하는 대신, 그전에 필수 인자들을 전달하어 빌더 객체를 만든 뒤, 빌더 객체에 정의된 설정 메서드들을 호출하여 인스턴스를 생성하는 것입니다.
빌더 패턴을 적용하면 장점이 여러 가지 생깁니다.
- 우선 인스턴스를 생성하는 데 있어서 필수적인 인자와 선택적인 인자를 구별할 수 있습니다.
- 선택적인 인자의 경우, 보다 가독성이 좋은 코드로 인자를 넘길 수 있습니다
- 객체 일관성을 깨지 않을 수 있습니다.
코드를 통해 살펴보겠습니다.
public interface Buildable {
T build();
}
일단 interface를 하나 만들어서 다른 builder 에도 적용할 수 있도록 할 수 있게 합니다.
public class Student {
private String id;
private String pw;
private String name;
private String address;
private String phoneNumber;
private Student(Builder builder) {
this.id = builder.id;
this.pw = builder.pw;
this.name = builder.name;
this.address = builder.address;
this.phoneNumber = builder.phoneNumber;
}
public String getId() {
return id;
}
public String getPw() {
return pw;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public String getPhoneNumber() {
return phoneNumber;
}
public static class Builder implements Buildable {
private final String id;
private final String pw;
private String name;
private String address;
private String phoneNumber;
@Override
public Student build() {
return new Student(this);
}
public Builder(String id, String pw) {
this.id = id;
this.pw = pw;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
}
}
Student 클래스 안에 Builder라는 정적 멤버 클래스(static member class)를 하나 만듭니다.
그리고 아까 만들어둔 interface를 구현하여줍니다.
최종적으로 build()라는 메서드를 통해 인스턴스를 생성할 합니다.
Student 클래스의 필드 중 id와 name을 필수 인자로 선택하여 final keyword를 추가하고, Builder를 생성할 때 인자로 넘겨받게 되었습니다.
그리고 나머지 선택적 인자들은 Builder를 return 하게 하여 Chaining을 통해 인자를 넘겨받을 수 있도록 하였습니다.
아래는 사용 예입니다.
Student student = new Student.Builder("qjatjr1108", "1234")
.name("bsjo")
.address("Junghwa 1-dong, Jungnang-gu, Seoul, Korea")
.phoneNumber("010-1234-1234")
.build();
여러 개의 파라미터를 전달하는데도 어떠한 값이 어떠한 의미인지 파악할 수 있으며, 필수 인자와 선택적 인자도 구별할 수 있게 되었습니다.
Lombok을 이용한 빌더 패턴 구현하기
이런 스타일의 빌더 패턴이라면 롬복의 @Builder 애노테이션으로 쉽게 사용할 수 있습니다.
다음과 같이 @Builder 애노테이션을 붙여주면 이펙티브 자바 스타일과 비슷한 빌더 패턴 코드가 빌드됩니다.
@Builder
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;
}
UseCase with Lombok
//chain
NutritionFacts facts = NutritionFacts.builder()
.calories(230)
.fat(10)
.build();
//not chain
NutritionFacts.NutritionFactsBuilder nutritionFactsBuilder = NutritionFacts.builder();
nutritionFactsBuilder.calories(230);
nutritionFactsBuilder.fat(10);
NutritionFacts nutritionFacts = nutritionFactsBuilder.build();
Kotlin에서 구현하기
kotlin을 사용할 경우 Builder 패턴을 굳이 사용할 필요가 없습니다.
kotlin에서는 생성자에서 초기값을 설정할 수 있기 때문에 반드시 필요한 변수 값만 입력받아 사용할 수 있습니다.
class User(var firstName: String,
var lastName: String,
var age: Int? = null) {
init {
if (age?.let { it <= 0 } == true) throw IllegalArgumentException("age must be positive")
}
}
또한 객체 생성 시점에 호출되는 init 메서드를 잘 활용하면, 잘못된 변수들에 대해서도 체크할 수 있기 때문에 대부분의 Builder는 굳이 사용할 필요가 없다고 느껴졌지만..
그래도 코틀린에서도 빌더 패턴을 적용할 여지는 충분하기 때문에 아래 예를 보도록 하겠습니다.
class MyDialog private constructor(
private val title: String,
private val text: String?,
private val onAccept: (() -> Unit)?
) {
class Builder(val title: String) {
private var text: String? = null
private var onAccept: (() -> Unit)? = null
fun setText(text: String?): Builder {
this.text = text
return this
}
fun setOnAccept(onAccept: (() -> Unit)?): Builder {
this.onAccept = onAccept
return this
}
fun build() = MyDialog(title, text, onAccept)
}
}
MyDialog는 비밀 생성자를 가지고 있고, 내부에 builder라는 클래스로 필드들을 초기화합니다.
사용 예는 아래입니다.
MyDialog.Builder("title")
.setText("hello?")
.setOnAccept { /**doSomethine*/ }
.build()
샘플 코드 보러 가기
'DesignPattern' 카테고리의 다른 글
[Design Pattern] template method pattern (4) | 2019.09.18 |
---|---|
[Design Pattern] Factory Method Pattern (4) | 2019.09.18 |
[Design Pattern] abstract factory pattern (5) | 2019.09.17 |
[Design Pattern] Singleton Pattern (4) | 2019.09.10 |
디자인패턴(DesignPattern)에 대하여 (5) | 2019.09.10 |
댓글