티스토리 뷰

728x90
반응형

이전 내용 앞에서 정적 팩토리 메서드를 사용하여 객체를 생성할 때 매개변수가 많을수록 적절히 대응하기 어렵다는 점을 느끼게 되었습니다. 책에서는 여러 매개변수를 받는 생성자를 만들어 놓았지만 저는 불편하여 빌더패턴을 사용했습니다.

 

점층적 생성자 패턴


💡 예제 코드

  • 아래 예제코드처럼 점층적 생성자 패턴을 사용할 수 있지만, 매개변수가 많아지면 생성자를 그만큼 많이 만들어줘야하고 클라이언트에서 객체를 생성하는 시점에 값의 의미가 무엇인지 헷갈릴 것이고, 매개변수가 몇 개인지도 주의해서 봐야할 것입니다.
public class User {

    private int id;
    private String name;
    private int age;
    private String phoneNumber;
    private String address;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public User(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    
    ... 추가적인 생성자
}

public class EffectiveJavaApplication {

    public static void main(String[] args) {
        User user = new User(1, "홍길동", 25);
    }
}

 

자바 빈즈 패턴


💡예제 코드

  • setter 메서드를 사용하여 점층적 생성자 패턴의 단점을 보안하였습니다.
  • 하지만 자바 빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러개 호출해야하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됩니다.
  • 점층적 생성자 패턴은 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데 자바 빈즈는 어디서든 setter를 사용할 수 있으므로 일관성이 무너지고 생각지 못한 버그가 생길 수 있습니다.
@Setter
public class User {

    private int id;
    private String name;
    private int age;
    private String phoneNumber;
    private String address;
}

public class EffectiveJavaApplication {

    public static void main(String[] args) {

        User user = new User();
        user.setId(1);
        user.setName("홍길동");
        user.setAge(25);
    }
}

 

빌더 패턴


💡예제 코드

  • 클라이언트가 직접 객체를 만드는 대신 필수 매개변수만으로 생성자를 호출하고 build() 메서드를 호출하여 객체를 얻습니다.
public class User {

    private int id;
    private String name;
    private int age;
    private String phoneNumber;
    private String address;

    private User (Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.age = builder.age;
        this.phoneNumber = builder.phoneNumber;
        this.address = builder.address;
    }

    public static class Builder {

        // 필수 매개 변수
        private final int id;
        private final String name;
        private final int age;

        // 선택 매개변수 - 기본값으로 초기화
        private String phoneNumber = null;
        private String address = null;

        public Builder(int id, String name, int age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        public Builder phoneNumber(String val) {
            this.phoneNumber = val;
            return this;
        }

        public Builder address(String val) {
            this.address = val;
            return this;
        }

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

public class EffectiveJavaApplication {

    public static void main(String[] args) {
        User user = new User.Builder(1, "홍길동", 25)
                            .phoneNumber("010-1234-5678")
                            .build();
    }
}

 

💡빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다

  • 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라 합니다. 이 기능을 사용하면 클라이언트가 형변환에 신경쓰지 않고도 빌더를 사용할 수 있습니다.
public abstract class Pizza {

    public enum Topping {
        HAM,
        MUSHROOM,
        ONION,
        PEPPER,
        SAUSAGE
    }

    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();
        }

        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}

public class NewYorkPizza extends Pizza {

    public enum Size {
        SMALL,
        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 NewYorkPizza build() {
            return new NewYorkPizza(this);
        }

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

    private NewYorkPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

public class MexicoPizza extends Pizza {

    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {

        private boolean sauceInside = false;

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        @Override
        public MexicoPizza build() {
            return new MexicoPizza(this);
        }

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

    private MexicoPizza(Builder builder) {
        super(builder);
        this.sauceInside = builder.sauceInside;
    }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) {

        NewYorkPizza newYorkPizza = new NewYorkPizza.Builder(NewYorkPizza.Size.SMALL)
                                                    .addTopping(Pizza.Topping.HAM)
                                                    .addTopping(Pizza.Topping.PEPPER)
                                                    .build();

        MexicoPizza mexicoPizza = new MexicoPizza.Builder()
                                                 .addTopping(Pizza.Topping.MUSHROOM)
                                                 .sauceInside()
                                                 .build();
    }
}

 

💡 결론

  • 생성자나 정적 팩토리가 처리해야할 매개변수가 많다면 빌더패턴을 사용하는게 좋습니다.
  • 매개변수 중 다수가 필수가 아니거나 같은 타입일 경우 더욱 그렇습니다.
  • 점층적 생성자보다 코드를 읽고 쓰는데 간결하며 자바빈즈보다 안전합니다.

 

728x90
반응형