정적 팩터리와 생성자는 모두 선택적인 매개변수가 많을때 적절히 대응하기 어렵다.
- 예를들어
- 필수 매개변수와 선택 매개변수 1개를 받는 생성자, 선택 매개변수 2개까지 받는 생성자... 선택적 매개변수 모두 받는 생성자 등등 모두 생성해야한다.
- 단점
- 작성하고, 읽기 어려움
- 만약 사용자가 설정하길 원치 않는 매개변수까지 포함되어 있어도 어쩔수없이 값을 설정해줘야함.
- 매개변수가 바껴도 컴파일러는 못잡아서 런타임 오류 발생 가능성
- 등등
매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8); //(1)
cocaCola.setCalories(100);
cocaCola.setSodium(35);
- 장점
- 앞선 생성자의 단점 해결. 인스턴스를 만들기 쉽고, 생성자 방식보단 읽기 쉬운 코드
- 단점
- 객체 하나를 만들려면 메서드를 여러개 호출해야함
- 객체가 완성되기 전까지는 일관성이 무너진 상태에 있다.
- 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.
- 예를들어 (1)에서 객체 사용하는 경우
- 반면 생성자패턴은 매개변수들이 유효한지 생성자에서만 확인하면 일관성 유지
- 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.
- getter, setter가 있어서 불변 클래스를 만들지 못함.
- (쓰레드 간에 공유 가능한 상태가 있으니까) 스레드 안정성 얻으려면 추가적인 수고(locking)필요하다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
- 특징
- 인자 순서 상관없다.
- 인자 많을수록 안전하게 객체 생성 가능
- 빌더패턴은 유효성 확인 할수 있다.
- (최대한 일찍 발견하려면) 각 빌더 생성자와 메서드에서 수행
- build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식 검사.
- (?) 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
- 상당히 유연하다.
- 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있다.
- 단점
- 빌더 생성 비용이 크지는 않지만 성능에 민감하면 문제가될 수도 있다.
- 생성자 패턴보다는 코드가 장황해져서 매개변수가 4개 이상은 되어야 값어치함
- but 시간이 지날수록 매개변수는 증가하는걸 고려하자.
- 예제코드 참조
앞서 builder를 모두 구하면 힘드니 Lombok라이브러리의 @Builder어노테이션을 활용하자.
- builder패턴 적용 클래스
@Entity
@Table(name = "refund")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Refund {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Embedded
private Account account;
@Embedded
private CreditCard creditCard;
@OneToOne
@JoinColumn(name = "order_id", nullable = false, updatable = false)
private Order order;
@Builder(builderClassName = "ByAccountBuilder", builderMethodName = "ByAccountBuilder") // 계좌 번호 기반 환불, Builder 이름을 부여해서 그에 따른 책임 부여, 그에 따른 필수 인자값 명확
public Refund(Account account, Order order) {
Assert.notNull(account, "account must not be null");
Assert.notNull(order, "order must not be null");
this.order = order;
this.account = account;
}
@Builder(builderClassName = "ByCreditBuilder", builderMethodName = "ByCreditBuilder") // 신용 카드 기반 환불, Builder 이름을 부여해서 그에 따른 책임 부여, 그에 따른 필수 인자값 명확
public Refund(CreditCard creditCard, Order order) {
Assert.notNull(creditCard, "creditCard must not be null");
Assert.notNull(order, "order must not be null");
this.order = order;
this.creditCard = creditCard;
}
}
- 테스트
public class RefundTest {
...
...
@Test
public void ByAccountBuilder_test() {
final Refund refund = Refund.ByAccountBuilder() // 빌더 이름으로 명확하게 그 의도를 드러 내고 있습니다.
.account(account)
.order(order)
.build();
assertThat(refund.getAccount()).isEqualTo(account);
assertThat(refund.getOrder()).isEqualTo(order);
}
@Test
public void ByCreditBuilder_test() {
final Refund refund = Refund.ByCreditBuilder() // 빌더 이름으로 명확하게 그 의도를 드러 내고 있습니다.
.creditCard(creditCard)
.order(order)
.build();
assertThat(refund.getCreditCard()).isEqualTo(creditCard);
assertThat(refund.getOrder()).isEqualTo(order);
}
}
- 필수 매개변수가 없을 경우 예외처리를 설정할 수 있다.
- builder 이름으로 책임을 부여하자.
- 예를들어, 주문에 대한 환불이 있을경우 신용카드 취소, 계좌 기반 환불이 있다.
- 각각 받아야할 정보들이 다른데 하나의 builder로 검증하기 힘들다.
- 이런경우 builder의 이름을 명확하게 해서 책임을 부여하자.
- Lombok 사용법
- Lombok에서 builder 안전하게 사용법
- Lombok builder 예제