필드가 많아지고 생성자에 매개변수가 많아진다면 코드가 복잡해집니다.
이럴 때 개발자들은 다양한 방법을 사용하곤 했습니다.
예제를 설명하기 위해, 제가 플레이했던 중세 시뮬레이션 게임인 '마운트앤블레이드'의 캐릭터를 만든다고 가정하겠습니다.
이렇게 생긴 게임입니다. 플레이어 외형을 정하기 위해 성별은 필수값으로 받는다고 가정하겠습니다.
플레이어의 나이, 체력, 마나, 공격력, 방어력은 선택변수 입니다.
필요한 매개변수를 점층적으로 받는 생성자입니다. 위 같은 코드는 단점이 명확합니다.
이번 예시에선 나이도 필수값으로 지정해보겠습니다.
public 생성자로 먼저 생성한 뒤에, setter를 통하여 값을 초기화하는 방식 입니다.
Player jihong = new Player();
jihong.setGender = true;
jihong.setAge = 26;
jihong.setHitPoint = 300;
...
점층적 생성자 패턴에서 엄청나게 많은 생성자를 만든 것에 비하면 코드가 정말 깔끔하고 헷갈리지도 않습니다.
하지만, 가장 중요한 불변이 아닙니다.
거기다가 필수값인 나이와, 성별을 주지 않는다면 제대로 동작하지 않고, 디버깅이 어렵게됩니다.
이런 단점들을 해소하고자 나온게 빌더 패턴입니다.
public class Player {
private final boolean gender; // 필수 true면 male, false면 female
private final int age; // 필수
private final int hitPoint;
private final int manaPoint;
private final int strikingPower;
private final int defensivePower;
public static class Builder {
// 필수값들
private final boolean gender;
private final int age;
// 필수 아닌 값, final이 아니고 기본값이 설정되어
// 있기 때문에 입력하지 않는다면 기본값으로 설정된다.
private int hitPoint = 100;
private int manaPoint = 100;
private int strikingPower = 30;
private int defensivePower = 30;
// 필수값 입력
public Builder(boolean gender, int age) {
this.gender = gender;
this.age = age;
}
// 메서드 이름을 필드명으로 줘서 가독성이 좋아진다!
public Builder hitPoint(int hitPoint) {
this.hitPoint = hitPoint;
// 자기 자신을 반환하기 때문에 메서드 체이닝이 가능
return this;
}
public Builder manaPoint(int manaPoint) {
this.manaPoint = manaPoint;
return this;
}
public Builder strikingPower(int strikingPower) {
this.strikingPower = strikingPower;
return this;
}
public Builder defensivePower(int defensivePower) {
this.defensivePower = defensivePower;
return this;
}
// 최종적으로 build()를 통해 본 클래스의 생성자를 호출
public Player build() {
return new Player(this);
}
}
// 본 클래스의 생성자는 빌더로 생성 가능하다.
public Player(Builder builder) {
this.gender = builder.gender;
this.age = builder.age;
this.hitPoint = builder.hitPoint;
this.manaPoint = builder.manaPoint;
this.strikingPower = builder.strikingPower;
this.defensivePower = builder.defensivePower;
}
}
---사용---
Player jihong = new Player.Builder(true, 26) // 필수값으로 빌더를 얻는다.
.hitPoint(300)
.manaPoint(150)
.defensivePower(30) // 순서가 바뀌어도 상관 x
.strikingPower(30) // 순서가 바뀌어도 상관 x
.build();
위의 코드를 통해 불변성과 가독성을 모두 얻을 수 있었습니다.
여담으로, 이런 패턴은 파이썬과 스칼라에 있는 네임드 옵셔널 파라미터를 흉내낸 것이라고 합니다.
정적 팩터리 메서드를 사용하면 Spring의 @Builder
어노테이션의 기본 형태와 같은 방식으로 빌더를 사용할 수 있습니다.
public class Player {
private final boolean gender; // 필수 true면 male, false면 female
private final int age; // 필수
private final int hitPoint;
private final int manaPoint;
private final int strikingPower;
private final int defensivePower;
public static class Builder {
private final boolean gender;
private final int age;
private int hitPoint = 100;
private int manaPoint = 100;
private int strikingPower = 30;
private int defensivePower = 30;
public Builder(boolean gender, int age) {
this.gender = gender;
this.age = age;
}
public Builder hitPoint(int hitPoint) {
this.hitPoint = hitPoint;
return this;
}
public Builder manaPoint(int manaPoint) {
this.manaPoint = manaPoint;
return this;
}
public Builder strikingPower(int strikingPower) {
this.strikingPower = strikingPower;
return this;
}
public Builder defensivePower(int defensivePower) {
this.defensivePower = defensivePower;
return this;
}
public Player build() {
return new Player(this);
}
}
public Player(Builder builder) {
this.gender = builder.gender;
this.age = builder.age;
this.hitPoint = builder.hitPoint;
this.manaPoint = builder.manaPoint;
this.strikingPower = builder.strikingPower;
this.defensivePower = builder.defensivePower;
}
// 추가
public static Builder builder(boolean gender, int age) {
return new Builder(gender, age);
}
}
---사용---
Player jihong2 = Player.builder(true, 26)
.hitPoint(300)
.manaPoint(150)
.defensivePower(30)
.strikingPower(30)
.build();
빌더 패턴은 계층적으로 설계된 클래스에서 사용하기 좋습니다.
(30 읽고 다시 작성)