스터디/이펙티브 자바

[Effective Java] Item 2. 생성자에 매개변수가 많다면 빌더

📝 작성 : 2022.05.11  ⏱ 수정 : 
728x90

생성자에 매개변수가 많다면?

생성자(또는 정적 팩토리 메서드)는 선택적 매개변수가 많을 때 문제가 있습니다.

public class User {
    private final String name;     //필수
    private final String birth;    //필수
    private final Integer height;  //선택
    private final Integer weight;  //선택

    public User(String name, String birth) {
        this(name, birth, null, null);
    }

    public User(String name, String birth, Integer height) {
        this(name, birth, height, null);
    }

    /* 컴파일에러 발생 (User(String, String, Integer) is already defined)
    public User(String name, String birth, Integer weight) {
        this(name, birth, null, weight);
    } 
    */

    public User(String name, String birth, Integer height, Integer weight) {
        this.name = name;
        this.birth = birth;
        this.height = height;
        this.weight = weight;
    }
}

위와 같은 형식을 점층적 생성자 패턴이라고 하는데 매개변수가 많아질 수록 코드를 작성하기 어렵습니다.
또한 코드를 읽을 떄 각 값의 의미가 무엇인지, 매개변수가 몇개짜리 생성자인지도 주의해서 읽어야 합니다.

이에 대한 대안으로 자바빈즈 패턴을 활용할 수 있습니다. 흔히 말하는 setter를 통해서 값을 넣는 방식입니다.

User user1 = new User();
user1.setName("홍길동");
user1.setBIrth("19920817");

User user2 = new User();
user2.setName("홍길동")
user2.setHeight(182);

이 방법도 단점이 있습니다. 자바빈즈 패턴의 가장 큰 단점은 불변 객체를 만들 수 없다는 것 입니다.

빌더 패턴

앞서 알아본 점층적 생성자 패턴자바빈즈 패턴의 대안으로 빌더 패턴이 있습니다.
필수 매개변수만으로 생성자(또는 정적 팩토리 메서드)를 통해 빌더 객체를 얻습니다. 이 빌더 객체가 제공하는 메서드를 통해 원하는 매개변수를 설정, build메서드를 통해 원하는 객체를 얻는 방법입니다.

public class User {
    private final String name;    //필수
    private final String birth;   //필수
    private final Integer height; //선택
    private final Integer weight; //선택

    private User(Builder builder) {
        name = builder.name;
        birth = builder.birth;
        height = builder.height;
        weight = builder.weight;
    }

    public static class Builder {
        private final String name;  //필수
        private final String birth; //필수

        private Integer height; //선택
        private Integer weight; //선택


        public Builder(String name, String birth) {
            this.name = name;
            this.birth = birth;
        }

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

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

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

User user = new User.Builder("홍길동", "19920817")
                .height(160)
                .build();
//빌더의 setter 메서드는 자신을 반환하기 때문에 연쇄적으로 호출할 수 있습니다.(플루언트 API 또는 메서드 연쇄라고 부릅니다.)

빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수(named optional parameters)를 흉내낸 것입니다.

자바빈즈 패턴과 빌더 패턴

//자바빈즈 패턴
User userByBeans = new User();
user.setName("홍길동");
user.setBirth("19920817");

//빌더 패턴
User userByBuilder = new User.Builder("홍길동", "19920817")
                .build();

userByBeans 객체는 이 후에 어느 곳에서나 setter메서드를 호출하여 키, 몸무게를 넣을 수 있습니다.
하지만 userByBuilder 객체는 한번 생성하면 땡입니다. 더이상 변할수 없습니다.

 

계층적으로 설계된 클래스와 빌더 패턴

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, OLIVE}
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        public abstract Pizza build();
        protected abstract T self(); 
    }

    Pizza(Builder<?> bUilder) {
        this.toppings = bUilder.toppings.clone();
    }
}
public class BulgogiPizza extends Pizza{
    public enum Size {MEDIUM, LARGE}

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public BulgogiPizza build() { //Pizza가 아닌 BulgogiPizza를 반환
            return new BulgogiPizza(this);
        }

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

    private BulgogiPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}
BulgogiPizza pizza = new BulgogiPizza.Builder(Size.LARGE)
                .addTopping(Topping.OLIVE)
                .addTopping(Topping.HAM)
                .build();

하위 클래스의 빌더(BulgogiPizza.Builder)의 build 메서드는 해당하는 구체 하위 클래스를 반환합니다.(BulgogiPizza)
하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환타입(Pizza)이 아닌, 그 하위 타입(BulgogiPizza)를 반환하는 기능을 공변 반환 타이핑(convariant return typing)이라고 합니다. 이 기능을 이용하면 형변환에 신경쓰지 않고 빌더를 다룰 수 있습니다.

 

위의 예제에서 2가지의 토핑을 추가했는데(.addTopping(Topping.OLIVE).addTopping(Topping.HAM)) 이는 생성자에서는 불가능한 빌더패턴의 이점입니다.

빌더패턴의 단점

객체를 만들기 전 빌더를 생성해야 해서 빌더 생성비용이 듭니다. 따라서 성능이 민감한 상황에서는 문제가 될 수 있습니다.
이 책에서는 매개 변수가 4개 이상은 되어야 값어치를 한다고 합니다.

 

번외) lombok의 @Builder 사용하면 어떻게 구현될까?

public class User {
    private final String name;
    private final String birth;
    private Integer height;
    private Integer weight;

    User(final String name, final String birth, final Integer height, final Integer weight) {
        this.name = name;
        this.birth = birth;
        this.height = height;
        this.weight = weight;
    }

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

    public static class UserBuilder {
        private String name;
        private String birth;
        private Integer height;
        private Integer weight;

        UserBuilder() {
        }

        public UserBuilder name(final String name) {
            this.name = name;
            return this;
        }

        public UserBuilder birth(final String birth) {
            this.birth = birth;
            return this;
        }

        public UserBuilder height(final Integer height) {
            this.height = height;
            return this;
        }

        public UserBuilder weight(final Integer weight) {
            this.weight = weight;
            return this;
        }

        public User build() {
            return new User(this.name, this.birth, this.height, this.weight);
        }

        public String toString() {
            return "User.UserBuilder(name=" + this.name + ", birth=" + this.birth + ", height=" + this.height + ", weight=" + this.weight + ")";
        }
    }
}
반응형