본문 바로가기
독서이야기/이펙티브 자바 3 edition

[아이템2] 생성자에 매개변수가 많다면 빌더를 고려하라

by 사랑꾼이야 2022. 8. 7.
반응형

이 내용은 이펙티브 자바 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개 이상은 되어야 값어치를 한다고 합니다.
    • 물론 이 부분도 롬북으로 해결은 됩니다.
  • 정적 팩토리 메소드의 큰 장점인 이름을 통해서 의도를 들어낼 수 있는 장점이 사라집니다.

빌더의 장점에 비해서 단점 또한 무시할 수 없습니다. 그리하여 현재 팀에서는 정적 팩토리 메소드를 사용하고 메소드의 명칭을 이해하기 쉽게 작성하는 것을 권장하고 있습니다.

정리

  • 처리해야할 매개변수가 많은 경우에는 빌더 패턴을 선택하는 것도 방법이 될 수 있습니다.
  • 매개변수가 많아도 정적 팩토리 메소드를 통해서 적절한 메소드 명칭을 통해서 의도를 드러내는 것도 방법이 될 수 있습니다.
반응형

댓글