이것저것

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

Effective Java

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

nays111 2021. 4. 17. 21:21

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


 

생성자 (점층적 생성자 패턴) 를 사용하여 인스턴스를 만들때, 매개변수 개수가 많아지면, 클라이언트 코드를 작성하거나 읽기 어려워진다.

매개변수의 순서에 따라 주입되는 값이 달라지고, 매개변수가 추가될 때마다 생성자를 추가로 생성해주어야한다.

 

이러한 단점을 보완하기 위해, setter 메서드를 호출하여 매개변수의 값을 설정하는 JavaBeans pattern 이 있다. JavaBeans pattern은 가독성은 좋지만, 하나의 객체를 만들기 위해 여러 setter 메서드를 호출해야하고, 객체가 완전히 완성되기 전까지 일관성이 무너진 상태이다.

 

이 포스팅에서는 Builder pattern만 다뤄본다!


Builder Pattern

: 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자 (혹은 static factory method) 를  호출해 빌더 객체를 얻는다. 그런 다음, 빌더 객체가 제공하는 일종의 setter 메서드들로 원하는 선택 매개변수들을 설정한다. 마지막으로 매개변수가 없는 build 메서드를 호출해 우리에게 필요한 객체를 얻는다.

 

(빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다.)

 

 

public class Member {
    private final String id;    // required
    private final String name;  // required
    private final String email; // optional
    private final int height;   // optional
    private final int weight;   // optional


    public static class Builder {
        // 필수 파라미터
        private final String id;
        private final String name;

        // 옵셔널 파라미터
        private String email;
        private int height;
        private int weight;

        // 1. 빌더 객체를 사용하려면 가장 먼저 필수 파라미터를 입력하도록 한다.
        public Builder(String id, String name) {
            this.id = id;
            this.name = name;
        }

        // 2. 옵셔널 파라미터는 선택적으로 호출되도록 한다.
        public Builder email(String email) {
            this.email = email;
            return this;
        }

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

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

        // 3. 옵셔널 세팅 작업이 완료되면 완성 메서드를 호출한다.
        public Member build() {
            return new Member(this); 
        }
    }
  
    // 4. 객체는 반드시 해당 객체의 Builder 객체로만 생성할 수 있도록 한다.
    private Member(Builder builder) {
        id = builder.id;
        name = builder.name;
        email = builder.email;
        height = builder.height;
        weight = builder.weight;
    }
}

 

빌더 객체를 만드는 방법은 다음 4가지 과정을 거친다.

  1. 빌더 객체를 사용하려면 가장 먼저 필수 파라미터를 입력하도록 한다.
  2. 옵셔널 파라미터는 선택적으로 호출되도록 한다.
  3. 옵셔널 세팅 작업이 완료되면 완성 메서드를 호출한다.
  4. 객체는 반드시 해당 객체의 Builder 객체로만 생성할 수 있도록 한다.

 

그러면 이제 빌더를 통해 만든 클래스로 인스턴스를 생성해보자

Member member1 = new Builder("ma-id", "ma-name")
                .height(100)
                .build();

우선 굉장히 가독성이 높아진 것을 확인할 수 있다. 필수인 id와 name 과 optional 한 값중 height에 대해서만 값을 설정하였다.

 

빌더 팬턴은 명명적 선택적 매개변수를 흉내낸 것이다.

 


빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다.

Guest와 Authnticated Member 모두 Member 클래스의 하위 클래스이다.

 

public abstract class Member {
    // 모든 서브 타입 객체에 공통적으로 필요한 타입
    public enum Authority { READ, WRITE, DELETE }
    final Set<Authority> authorities;

    // 재귀적 타입 파라미터를 가진 제너릭 타입
    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Authority> authorities = EnumSet.noneOf(Authority.class);
        // 서브타입에서 권한을 정의할 수 있음
        public T addAuthorities(Authority types) {
            authorities.add(Objects.requireNonNull(types));
            return self();
        }

        abstract Member build();

        // 하위 클래스는 반드시 자기 자신을 반환하는 메서드를 오버라이드 해야 한다.
        protected abstract T self();
    }

    Member(Builder<?> builder) {
        authorities = builder.authorities.clone();
    }
}
public class Guest extends Member {
    private final String name;
    public static class Builder extends Member.Builder<Builder> {
        private final String name;

        // 1. Guest 객체가 가져야할 인스턴스 변수
        public Builder(String name) { this.name = Objects.requireNonNull(name); }

        // 2. 마지막으로 호출되는 빌드 완성 메서드
        // - 공변 반환 타이핑(covariant return typing)을 사용하면, 빌더를 사용하는 클라이언트는 캐스팅이 필요없다.
        @Override Guest build() { return new Guest(this); }

        // 3. 부모 타입에서 필요한 서브 타입의 참조
        @Override protected Builder self() { return this; }
    }
    private Guest(Builder builder) {
        super(builder);
        name = builder.name;
    }
}
public class AuthenticatedMember extends Member {
    private final String name;  // 필수 속성
    private final String email; // 필수 속성

    public static class Builder extends Member.Builder<Builder> {
        private String name;
        private String email;
        
        // 1. 필수 속성은 빌더 생성자에서 바로 받도록 한다.
        public Builder(String name, String email) {
            this.name = name;
            this.email = email;
        }
        // 2. 최종 빌더 완성 메서드
        @Override AuthenticatedMember build() { return new AuthenticatedMember(this); }
        
        // 3. 부모 타입에서 필요한 메서드
        @Override protected Builder self() { return null; }
    }
    
    public AuthenticatedMember(Builder builder) {
        super(builder);
        name = builder.name;
        email = builder.email;
    }
}

각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다.

Gest.Builder는 Guest를 반환, AuthorizedMember.Builder 는 AuthorizedMember를 반환한다는 뜻이다. 하위 클ㄹ래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌 , 그 하위 타입을 반환하는 기능을 공변 변환 타이핑 이라 한다.

 

public static void main(String[] args) {

  Guest guest = new Guest.Builder("myguests")
    .addAuthorities(Member.Authority.READ).build();

  AuthenticatedMember aMember = new AuthenticatedMember.Builder("member name", "abc@def.com")
    .addAuthorities(Member.Authority.READ)
    .addAuthorities(Member.Authority.WRITE).build();

}

Member에서 정의 Authority를 자유롭게 추가 가능해진다.

 


"생성자나 정적 팩토리가 처리해야할 매개변수가 많다면 Builder Pattern을 선택하는게 더 낫다. (Builder는 점층적 생성저보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, Java Beans보다 안전하다.)"

Comments