생성자와 정적 팩토리 메서드는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 단점이 있다.
https://lsh2016.tistory.com/101
과거에 여러 블로그를 참조해서 위와 같은 내용으로 빌더 패턴을 정리한적이 있는데 이제와서 알고보니 현재 보고있는 이펙티브 자바에 있는 내용이였다.
간략히 설명하자면 점층적 생성자 패턴의 심각한 가독성과 자바빈즈 패턴의 무너진 객체 일관성을 지적하며 빌더 패턴을 사용해야 하는 이유에 대해 포스팅한 글이다.
빌더 패턴을 사용해야 하는 이유는 과거부터 잘 알고 있었지만 자바 기초가 부족해서 그런지 책에 있는 예제는 잘 이해가 안가서 비슷한 예제를 만들어보았다.
public abstract class Adventurer {
public enum Ability { STR, DEX, INT, LUK }
final Set<Ability> abilities;
abstract static class Builder<T extends Builder<T>>{ // 재귀적 타입 한정
EnumSet<Ability> abilities = EnumSet.noneOf(Ability.class);
public T increaseAbility(Ability ability){
abilities.add(Objects.requireNonNull(ability));
return self();
}
abstract Adventurer build();
protected abstract T self();
}
Adventurer(Builder<?> builder){
abilities = builder.abilities.clone(); // clone > 원래의 EnumSet과 동일한 열거형 상수를 복사, 원본과는 서로 독립적인 인스턴스임.
}
class Warrior extends Adventurer{
public enum Job { FIGHTER, SPEARMAN, PAGE }
private final Job job; // 필수 매개변수
private String name; // 선택 매개변수
public static class Builder extends Adventurer.Builder<Builder>{
private final Job job;
private String name;
public Builder(Job job){
this.job = Objects.requireNonNull(job);
}
public Builder name(String value){
this.name = value;
return this;
}
public Warrior build(){
return new Warrior(this);
}
protected Builder self() {
return this;
}
}
Warrior(Builder builder) {
super(builder);
job = builder.job;
name = builder.name;
}
}
class Mage extends Adventurer{
public enum Element { FLAME, ICE, LIGHT }
private final Element element;
public static class Builder extends Adventurer.Builder<Builder>{
private final Element element;
public Builder(Element element){
this.element = Objects.requireNonNull(element);
}
@Override
Mage build() {
return new Mage(this);
}
@Override
protected Builder self() {
return this;
}
}
Mage(Builder builder){
super(builder);
element = builder.element;
}
}
}
최상위 모험가 추상 클래스 Adventurer는 4가지의 능력치 enum값을 가질수 있고, 내부 정적 클래스 Builder가 있다.
전사 Warrior 클래스와 마법사 Mage 클래스는 Adventurer 클래스를 상속받고 있고, 하위 클래스는 모두 Adventurer.Builder를 각각 상속받아 Builder 내부 클래스를 가지고 있다.
여기서 주요하게 살펴볼 점은
1.제네릭(Generic)
2.시뮬레이트된 셀프 타입 (simulated self-type)
3.재귀적 타입 한정(Recursive Type Bound)
위 3가지가 있다.
차근차근 하나씩 살펴보자.
1. 제네릭(Generic)
제네릭을 사용하면 타입 안정성이 유지되고, 타입 캐스팅이 불필요하다.
...
abstract static class Builder<T extends Builder<T>>{
EnumSet<Ability> abilities = EnumSet.noneOf(Ability.class);
public T increaseAbility(Ability ability){
abilities.add(Objects.requireNonNull(ability));
return self();
}
abstract Adventurer build();
protected abstract T self();
}
...
위에서 T는 아직 어떤 값이 들어올지 모르지만, 상속받은 클래스를 생성하는 순간 구체화된다.
public static void main(String[] args) {
Warrior warrior = new Warrior.Builder(Warrior.Job.FIGHTER) // 1번
.increaseAbility(STR)
.increaseAbility(DEX)
.name("최강전사")
.build();
}
new Warrior.Builder()호출 시 내부 생성자는 암묵적으로 super()를 호출하여 부모 생성자를 호출할 것이다.
부모 생성자 호출시점에 T는 자동으로 Warrior.Builder타입으로 변경될 것이기 때문에 부모 클래스에 있는 메서드를 자식 타입으로 확정지을 수 있다.
2.시뮬레이트된 셀프 타입(simulated self-type)
...
abstract static class Builder<T extends Builder<T>>{
EnumSet<Ability> abilities = EnumSet.noneOf(Ability.class);
public T increaseAbility(Ability ability){
abilities.add(Objects.requireNonNull(ability));
return self(); // 자식 타입을 계속 리턴하므로 메서드 체이닝 유지 가능
}
abstract Adventurer build();
protected abstract T self(); // 자식 타입을 리턴
}
...
self()는 나중에 자식 클래스가 위 Builder 클래스를 구현했을 때, 자식 객체 본인을 리턴하게 하도록 하는 메서드이다.
self()의 반환 타입은 T로, 1번 제네릭의 장점을 살려 하위 타입으로 리턴하게 할 수 있다.
어떻게보면 제네릭의 장점이지만 책에선 Self 타입이 없는 자바를 위한 우회 방법을 "시뮬레이트한 셀프타입"(simulated self-type) 관용구라고 명시하기 때문에 따로 빼두었다.
만약 제네릭을 쓰지 않는다면 아래와 같은 문제가 발생한다.
abstract static class Builder{
EnumSet<Ability> abilities = EnumSet.noneOf(Ability.class);
public Builder increaseAbility(Ability ability){
abilities.add(Objects.requireNonNull(ability));
return self();
}
abstract Adventurer build();
protected abstract Builder self();
}
제네릭을 없애고 다시 Warrior 객체를 생성한다.
public static void main(String[] args) {
Warrior warrior = new Warrior.Builder(Warrior.Job.FIGHTER)
.increaseAbility(STR) // 반환타입 > Adventurer.Builder
.increaseAbility(DEX) // 반환타입 > Adventurer.Builder
.name("최강 전사") // CompileError
.build();
}
위에서 .increaseAbility()시점에 반환 타입은 Warrior.Builder타입이 아닌 Adventurer.Builder()타입이 될 것이기에 메서드 체이닝을 이어갈 수 없고, 타입변경이 필요하다는 에러와 함께 컴파일 에러가 발생한다.
3.재귀적 타입 한정(Recursive Type Bound)
재귀적 타입 한정은 클래스가 자기 자신을 매개변수화할 때 사용되며, 클래스가 자기 자신 또는 자신을 상속한 클래스를 제한하는데 사용된다.
abstract static class Builder<T extends Builder<T>>{
...
}
Builder<T extends Builder<T>>
T는 Builder의 하위 클래스이며, Builder<T>타입의 인스턴스여야 한다. 그런데 여기서 또다시 T는 builder 클래스의 하위 클래스의 타입을 가리킨다.
즉, 이 구조는 재귀적 구조로 Builder 클래스는 T를 통해 자신을 재귀적으로 참조하는 형태가 된다.
아래는 그냥 본인이 헷갈려서 정리한 타입 파라미터의 유무에 따른 결과이다.
class Builder<T extends Builder>{ // Raw use of parameterized class 'Builder' 경고,참고로 재귀적 타입 한정 X
...
}
class Builder<T extends Builder<T>>{ // 타입 안정성을 가짐
...
}
결론
어쩌다보니 본문은 자바를 해석하며 주르륵 써내려간 코드 해석문이지만 책에서 보여지는 큰틀에서 정리하자면 생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 적극 활용하자. 이다
'Java' 카테고리의 다른 글
Java) 인스턴스화를 막으려거든 private 생성자를 사용하라 (1) | 2024.11.07 |
---|---|
Java) 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2024.11.05 |
Java) 생성자 대신 정적 팩토리 메서드 (1) | 2024.10.30 |