이 내용은 이펙티브 자바 Effective Java 3/E
를 읽으면서 정리한 내용을 포함하고 있습니다.
정적 팩토리 메소드와 생성자를 이용하다 보면 제약사항이 생겨지게 됩니다. 매개변수가 많아지면 적절히 대응을 하기가 어려워집니다. 생성자에 매개변수가 많아졌을 경우 어떠한 패턴들을 통해서 처리할 수 있는지 알아보도록 하겠습니다.
점층적 생성자 패턴
주문 아이템 도메인은 테스트용 정적 팩토리 메소드를 포함하여 아래와 같이 사용하고 있습니다. 여기에서 필드가 추가된다면 정적 팩토리 메소드는 점점 추가될 수 있습니다.
public class OrderItem {
private Long seq;
private Long orderId;
private Long menuId;
private Long quantity;
protected OrderItem() {
}
private OrderItem(final Long seq, final Long orderId, final Long menuId, final Long quantity) {
this.seq = seq;
this.orderId = orderId;
this.menuId = menuId;
this.quantity = quantity;
}
public static OrderItem of(final Long menuId, final long quantity) {
return new OrderItem(null, null, menuId, quantity);
}
public static OrderItem of(final Long orderId, final Long menuId, final long quantity) {
return new OrderItem(null, orderId, menuId, quantity);
}
public static OrderItem of(final Long seq, final Long orderId, final Long menuId, long quantity) {
return new OrderItem(seq, orderId, menuId, quantity);
}
...
}
이러한 방식은 매개변수가 많아질때 해결할 수 있는 대안 중 하나인 점층적 생성자 패턴
이라고 합니다. 필요한 정적 팩토리 메소드를 골라서 사용하면 됩니다. 점층적 생성자 패턴
도 사용할 수는 있겠지만, 우리가 사용하는 애플리케이션의 코드는 이것보다 더 복잡할 것입니다. 또한 직접 개발하기 위해서 사용한다면 코드를 작성하거나 읽기 어려울 것입니다.
자바빈즈 패턴
또 다른 대안인 자바빈즈 패턴이 있습니다. 자바빈즈 패턴은 매개변수가 없는 생성자를 객체로 만든 후, 세터 메소드를 호출하여 원하는 매개변수의 값을 설정하는 방식입니다.
public class OrderItem {
private Long seq;
private Long orderId;
private Long menuId;
private Long quantity;
public OrderItem(){}
public void setSeq(Long seq) { this.seq = seq; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public void setMenuId(Long menuId) { this.menuId = menuId; }
public void setQuantity(Long quantity) { this.quantity = quantity; }
...
}
하지만 이 방법은 심각한 단점을 지니고 있습니다. 자바빈즈 패턴에서는 객체 하나를 만들기 위해서 setter 메서드를 여러 번 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됩니다. 그리고 setter 메서드를 public 으로 열어놓게 되면 어디에서 변경되어서 문제가 생길지 모르게 됩니다.
즉, 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없습니다.
빌더 패턴
마지막 대안으로는 빌더 패턴
이 있습니다. 필요한 객체를 직접 만드는 대신에 필수 매개변수만으로 생성자 또는 정적 팩토리 메소드를 호출해 빌더 객체를 얻습니다. 그런 다음 빌더 객체가 제공하는 일종의 setter 메소드들로 원하는 선택 매개변수들을 설정할 수 있습니다. 마지막으로 매개변수가 없는 build 메서드를 호출하여 불변 객체를 얻습니다.
public class OrderItem {
private Long seq;
private Long orderId;
private Long menuId;
private Long quantity;
protected OrderItem() {}
private OrderItem(final Long seq, final Long orderId, final Long menuId, final Long quantity) {
this.seq = seq;
this.orderId = orderId;
this.menuId = menuId;
this.quantity = quantity;
}
// Builder 정적 메소드
public static OrderItemBuilder builder() {
return new OrderItemBuilder();
}
// Buider 클래스
public static class OrderItemBuilder {
private Long seq;
private Long orderId;
private Long menuId;
private Long quantity;
OrderItemBuilder() {
}
public OrderItemBuilder seq(final Long seq) {
this.seq = seq;
return this;
}
public OrderItemBuilder orderId(final Long orderId) {
this.orderId = orderId;
return this;
}
public OrderItemBuilder menuId(final Long menuId) {
this.menuId = menuId;
return this;
}
public OrderItemBuilder quantity(final Long quantity) {
this.quantity = quantity;
return this;
}
public OrderItem build() {
return new OrderItem(this.seq, this.orderId, this.menuId, this.quantity);
}
public String toString() {
return "OrderItem.OrderItemBuilder(seq=" + this.seq + ", orderId=" + this.orderId + ", menuId=" + this.menuId + ", quantity=" + this.quantity +")";
}
}
...
}
모든 매개변수에 대한 객체가 필요할때는 다음과 같이 작성하면 됩니다.
@DisplayName("모든 항목에 대해서 주문 아이템을 생성한다.")
@Test()
void create_all_orderItem() {
final OrderItem orderItem = builder()
.seq(1L)
.orderId(1L)
.menuId(1L)
.quantity(10L)
.build();
assertAll(
() -> assertEquals(orderItem.getSeq(), 1L),
() -> assertEquals(orderItem.getOrderId(), 1L),
() -> assertEquals(orderItem.getMenuId(), 1L),
() -> assertEquals(orderItem.getQuantity(), 10L)
);
}
일부 매개변수가 필요없는 객체는 아래와 같이 작성하지 않으면 됩니다.
@DisplayName("메뉴 아이디를 제외한 주문 아이템을 생성한다.")
@Test()
void create_not_menu_orderItem() {
final OrderItem orderItem = builder()
.seq(1L)
.orderId(1L)
.quantity(10L)
.build();
assertAll(
() -> assertEquals(orderItem.getSeq(), 1L),
() -> assertEquals(orderItem.getOrderId(), 1L),
() -> assertEquals(orderItem.getMenuId(), null),
() -> assertEquals(orderItem.getQuantity(), 10L)
);
}
Builder 클래스를 생성하는 방법은 IDE에서 2가지 방법이 존재합니다.
- plugin ->
Builder Generator
설치해서 Builder Class 생성 - Lombok ->
@Builder
를 이용한 방법
장점
- 빌더 하나로 여러 객체를 순회하면서 만들 수 있습니다.
- 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있습니다.
- 객체마다 부여되는 특정한 일련번호와 같은 필드도 빌더가 채울 수 있습니다.
단, 특징에 비해서 고민이 되는 부분도 있습니다.
단점
- 점층적 생성자 패턴보다는 코드의 양이 많고 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다고 합니다.
- 물론 이 부분도 롬북으로 해결은 됩니다.
- 정적 팩토리 메소드의 큰 장점인 이름을 통해서 의도를 들어낼 수 있는 장점이 사라집니다.
빌더의 장점에 비해서 단점 또한 무시할 수 없습니다. 그리하여 현재 팀에서는 정적 팩토리 메소드를 사용하고 메소드의 명칭을 이해하기 쉽게 작성하는 것을 권장하고 있습니다.
정리
- 처리해야할 매개변수가 많은 경우에는 빌더 패턴을 선택하는 것도 방법이 될 수 있습니다.
- 매개변수가 많아도 정적 팩토리 메소드를 통해서 적절한 메소드 명칭을 통해서 의도를 드러내는 것도 방법이 될 수 있습니다.
'독서이야기 > 이펙티브 자바 3 edition' 카테고리의 다른 글
[아이템6] 불필요한 객체 생성을 피하라 (0) | 2022.09.03 |
---|---|
[아이템5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.09.03 |
[아이템4] 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.08.07 |
[아이템3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2022.08.07 |
[아이템1] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.08.07 |
댓글