본문 바로가기

Reading Record/이펙티브자바

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

빌더패턴 전에 ,,

 

정적팩터리와 생성자는 "선택적 매개변수"가 많을 떄 적절하게 대응하기 어렵다는 단점이 있다.

만약 있을 수도 없을 수도 있는 필드가 많은 객체가 있다고 가정하자.

 

public class Javabom {
    private Long id;
    private List<String> members;
    private String email;
    private LocalDateTime createDateTime;
    private boolean isOpen;

    public Javabom(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 Javabom(Long id, LocalDateTime createDateTime) {
        this.id = id;
        this.createDateTime = createDateTime;
    }

    public Javabom(Long id, List<String> members, LocalDateTime createDateTime) {
        this.id = id;
        this.members = members;
        this.createDateTime = createDateTime;
    }
}

 

Javabom 객체는 id와 createDateTime 필드를 제외하고 다른 필드는 생성할 때 초기화가 되어도되고 그러지 않아도된다고 가정하자.

그럼 나머지 세개의 필드가 있고 없고에 대한 모든 생성자를 만들어야할까? 

앞으로 필드가 추가되면 그만큼 생성자가 늘어야할까?

 

 

위와같은 생성자패턴을 점층정 생성자 패턴이라고 한다.

정확히는 "필수 매개변수를 받는 생성자 1개, 그리고 선택매개변수를 하나씩 늘여가며 생성자를 만드는 패턴"이다. 

 

이 생성자패턴의 단점은 명확하다.

1. 클라이언트가 초기화하고 싶은 필드만 포함한 생성자가 없으면, 원치 않는 필드까지 불가피하게 초기화해야한다.

2. 복잡하고 읽기 어렵다.

 

위의 문제에 대한 대안으로 자바빈즈 패턴을 사용할 수 잆다.

말은 어려울 수 있으나 단순한 Setter 패턴이다

 

public class Javabom {
    private Long id;
    private List<String> members;
    private String email;
    private LocalDateTime createDateTime;
    private boolean isOpen;

    public Javabom(Long id, LocalDateTime createDateTime) {
    }

    public void setMembers(List<String> members) {
        this.members = members;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setOpen(boolean open) {
        isOpen = open;
    }
}

 

 

위 코드를 사용하면 더 읽기 쉬워지고 클라이언트는 자신이 초기화하고싶은 필드만 골라서 초기화할 수 있다.

하지만 아래와같은 단점을 가진다.

1. 객체 하나를 만들려면 메서드를 계속해서 호출해야하고,

2. 그 사이에 일관성을 유지하기 어렵다.

즉, Setter가 가지는 모든 단점을 가지게된다. 객체를 불변으로 만들 수 없는 것이다.

 

 

이에대한 대안으로 "freezing" 기법에 대한 언급이 나오는데, 정확히 어떤 기법인지 찾기가 쉽지 않다(그만큼 잘 안쓰니까 그런거 아닐까,,?) 

내 생각엔 아래처럼 사용하는 것이 아닌가 싶은데 ,, 조금 더 고민해봐야 할 것 같다 ㅎㅅㅎ

public class Javabom {
    private Long id;
    private List<String> members;
    private String email;
    private LocalDateTime createDateTime;
    private boolean isOpen;

    private boolean frozen;

    public Javabom(Long id, LocalDateTime createDateTime) {
    }

    public void setMembers(List<String> members) {
        this.members = members;
        this.frozen = false; //Member Setting을 하면 객체를 사용할 수 있다.
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setOpen(boolean open) {
        isOpen = open;
    }

    public void doSometing(){
        if(frozen){ // Check!
            throw new IllegalStateException("freezing!");
        }
    }
}

 

 

 

마지막 대안으로 나오는 것이 바로 "빌더패턴"이다.

 

빌더패턴

필수 매개변수로 객체를 생성하고 일종의 "Setter"를 사용하여 필수 매개변수를 초기화한 뒤 build() 메서드를 호출하여 완전한 객체를 생성하는 패턴이다.

"정적멤버클래스"를 사용하여 직접 구현할 수 있다.(보통 롬복을 사용하기 때문에 굉장히 낯설었다)

 

아래와 같이 필수매개변수를 생성자로 받는 Builder 생성자와 선택매개변수를 매개변수로받는 일종의 Setter를 사용하여 구현할 수 있다.

 

public class Javabom {
    private final Long id;
    private final List<String> members;
    private final String email;
    private final LocalDateTime createDateTime;
    private final boolean isOpen;

    public Javabom(Builder builder) {
        this.id = builder.id;
        this.members = builder.members;
        this.email = builder.email;
        this.createDateTime = builder.createDateTime;
        this.isOpen = builder.isOpen;
        // 검증식
    }

