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

이 글은 Effective Java 책을 읽으면서 중요한 내용 및 알아야 하는 지식들을 정리한 글입니다. 또한, 백기선님의 Youtube 강의영상도 참고 했습니다. https://www.youtube.com/watch?v=OwkXMxCqWHM&list=PLfI752FpVCS8e5ACdi5dpwLdlVkn0QgJJ&index=2

아이템1 - 정적 팩터리 와 생성자에도 Optional 한 매개변수가 많아질 경우에 불편해진다.

매개변수가 다른 생성자 N개

인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라서 호출하면 된다.

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

하지만 이 방식은, 원히지 않는 매개변수까지 포함시켜서 억지로 넘겨줘야 한다. (0으로 설정한 것 처럼) 또한, 매개변수 개수가 많아지면 어떤 매개변수가 무엇을 의미하는지 개발자가 실수할 수도 있다. (IDE에서는 친절하게 모두 보여주지만)

자바빈즈 패턴 (JavaBeans pattern)

매개변수가 없는 생성자를 개체로 만든 후, setter 메서드를 호출해서 값을 세팅해준다.

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

Java 실무 개발자라면 많이 본 패턴일 것이다. 이 방법은 인스턴스 생성 시, 어떤 값을 setting하는지 직관적으로 볼수 있다. 하지만 심각한 단점이 있다 (몰랐던 부분이다)

  • 객체가 완전히 생성되기 전까지는 일관성이 무너진다.
    • 즉, 객체의 필수값에 setter를 해주지 않을 수도 있다.
  • 불변으로 만들 수 없다
    • 멀티 스레드 환경에서 각 스레드가 setter로 값을 언제든지 수정 가능하다.
      • 객체 freezing 기능을 사용하면 되지만, 해당 기능은 javascript에만 존재한다.
      • 또한 책에서도 권고하지 않는다.
      • 객체 안에 flag 값을 하나 두고, 해당 값이 true이면 setter 호출 시 error를 뱉도록 구성할 수 있는 것만 알고 있자!

빌더패턴 (Builder pattern)

롬복을 사용하는 개발자라면 Builder 가 어떤 기능을 하는지 대충 감이 올것이다.

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100).sodium(35).carbohydrate(27).build();

빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 게 보통이다.

public static class Builder {
    // 필수 매개변수
    private final int servingSize;
    private final int servings;

    // 선택 매개변수 - 기본값으로 초기화한다.
    private int calories      = 0;
    private int fat           = 0;
    private int sodium        = 0;
    private int carbohydrate  = 0;

    public Builder(int servingSize, int servings) {
        this.servingSize = servingSize;
        this.servings    = servings;
    }

    public Builder calories(int val)
    { calories = val;      return this; }
    public Builder fat(int val)
    { fat = val;           return this; }
    public Builder sodium(int val)
    { sodium = val;        return this; }
    public Builder carbohydrate(int val)
    { carbohydrate = val;  return this; }

    public NutritionFacts build() {
        return new NutritionFacts(this);
    }
}
  • NutritionFacts 클래스의 정적 멤버 클래스인 Builder를 생성하고, 그 안에 필수 매개변수와 선택매개변수를 선언한다.
  • 필수 매개변수를 받는 생성자를 선언한다.
  • 선택 매개변수의 이름으로 Builder 를 반환하는 메소드를 선언한다.
    • 이렇게 하면 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
    • fluent API 혹은 method chaining이라고 한다.

이렇게 만들어진 Builder에 유효성 검사를 추가할 수 있다. (가령 calories에는 1000이 넘지 않는다라던지..)

빌더 패턴을 계층적으로 설계된 클래스와 함께 써보자. Pizza 라는 추상 클래스를 만들고, 이를 상속하는 NyPizza, Calzone 클래스를 만들어보자.

public abstract class Pizza {
    // 추상 클래스
    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();

        // 추상 메서드
        // 하위 클래스는 이 메서드를 재정의(overriding)하여
        // "this"를 반환하도록 해야 한다.
        protected abstract T self();
    }
}
  • Builder<T extends Builder<T>> : Builder의 하위타입을 매개변수로 받는다. 이렇게 하면 매개변수로 받는 제터릭 타입 `T`에 대해서 `T`를 리턴할수 있으므로 형변환도 필요없고, 메서드 체이닝도 가능하다.
  • EnumSet<Topping> toppings : Pizza 클래스가 가지는 변수
  • addTopping : 제너릭 추상 메서드로서, topping을 추가하면서 제너릭 type T (self)를 반환해서, 연쇄 메서드를 가능하게 한다.
  • Objects.requireNonNull : Null 인지 Not Null인지 확인한 뒤에 세팅해준다.

Pizza 추상 클래스를 구현하는 구현클래스 Calzone

public class Calzone extends Pizza{

	private final boolean sauceInside;

	// 추상 빌더를 구현하는 Calzone의 빌더
	public static class Builder extends Pizza.Builder<Builder> {

		private boolean sauceInside = false; // 기본값

		// Calzone만 가지고 있는 메서드
		public Builder sauceInside() {
			sauceInside = true;
			return this;
		}

		// 추상 메서드를 구현하는 메서드, 자기 자신을 리턴하게 함으로써 형변환을 따로 안하게 한다.
		@Override
		Calzone build() {
			return new Calzone(this);
		}

		// 자기 자신을 반환해서, 연쇄 메서드 기능을 하게 한다.
		@Override
		protected Builder self() {
			return this;
		}

	}

	// Builder를 넘겨받아서 인스턴스를 생성한다.
	private Calzone(Builder builder) {
		super(builder);
		sauceInside = builder.sauceInside;		
	}

}
  • sauceInside : Calzone 만 가지고 있는 또 다른 변수
  • Builder extends Pizza.Builder<Builder> : Pizza의 Builder를 구현한다.
    • Calzone만 가지고 있는 메서드 sauceInside를 추가했다.
    • build 메서드는 Calzone를 반환함으로써, 따로 형변환을 하지 않아도 된다.
  • private Calzone(Builder builder) : Builder를 넘겨받아서 인스턴스를 생성한다.

이렇게 생성된 Builder는 아래와 같이 사용할 수 있다.

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
        .addTopping(HAM).sauceInside().build();

기본적으로 빌더패턴은 코드가 조금 많아지므로 장황해보일 수 있지만 매개변수가 많아질 경우 보다 간결하고 직관적인 코드를 작성할 수 있다.

롬복(lombok)

@Builder Annotation을 추가하면 위의 Builder를 자동으로 생성해준다.

추상 클래스

추상 클래스 abstract class를 만들고, 그 안에 또 추상 멤버 클래스 abstract static class를 만들어서, 이 추상 클래스를 구현하는 구현체가 추상 멤버클래스도 구현하게끔 만들 수 있다.

  • 꼭 구현해야 하는 클래스를 강제할 수 있다.
  • 부모 클래스의 메서드를 상속받아서 쓸 수 있다.

Effective-Java 소스코드

Effective-Java 소스코드 에서 풀 소스를 참고할 수 있다.

댓글남기기