생성자에 매개변수가 많다면 빌더를 고려하라
일단 빌더패턴 전에
빌더 패턴을 사용하기 전, 많은 선택적 매개변수를 처리하기 위해 아래 두 가지 패턴이 사용되었다.
점층적 생성자 패턴
정적 팩터리와 생성자에는 똑같은 제약이 하나 있다.
선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다.
그리하여 옛날 프로그래머들은 이럴 때 점층적 생성자 패턴을 즐겨 사용했고 한다.
정적팩터리와 생성자는 선택적 매개변수가 많을 떄 적절하게 대응하기 어렵다는 단점이 있다.
필드가 많은 객체가 있다고 가정하자.
public class HeeDo {
private Long id;
private List<String> members;
private String email;
private LocalDateTime createDateTime;
private boolean isOpen;
public HeeDo(Long id, List<String> members, String email, LocalDateTime createDateTime, boolean isOpen) {
this.id = id;
this.members = members;
this.email = email;
this.createDateTime = createDateTime;
this.isOpen = isOpen;
}
public HeeDo(Long id, LocalDateTime createDateTime) {
this.id = id;
this.createDateTime = createDateTime;
}
public HeeDo(Long id, List<String> members, LocalDateTime createDateTime) {
this.id = id;
this.members = members;
this.createDateTime = createDateTime;
}
}
예를 들어 식품 영양소 클래스에서 20개가 넘는 영양 정보들 (탄수화물, 지방, 단백질 총 칼로리 등 ...)이 들어가지만
정작 사용하는 변수들은 많지 않을 것입니다.
그렇기 때문에 예전에는 받는 매개변수가 다른 생성자를 각각 만들어 객체를 생성해주었다.
HeeDo 객체는 id와 createDateTime 필드를 제외하고 다른 필드는 생성할 때 초기화가 되어도 되고, 그러지 않아도 된다고 가정해 봅시다.
그러면 이런 질문들을 할 수 있게 된다.
정확히는 "필수 매개변수를 받는 생성자 1개, 그리고 선택 매개변수를 하나씩 늘여가며 생성자를 만드는 패턴"이다.
점층정 생성자 패턴의 단점은 명확하다.
클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러는 알아채지 못하고,
결국 런타임에 엉뚱한 동작을 하게 되는 것이다.
위의 문제에 대한 대안으로 자바빈즈 패턴을 사용할 수 있다.
그냥 단순히 Setter 패턴이다.
자바 빈즈 패턴(JavaBeans Pattern)
기본 생성자로 객체를 생성한 후에, Setter를 호출하여 값을 고정 시키는 방법이다.
자바 빈즈 패턴은
자바 빈즈 패턴을 사용하면 보시다시피
더 읽기 쉬워지고 클라이언트는 자신이 초기화하고 싶은 필드만 골라서 초기화할 수 있다.
public class HeeDo {
private Long id;
private List<String> members;
private String email;
private LocalDateTime createDateTime;
private boolean isOpen;
public HeeDo() {}
public void setMembers(List<String> members) {
this.members = members;
}
public void setEmail(String email) {
this.email = email;
}
public void setOpen(boolean open) {
isOpen = open;
}
}
사용할 때에는 아래와 같이 사용합니다.
인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드가 된다.
HeeDo me = new HeeDo();
me.setName("fresh");
me.setAge(10);
me.setBirth(100070);
하지만 아래와 같은 단점을 가진다.
즉, Setter가 가지는 모든 단점을 가지게 된다는 것이다.
객체를 불변으로 만들 수 없는 것이다.
어딘가에서 일관성이 깨진 희도 인스턴스를 생성하고,어딘가에서 그 인스턴스를 사용하게 되면 런타임에 문제가 발생하게 될 것이다.
하지만, 이미 객체를 생성하는 부분과 문제가 발생한 코드가 물리적으로 멀리 떨어져 있으므로 디버깅이 쉽지 않습니다.
이에 대한 대안으로
스레드 안전성를 얻기 위해서는 객체를 수동으로 얼리는 (Freezing) 방법을 사용할 수 있다는데,
실전에서는 다루기 어려워 쓰이지 않는다고 한다.
한번 찾아보려고 했으나, 어떤 기법인지 찾기가 쉽지 않았습니다.
(그만큼 잘 안쓰니까 그런게 아닐까,,?)
빌더패턴 (Builder Pattern)
빌더 패턴은 선택적 매개변수가 많은 상황에서 생성자 혹은 정적 팩토리 메소드보다 더 유용하게 사용할 수 있다.
그리고 빌더 패턴은 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비하였다고, 말할 수 있다.
(=보통 롬복을 사용하기 때문에 굉장히 낯설었다)
클라이언트에서 필수 매개변수만 가지고 있는 생성자를 호출해서 객체를 만든 후,
필요하면 Setter메소드로 선택 매개변수의 값을 추가한다.
코드를 보자면, 필수 매개변수를 생성자로 받는 Builder 생성자와
선택 매개변수를 매개변수로 받는 일종의 Setter를 사용하여 구현할 수 있다.
public class HeeDo {
private final String name;
private final Integer age;
private final Integer birth;
private final boolean isStudent = true;
public static class Builder {
// 필수 매개변수
private final String name;
private final Integer age;
// 선택 매개변수
private Integer birth = 000000;
private boolean isStudent = true;
// 필수 매개변수만 받는 생성자 호출
public Builder(String name, Integer age) {
this.name = name;
this.age = age;
}
// setter
public Builder birth(int val){
birth = val;
return this;
}
// setter
public Builder isStudent(boolean val){
isStudent = val;
return this;
}
// setter
public HeeDo build(){
return new HeeDo(this);
}
}
private HeeDo (Builder builder) {
name = builder.name;
age = builder.age;
birth = builder.birth;
isStudent = builder.isStudent;
}
}
롬복의 @Builder 를 사용했다면 굉장히 익숙한 코드일 것이다.
이러한 빌더 패턴 코드는 쓰기 쉽고, 읽기도 쉽다.
그리고 이는 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
return User.builder()
.username(member.getLoginId())
.password(member.getMemberPwd())
.roles("USER") // 권한 이름
.build();
}
즉, new User(member.getLoginId(), member.getMemberPwd(), "USER"); 이거랑 같은 코드인 것이다.
이러한 빌더 패턴은
가변인수 매개변수를 여러개 사용할 수 있다는 이점이 있는데, 이는 생성자로는 누릴 수 없는 사소한 이점 중 하나이다.
또한 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
그러나 단점으로는 객체를 만들기 위해서 빌더부터 만들어야 한다는 것이다.
빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.
정리
장점 : "불변"을 보장할 수도 있고, 가독성도 훨씬 좋아진다.
단점 : 빌더를 만드는 시간적인 비용이 발생한다.
그리고 책에서는 매개변수가 4개 이상 되어야 그 값어치를 한다고 명시되어있다.
마무리
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.
빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.