    public static class Builder{
        private final Long id;
        private final LocalDateTime createDateTime;

        private List<String> members = new ArrayList<>();
        private String email = "default@email.com";
        private boolean isOpen = false;

        public Builder(Long id, LocalDateTime createDateTime) {
            this.id = id;
            this.createDateTime = createDateTime;
        }

        public Builder members(List<String> members){
            this.members = members;
            return this;
        }

        public Builder email(String email){
            this.email = email;
            return this;
        }

        public Builder isOpen(boolean isOpen){
            this.isOpen = isOpen;
            return this;
        }

        public Javabom build(){
            return new Javabom(this);
        }
    }
}

 

 

클라이언트는 다음과 같이 사용한다.

@DisplayName("빌더를 사용한 패턴")
    @Test
    void builder(){
        Javabom javabom = new Javabom.Builder(1L, LocalDateTime.now())
                .email("javabom@gmail.com")
                .members(Arrays.asList("crud", "jyami", "bibab", "gobuk", "jy"))
                .isOpen(true)
                .build();
    }

 

롬복의 @Builder 를 사용했다면 굉장히 익숙한 코드일 것이다.

 

 

위와같이 사용하면

"불변"을 보장할 수도 있고, 가독성도 훨씬 좋아진다. 

 

 

빌더패턴은 계층적으로 설계된 클래스와 함꼐 사용하기에도 좋다.  아래 코드는 아이템 30(제네릭)의 이슈에 작성된 코드이다.

https://github.com/Java-Bom/ReadingRecord/issues/75

 

[아이템 30] 시뮬레이트한 셀프 관용구 · Issue #75 · Java-Bom/ReadingRecord

180p 시뮬레이트한 셀프관용구에 대한 설명이 나오는데, 이게 2장의 Builder패턴 pizza를 상속받은 nypizza 빌더패턴에 대한 이야기야, 이거 한번 구현해보고 실제로 왜 재귀적 타입한정을 써야 이런 ��

github.com

 

public abstract class PayCard {
    final Set<Benefit> benefits;

    PayCard(Builder<?> builder) {
        benefits = builder.benefits.clone();
    }

    public enum Benefit {
        POINT("포인트"), SALE("할인"), SUPPORT("연회비지원");

        Benefit(String benefit) {
        }
    }

    abstract static class Builder<T> { // 재귀적 타입한정
        EnumSet<Benefit> benefits = EnumSet.noneOf(Benefit.class);

        public T addBenefit(Benefit benefit) {
            this.benefits.add(benefit);
            return self();
        }

        abstract PayCard build();

        protected T self() {
            System.out.println("PayCardSelf");
            return (T) this;
        }
    }
}

public class LottePayCard extends PayCard {
    private final Sale sale;

    LottePayCard(Builder builder) {
        super(builder);
        sale = builder.sale;
    }

    public enum Sale {
        LOTTE_WORLD("롯데월드"), LOTTE_DEPT("롯데백화점");

        Sale(String sale) {
        }
    }

    public static class Builder extends PayCard.Builder<Builder> {
        private final Sale sale;

        public Builder(Sale sale) {
            this.sale = sale;
        }

        @Override
        LottePayCard build() {
            return new LottePayCard(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }
}

 

    @DisplayName("시뮬레이트한 셀프타입 관용구")
    @Test
    void simulate() {
        LottePayCard lottePayCard = new LottePayCard.Builder(LOTTE_DEPT)
                .addBenefit(PayCard.Benefit.POINT)
                .build();

    }

 

 

하위클래스가 상위타입의 메서드를 재정의하면 하위타입이 그 메서드를 사용할 때 하위타입이 반환한 것을 반환하게된다.(공변 반환 타이핑). 즉, PayCard의 build와 self 메서드는 LottePayCard에서 재정의한대로 LottePayCard 인스턴스를 런타임에 반환하게된다.

 

따라서, 아래 테스트코드처럼 클라이언트는 PayCard의 메서드인지아닌지 신경쓰지 않고 메서드체이닝의 이점을 누릴 수 있다.

 

 

그 외에도 빌더패턴을 사용하면 가변인수를 사용하거나 매개변수에 따라 다른 객체를 반환하는 등 생성자에비해 유연하게 사용할 수 있다.

하지만, 위처럼 직접 구현하는 비용이 들기 때문에 선택 매개변수가 너무 적으면 그 이점을 누리기 쉽지 않다.

 

책에서는 매개변수가 4개 이상 되어야 그 값어치를 한다고 명시되어있다.