생성자에서 같은 타입의 인자를 여러개 받을 때, Builder 패턴을 사용해보자
Spring

생성자에서 같은 타입의 인자를 여러개 받을 때, Builder 패턴을 사용해보자

https://joanne.tistory.com/90

 

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

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라. 생성자에 매개변수가 많을 때, 사용하는 점층적 생성자 패턴의 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 되는

joanne.tistory.com

 

사실 위 글에서 알 수 있듯이, 이펙티브 자바에서 저 내용을 읽은 적이 있다. 하지만 전혀••• 기억하지 못하고 있었다.

하지만 이번에, 위와 같은 리뷰를 받게 되었다.

 

우선, Section 클래스는 다음과 같은 필드를 갖고 있다.

 

public class Section {

    private Long id;
    private Station upStation;
    private Station downStation;
    private int distance;
}

 

그래서 생성자에서 같은 Station 타입의 값을 upStation, downStation 두 개 받게 된다.

이렇게 되면, Section을 생성할 때 upStation, downStation의 순서를 틀릴 수도 있고, 헷갈릴 가능성이 존재한다.

그리고 순서를 헷갈려 잘못 사용하게 될 경우 디버깅하기가 매우 어렵다!!

 

그래서 이번에는 위 클래스를 생성할 때 빌더 패턴을 적용해보고자 한다.

 

먼저, 빌더 패턴을 사용할 클래스 내부에 Builder라는 정적 클래스를 생성한다.

public class Section {

    private final Long id;
    private final Station upStation;
    private final Station downStation;
    private final int distance;

    // private 생성자로 변경 불가능한 객체 생성
    private Section(Builder builder) {
        this.id = builder.id;
        this.upStation = builder.upStation;
        this.downStation = builder.downStation;
        this.distance = builder.distance;
    }

    // 정적 내부 클래스
    public static class Builder {
        private final int distance;
        private Long id;
        private Station upStation;
        private Station downStation;
        
        // 생성자에서 타입으로 식별이 가능한 것은 Builder의 생성자 인자로 설정
        public Builder(Long id, int distance) {
            this.id = id;
            this.distance = distance;
        }

        public Builder(int distance) {
            this.distance = distance;
        }

        // 타입이 동일해 구분하기 어려운 것들은 체이닝을 통해 set할 수 있도록
        public Builder upStation(Station upStation) {
            this.upStation = upStation;
            return this;
        }

        public Builder downStation(Station downStation) {
            this.downStation = downStation;
            return this;
        }

        // 변경 불가능 객체 생성 메서드
        public Section build() {
            return new Section(this);
        }
    }
}

 

Builder라는 내부 정적 클래스를 통해 타입으로 식별 가능한 인자는 기본 생성자에 두고, 타입을 통해 구분이 어려운 인자는 따로 메서드를 구분하여 생성할 수 있도록 구현한다. 

그리고 마지막에 build()라는 메서드를 이용하여 실제 Section 객체를 완성하여 리턴하도록 구현한다.

 

빌더 패턴을 적용하기 전 Section을 생성할 땐 다음과 같이 할 수 있었다.

// given
final Long id = 1L;
final Station upStation = new Station(1L, "상행역");
final Station downStation = new Station(2L, "하행역");
final int distance = 10;

// when
final Section section = new Section(id, upStation, downStation, distance); //OK
final Section section = new Section(id, downStation, upStation, distance); //OK

upStation과 downStation의 순서가 변경되어도 타입이 같기 때문에 예외를 발생시키지 않았다. 그리고 구분하는 것이 어렵다.

 

빌더 패턴을 적용하여 객체를 생성하는 과정은 다음과 같다.

1. 필요한 객체를 직접 생성하는 대신 먼저 타입이 중복되지 않고 필수인자인 것들을 생성자에 전달하여 빌더 객체를 만든다.

2. 빌더 객체에 정의된 설정 메서드들을 호출하여 타입이 중복이거나 선택 인자인 것들을 추가한다.

3. build 메서드를 호출하여 변경 불가능한 객체를 생성한다.

// given
final Long id = 1L;
final Station upStation = new Station(1L, "상행역");
final Station downStation = new Station(2L, "하행역");
final int distance = 10;

// when
final Section section = new Section.Builder(id, distance)
                                   .upStation(upStation)
                                   .downStation(downStation)
                                   .build();

이 경우에도 물론 upStation과 downStation을 잘못 넣어도 Section이 생성되지만, 메서드명을 통해 upStation과 downStation을 구분할 수 있어 가독성이 좋아진다.

 

끝으로 빌더 패턴의 장단점을 요약하자면,

 

장점

- 작성하기 쉽고, 가독성이 좋아진다.

- 인자에 불변식을 적용할 수 있다.

- 하나의 빌더 객체로 여러 객체를 만들 수 있는 유연함을 갖는다.

 

단점

- 객체 생성 시 빌더 객체부터 생성해야하기 때문에 성능이 떨어질 수 있다.

- 인자가 충분히 많지 않거나, 타입이 중복되지 않는 경우에는 사용하지 않는 것이 성능 상 바람직하다.

- 하지만 인자가 늘어날 가능성이 높은 경우를 대비하여 사용하는 것이 나을수도..?