DEV ℧ Developer Diary

[EffectiveJava] item02 - 생성자에 매개변수가 많다면 빌더를 고려하라.

생서자에 매개변수가 많다면 빌더를 고려하라.

정적 팩터리와 생성자에는 똑같은 제약이 하나있다. 선택적 매개변수가 많을때 적절히 대응하기 어렵다는 점이다.

예를 들어, 다수의 항목을 가지고 있는 클래스가 있다고 생각해보자.

public class Music{
    private String title;           /* 제목 */
    private String artist;          /* 가수 */
    private String albumName;       /* 앨범명 */
    private String genre;           /* 장르 */
    private String releaseDate;     /* 발매일 */
}

1. 점층적 생성자 패턴(telescoping constructor pattern)

기존 다수의 항목을 가지고있던 클래스에서는 점층적 생성자 패턴을 사용해왔다.

public class Music{
    private String title;           /* 제목 */
    private String artist;          /* 가수 */
    private String albumName;       /* 앨범명 */
    private String genre;           /* 장르 */
    private String releaseDate;     /* 발매일 */

    public Music(String title) {
        this(title);
    }

    public Music(String title, String artist) {
        this(title, artist);
    }

    public Music(String title, String artist, String albumName) {
        this(title, artist, albumName);
    }

    public Music(String title, String artist, String albumName, String genre) {
        this(title, artist, albumName, genre);
    }

    public Music(String title, String artist, String albumName, String genre, String releaseDate) {
        this.title = title;
        this.artist = artist;
        this.albumName = albumName;
        this.genre = genre;
        this.releaseDate = releaseDate;
    }
}

보통 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수 없이 그런 매개변수에 도 값을 지정 해줘야한다.

Music music = new Music("그래서그래", "epikhigh", "EPIK HIGH IS HERE", "HIPHOP","20990101");

발매일을 모를 경우 어쩔수없이 발매일 데이터에 2099년01월01일과 같이 임의의 데이터를 넣어줘야 한다. 해당 항목이 5개 이상이었다면, 저런 데이터들은 걷잡을 수 없이 커진다.

즉, 점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트의 코드를 작성하거나 읽기가 어렵다.

  1. 코드를 읽을 때 각 값의 의미가 무엇인지?
  2. 매개변수가 총 몇개인지?
  3. 매개변수의 타입이 어떤것인지?

와 같은 문제점으로인해 버그로 이어 질 수 있다.

2. 자바빈즈 패턴(JavaBeans Pattern)

그렇다면 두번째 대안인 자바빈즈(JavaBeansPattern)을 보자. 자바빈즈 패턴이란 매개변수가 없는 생성자로 만든 후 세터 메서들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.

public class Music{
    private String title = "";           /* 제목 */
    private String artist = "";          /* 가수 */
    private String albumName = "";       /* 앨범명 */
    private String genre = "";           /* 장르 */
    private String releaseDate = "";     /* 발매일 */

    public Music(){}

    /* setter 메서드들 */
    public void setTitle(String title) { this.title = title; }
    public void setArtist(String artist) { this.artist = artist; }
    public void setAlbumName(String albumName) { this.albumName = albumName; }
    public void setGenre(String genre) { this.genre = genre; }
    public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; }
}

코드가 길어지긴 했지만 인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드가 되었다.

Music music = new Music();
music.setTitle("그래서그래");
music.setArtist("epikhigh");
music.setAlbumName("EPIK HIGH IS HERE");
music.setGenre("HIPHOP");

하지만 자바빈즈 또한 자신만의 치명적인 단점을 지니고 있는데,

자바빈즈 패턴에서는 객체 하나를 만들기 위해서는 메서드를 여러개 호출 해야하고, 객체가 완전히 생성되기 전까지 일관성이 무너진 상태가 된다.

위에 예시처럼 Music의 객체를 완성 시키기 위해서 4개의 set메서드를 호출하였고, 4개의 set메서드를 모두 호출 시키기 전까지는 일관성이 깨진 객체가 되기 때문이다.

자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며, 스레드 안정성을 얻기 위해서는 프로그래머가 추가 작업을 해줘야한다.

3. 빌더 패턴(Builder Pattern)

여기서 대안으로 나온것이 빌더패턴이다. 빌더패턴이란 필요한 객체를 직접 만든느 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻어내고, 빌더 객체가 제공하는 일종의 setter메서드로 원하는 데이터를 set해준다.

말로는 쉽게 이해가 될 수 없으니 아래 예시를 보겠다.

public class Music{
    private String title;           /* 제목 */
    private String artist;          /* 가수 */
    private String albumName;       /* 앨범명 */
    private String genre;           /* 장르 */
    private String releaseDate;     /* 발매일 */

    /* 매개변수를 담는 Builder inner class */
    public static class Builder {
        /* 필수 매개변수 */
        private String title;

        /* 선택 매개변수 - 기본값으로 초기화 */
        private String artist = "";
        private String albumName = "";
        private String genre = "";
        private String releaseDate = "";

        public Builder(String title) {
            this.title = title;
        }

        public Builder artist(String artist) { this.artist = artist;    return this; }
        public Builder albumName(String albumName) { this.albumName = albumName;    return this; }
        public Builder genre(String genre) { this.genre = genre;    return this; }
        public Builder releaseDate(String releaseDate) { this.releaseDate = releaseDate;    return this; }

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

    public Music(Builder builder){
        this.title = builder.title;
        this.artist = builder.artist;
        this.albumName = builder.albumName;
        this.genre = builder.genre;
        this.releaseDate = builder.releaseDate;
    }
}

이렇게 빌더패턴을 이용하면 Music객체는 불변으로 선언되며, 모든 매개변수의 기본값을 Builder라는 inner class에 모아 둔 뒤, set메서드들은 각자 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 해당 방식은 플루언트API(fluent API) 혹은 메서드 연쇄(method chaining)라 한다.

해당 패턴을 호출한다면 아래와 같이 쓸 수 있다.

Music music = new Music.Builder("그래서그래")
        .artist("epikhigh")
        .albumName("EPIK HIGH IS HERE")
        .genre("HIPHOP")
        .build();

빌더 패턴을 이용하면 코드를 쓰거나 읽기가 쉬워진다. 빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 선택적 매개변수를 흉내낸것이다.

객체를 생성할때 불변으로 선언되는것도 중요한 것이, 2번 패턴처럼 set메서드 처럼 어느곳에서나 정보를 변경할 수 있는것과 달리, 빌더패턴처럼 주어진 조건내에서 허용하거나, 변경이 쉽지 않으면, 정보의 정합성에 대해 장점으로 가져올 수 있다.

해당 Builder 패턴은 Lombok 라이브러리에도 어노테이션으로 구현되어 있어 쉽게 호출이 가능하다.

public class Music{
    private String title;           /* 제목 */
    private String artist;          /* 가수 */
    private String albumName;       /* 앨범명 */
    private String genre;           /* 장르 */
    private String releaseDate;     /* 발매일 */

    /* lombok */
    @Builder
    public Music(String title, String artist, String albumName, String genre, String releaseDate) {
        this.title = title;
        this.artist = artist;
        this.albumName = albumName;
        this.genre = genre;
        this.releaseDate = releaseDate;
    }
}

lombok 어노테이션을 사용하면 생성자에 @Builder 어노테이션을 선언해주는것으로 빌더패턴을 쉽게 사용 할 수 가 있다.