선택적으로 값을 추가할 수 있도록 코드를 수정해봤습니다.
필수 값들의 초기화 체이닝이 끝나면, 선택적으로 빌드할 수 있는 타입으로 반환되고,
이 타입은 남은 필드들과 빌드할 수 있는 기능을 가지고 있습니다.
지난 포스트에서 구조는 다음과 같았습니다.
FieldBuilder1에서부터 2, 3, BuildBuilder까지 체이닝 형태로 반환 타입이 연결되는 상황입니다.
BuildBuilder가 빌드하는 기능뿐만 아니라, 나머지 필드들을 초기화는 기능도 가지도록 수정하였습니다.
BuildBuilder에 나머지 필드를 초기화하는 기능을 추가하면서 OptionalBuilder로 이름을 바꾸고,
다른 FieldBuilder의 인터페이스 명은 가독성을 위해 접미어 Builder를 제외하였습니다.
코드는 아래와 같습니다.
public class Person {
private String firstName;
private String lastName;
private int age;
private String phoneNumber;
private Person(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phoneNumber = builder.phoneNumber;
}
public interface FirstName {
LastName firstName(String firstName);
}
public interface LastName {
OptionalBuilder lastName(String lastName);
}
public interface OptionalBuilder {
OptionalBuilder age(int age);
OptionalBuilder phoneNumber(String phoneNumber);
Person build();
}
public static class Builder implements FirstName, LastName, OptionalBuilder {
private String firstName;
private String lastName;
private int age;
private String phoneNumber;
private Builder() {}
public static FirstName builder() {
return new Builder();
}
@Override
public LastName firstName(String firstName) {
this.firstName = firstName;
return this;
}
@Override
public OptionalBuilder lastName(String lastName) {
this.lastName = lastName;
return this;
}
@Override
public OptionalBuilder age(int age) {
this.age = age;
return this;
}
@Override
public OptionalBuilder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
@Override
public Person build() {
return new Person(this);
}
}
@Override
public String toString() {
return "Person{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
", phoneNumber='" + phoneNumber + '\'' +
'}';
}
}
필수 타입인 firstName과 lastName만 인터페이스를 정의하고,
나머지 타입들은 OptionalBuilder에서 초기화할 수 있도록 하였습니다.
필수 타입 간에는 이전과 동일하게 반환 타입이 체이닝되고, 마지막 필수 타입은 OptionalBuilder로 캐스팅하여 반환합니다.
이 타입에서는 원하는 필드만 초기화하고 빌드를 수행할 수 있습니다.
이것으로 선택적으로 값들을 초기화하는 방식이 동작하게 되었습니다.
필수 타입과는 다르게, 선택적인 값들은 중복 호출을 제한하지 못합니다.
하지만 요구 사항에 따라 필드가 언제든 추가될 수 있는 상황을 고려해보면, 굳이 제한하는 것 보단 기존의 빌더 방식과 동일하게 열어두는 것도 괜찮을거라 생각됩니다.
필수 값들도 중복 호출을 굳이 제한해야하나 싶어서 개선할 방법을 고민해봤는데,
컴파일 타임에 필수 값이 누락된 에러를 감지하기에는 마땅한 방법이 떠오르지 않았습니다.
런타임에는 검사해낼 수 있겠지만, 불필요한 검사 과정이 추가되기도 하고, 컴파일 타임에 잡아내지 못한다면 크게 의미가 없을 것이라 생각되었습니다.
각 필수 값마다 필요한 인터페이스를 제거하고,
builder().required(firstName("Song"), lastName("HeeJae")) 와 같이 스태틱 메소드를 이용한 형태로 컴파일 타임에 잡아낼 수 있도록 만드는 방법도 생각해보긴 했습니다.
하지만 위 방식을 취하게 되면, 각 위치마다 필요한 파라미터가 무엇인지 확인하기 위해 코드를 열어봐야하는 상황은 여전히 지속되게 됩니다.
각 위치마다 필드에 강제되는 인스턴스를 생성해서 제약시킬 수도 없는 노릇이었습니다.
가독성은 확실히 좋아지지만 제가 초기(1편)에 추구했던, IDE 자체에서 필수 필드의 초기화 메소드만 자동으로 제시하면서 빠르게 인스턴스를 빌드할 수 있는 방법이 제한될 수도 있는 것입니다.
또는, 필수 타입과 선택 타입들에 대한 두 개의 인터페이스만 정의하여 사용하는 방법도 고려해봤지만,
생각만큼 깔끔한 코드가 나올 것 같지 않았고(ex. 필수 타입이 없는 상황), 이 방법 역시 컴파일 타임에 값이 누락되었는지 에러를 잡아내기 어려웠습니다.
그래서 결국 필수 필드들은 기존의 방식처럼 반환 타입의 체이닝을 유지하면서 중복을 제한하는 방식을 택하였습니다.
중간에 필드가 추가되었을 때 순서에 따라 코드를 다시 수정해야하는 문제 때문에 고려했던 내용이었는데,
다시 생각해보면, 기존의 생성자나 초기화 필드가 필요한 빌더 방식에서도, 어차피 새로운 필드가 추가되면 그 중간에 순서에 맞게 다시 코드를 수정해야합니다.
이러한 점들을 고려했을 때, 필수 값들 만큼은 순서에 따라서 제한하는게 크게 나쁠 것이라고 생각되지 않았습니다.
다음으로 해보고자 했던 것은, public 인터페이스의 노출을 방지하는 것이었습니다.
Person 내부의 Builder뿐만 아니라, Builder의 상위 타입 인터페이스들이 외부로 노출되고 있는 상황입니다.
이 때문에 내부가 드러남으로써 캡슐화가 깨지게 되고, 클라이언트와 결합도가 올라가게 됩니다.
확실히 좋은 현상은 아닙니다.
이를 해결하기 위해 interface의 접근 제어자를 바꿔보거나, 람다를 이용해서 초기화 코드를 작성해보려고 했지만, 생각대로 코드를 작성하기가 어려웠습니다.
아무래도 이 부분은 조금 더 고민해봐야 할 것 같습니다.
코드는 아래 링크의 custom.v2 패키지에서 만나볼 수 있습니다.
'Java' 카테고리의 다른 글
Java(자바) Optional 클래스 (0) | 2021.11.11 |
---|---|
Java 8 Default Method 정리 및 우선 순위 알아보기 (0) | 2021.11.10 |
빌더 패턴(Builder pattern) 변형하기 - 2 (0) | 2021.11.09 |
빌더 패턴(Builder pattern) 변형하기 - 1 (0) | 2021.11.09 |
double brace initialization (0) | 2021.11.09 |