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

주제 선정의 이유

미션을 진행하다 보니 도메인이 너무 많은 필드를 가지고 있어 생성자의 유지보수에 어려움을 겪었습니다.

public class Section {
    private final Long id;
    private final Station upStation;
    private final Station downStation;
    private final int distance;
}

위와 같은 도메인의 생성자를 호출할 때, upStationdownStation이 뒤바뀌거나 한다면?

Section 강남_역삼 = new Section(1L, 강남, 역삼, 1);
Section 강남_ = new Section(1L, 역삼, 강남, 1);

둘 사이의 구분을 하기가 힘듭니다. 어떤 매개변수 자리에 어떤 값이 들어가야 하는지 헷갈릴 가능성이 크죠.

지금은 비록 생성자 매개변수가 4개라 쉽게 이해할 수 있고, IDE의 도움을 받으면 좀 더 수월하게 매개변수를 집어넣을 수도 있지만 필드가 늘어난다면 가독성은 점점 더 안좋아질 것입니다.

게다가 이번 미션을 진행하면서 DB 저장 전에는 id 값이 비어 있는 객체를, DB 저장 후에는 id 값이 들어 있는 객체를 생성해야 하니 자동적으로 점층적 생성자 패턴까지 사용하게 되면서, 생성자가 많아지고 관리하기 어렵게 되었습니다.

어떻게 하면 객체 생성의 가독성과 유지보수를 챙길 수 있을까 하다가 이펙티브 자바에서 읽은 빌더 패턴에 대해 생각이 났습니다.

빌더 패턴

빌더 패턴은 GoF 디자인 패턴 중의 하나입니다. 빌더 패턴은 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비한 패턴으로, 클라이언트가 필요한 객체를 직접 만드는 대신, 필수 매개변수 만으로 생성자를 호출해 빌더 객체를 얻고, 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한 뒤, 마지막으로 build 메서드를 호출해 객체(일반적으로는 불변인)를 얻는 방식입니다.

예를 들어 앞선 Section 클래스를 빌더 패턴의 형태로 바꿔본다면,

public class Section {
    private final Long id;
    private final Station upStation;
    private final Station downStation;
    private final int distance;

    private Section(Builder builder) {
        this.id = builder.id;
        this.upStation = builder.upStation;
        this.downStation = builder.downStation;
        this.distance = builder.distance;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private Long id;
        private Station upStation;
        private Station downStation;
        private int distance;

        private Builder() {
        }

        public Builder id(Long id) {
            this.id = id;
            return this;
        }

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

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

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

        public Section build() {
            return new Section(id, upStation, downStation, distance);
        }
    }
}

(책에서는 Builder의 생성자를 public으로 만들고 원본 객체의 new 키워드와 Builder의 생성자를 함께 사용하는 방식으로 사용했지만, 저는 생성하고자 하는 객체 내에 builder() 라는 빌더를 반환해주는 정적 팩터리 메서드를 만들어주고 해당 메서드를 호출하는 것이 new 키워드의 사용도 없앨 수 있고 가독성도 더 좋다고 생각하기 때문에 이와 같은 방법을 사용합니다.)

와 같이 작성하고,

Section 강남_역삼 = Section.builder()
                .id(1L)
                .upStation(강남)
                .downStation(역삼)
                .distance(1)
                .build();

와 같이 객체를 생성해 줄 수 있습니다. 만약 DB에 저장하기 전이라서 id 값이 없는 객체라면

Section 강남_역삼 = Section.builder()
                .upStation(강남)
                .downStation(역삼)
                .distance(1)
                .build();