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

gang_shik·2022년 2월 16일
0

Effective Java 2장

목록 보기
2/9

아이템 1에서 정적 팩터리 메서드와 그리고 그냥 생성자에 대해서 다루어 보았음

그리고 정적 팩터리 메서드를 사용하는 것이 생성자를 쓰는 것보다 낫다고 하였는데 여기서 둘 다 똑같은 제약이 존재함, 선택적 매개변수가 많을 때 적절히 대응하기가 어려움

즉, 선택 항목의 대다수 값이 0이면 굳이 그렇게 쓸 이유가 없는 것임

점층적 생성자 패턴

  • 필수 매개변수만 받는 생성자, 필수 매개변수선택 매개변수 1개를 받는 생성자, 선택 매개변수를 2개까지 받는 생성자,... 형태로 선택 매개변수를 전부 받는 생성자까지 늘려가는 방식임

  • 예를 들면 아래와 같음

public class NutritionFacts {
    private final int servingSize; // (ml, 1회 제공량) 필수
    private final int servings; // (회, 총 n회 제공량) 필수
    private final int calories; // (1회 제공량당) 선택
    private final int fat; // (g/1회 제공량) 선택
    private final int sodium; // (mg/1회 제공량) 선택
    private final int carbohydrate; //(g/1회 제공량) 선택
    
    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}
  • 이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 됨 NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

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

  • 예시코드만 봤을 때 매개변수가 6개 뿐이라서, 쓸 만 하다고 생각될 수 있지만 수가 더 늘어나면 걷잡을 수 없게됨

  • 위와 같이 점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려움

  • 어떤 측면에서 어렵냐면 코드를 읽을 때 각 값의 의미무엇인지 헷갈리고 매개변수가 몇 개 인지도 주의해서 세어 보아야하고 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어질 수 있음

  • 그리고 클라이언트 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러알아채지 못하고 엉뚱하게 동작을 함

자바 빈즈 패턴

  • 매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드들을 호출해 원하는 매개변수 값을 설정하는 방식

  • 인스턴스를 만들기 쉽고 그 결과 더 읽기 쉬운 코드가 됨

  • 예를 들면 아래와 같음

public class NutritionFacts {
    // 매개변수들은(기본값이 있다면) 기본값으로 초기화된다.
    private final int servingSize = -1; // 필수; 기본값 없음  
    private final int servings = -1;  // 필수; 기본값 없음
    private final int calories = 0; 
    private final int fat = 0;
    private final int sodium = 0; 
    private final int carbohydrate = 0; 

    public NutritionFacts() { }
    // 세터 메서드들
    public void setServingSize(int val) {servingSize = val;}
    public void setServings(int val) {servings = val;}
    public void setCalories(int val) { calories = val;} 
    public void setFat(int val) { fat = val;}
    public void setSodium(int val) {sodium = val;}
    public void setCarbohydrate(int val) { carbohydrate =val;}
    
}
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
  • 하지만 이 자바 빈즈 패턴에도 큰 단점이 있음, 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고 객체가 완전히 생성되기 전까지는 일관성무너진 상태에 놓이게 됨

  • 일관성이 무너지기 때문에 자바 빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야만 함

  • 이러한 단점을 보완하고자 생성이 끝난 객체를 수동으로 얼리고 얼리기 전에는 사용할 수 없도록 하기도 함, 이 방법을 쓴다고 해도 freeze 메서드를 확실히 호출 했는지 보증이 없어서 런타임 오류에 취약함

freeze 메서드 사용을 통한 불변성 유지? 보증을 못 함?

freeze 메서드 활용

이 자바 빈즈 패턴에서 freeze 메서드를 사용하는 것을 간단히 알아보면 아래와 같이 setter메서드에서 freeze를 주는 것임

public class A {
    private String a;
    private int b;

    private boolean freeze = false;

    public A() {}

    public void setA(String a) {
        if (isFreeze()) {
            throw new AssertionError("완성된 객체는 불변성을 유지해야 합니다.");
        }
        this.a = a;
    }

    public String getA() {
        return a;
    }

    public void setB(int b) {
        if (isFreeze()) {
            throw new AssertionError("완성된 객체는 불변성을 유지해야 합니다.");
        }
        this.b = b;
    }

    public int getB() {
        return b;
    }

    public boolean isFreeze() {
        return freeze;
    }

    public void freeze() {
        this.freeze = true;
    }
}
A a = new A();
a.setA("A");
a.setB(2);
a.freeze();

하지만 위와 같이 처리를 했어도 멀티 스레딩 환경의 경우 이 메서드를 확실히 호출했다고 컴파일러보증할 방법이 없음

빌더 패턴

  • 점층적 생성자 패턴안전성자바 빈즈 패턴가독성을 겸비한 빌더 패턴이 있음

  • 클라이언트는 필요한 객체를 직접 만드는 대신 필수 매개변수만으로 생성자(혹은 정적 팩터리)호출빌더 객체를 얻음

  • 그런 다음 빌더 객체가 제공하는 일종의 setter 메서드들로 원하는 선택 매개변수설정

  • 마지막으로 매개변수가 없는 build 메서드호출해 우리에게 필요한 객체를 얻음

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

  • 그 예를 보면 아래와 같음

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;


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

        // 선택 매개변수 - 기본값으로 초기화함
        private final int calories = 0;
        private final int fat = 0;
        private final int sodium = 0;
        private final 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);
        }
    }
    
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
    
}
  • NutritionFacts 클래스는 불변이고 모든 매개변수기본값들을 한 곳에 모아둠

  • 빌더의 setter 메서드들은 빌더 자신반환하기 때문에 연쇄적으로 호출 할 수 있음, 이런 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API 혹은 메서드 연쇄라고 함


여기서 책에서 유효성 검사를 생략했다는데 이를 추가하면 어떻게 될까?

빌더 패턴에서의 유효성 검사

빌더 패턴은 개발하는데 있어서 자바 기반의 프레임워크에 개발을 할 때 종종 사용됨이 내포되어 있음
유효성 검사를 추가한다면 빌더의 생성자메서드에서 검사하고 build()호출하는 생성자에서 불변식 검사추가할 수 있음

TargetObject build() {
    TargetObject res = new TargetObject();
    res.setProperty1();
    res.setProperty2();
    validate(res); // This call may throw an exception
    return res;
}

void validate(TargetObject obj) {
    if (...) {
        throw new IllegalStateException();
    }
}
TargetObject build() {
    TargetObject res = new TargetObject();
    res.setProperty1();
    res.setProperty2();
    if (...) {
        throw new IllegalStateException();
    }
    return res;
}

위와 같이 빌더 패턴 사용시 유효성 검사를 추가적으로 진행해 줄 수 있음

  • 클라이언트에서는 아래와 같이 사용 가능함
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
				.calories(100).sodium(35).carbohydrate(27).build();
  • 이 클라이언트 코드는 쓰기 쉽고, 읽기 쉬움, 빌더 패턴은 명명된 선택적 매개변수를 흉내 낸 것

  • 여기서 잘못된 매개변수를 최대한 일찍 발견하려면 위와 같이 빌더의 생성자메서드에서 입력 매개변수검사하고 이런식으로 불변식 검사를 할 수 있음

  • 여기서 검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IllegalArgumentException을 던질 수 있음(이 부분에 대해서는 아이템 50, 75에서 상세히 다룰 것임)

불변과 불변식?

불변

불변은 어떠한 변경도 허용하지 않는다는 뜻으로, 주로 변경을 허용하는 가변 객체와 구분하는 용도로 쓰임(대표적으로 String객체는 한 번 만들어지면 절대 값을 바꿀 수 없는 불변 객체임)

불변식

불변식은 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 말함, 변경을 허용할 수 있으나 주어진 조건 내에서만 허용한다는 뜻임


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

  • 각 계층의 클래스에 관련 빌더멤버로 정의함, 추상 클래스추상 빌더구체 클래스구체 빌더를 갖게함

  • 예를 들어 피자의 다양한 종류를 표현하는 계층구조의 루트에 놓인 추상 클래스를 아래와 같이 쓸 수 있음

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.requiredNonNull(topping));
						return self();
				}
				
				abstract Pizza build();
			
				// 하위 클래스는 이 메서드를 재정의(overriding)하여 
				// "this"를 반환하도록 해야 한다.
				protected abstract T self();
		}

		Pizza(Builder<?> builder) {
				toppings = builder.toppings.clone();
		}
}
  • Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입

  • 여기에 추상 메서드self를 더해 하위 클래스에서는 형변환하지 않고도 메서드 연쇄를 지원할 수 있음, self타입이 없는 자바를 위한 이 우회 방법시뮬레이트한 셀프 타입 관용구라고 함

재귀적 타입 한정과 시뮬레이트한 셀프 타입 관용구?

재귀적 타입 한정

재귀적 타입 한정은 자기 자신이 들어간 표현식을 사용하여 타입 매개변수허용 범위한정하는 케이스임

이를 이해하기 쉽게 Comparable비교를 해본다면 static <T extends Comparable<T>>가 있는데 이는 자신과 비교될 수 있는 모든 타입 T 즉, 모든 타입 T자신과 비교할 수 있다는 것임

여기서 Comparable의 예시를 생각하면 Wrapper타입의 경우, 이를 비교 연산을 한다고 할 때 비교연산자를 사용할 수 없기 때문에 이를 재귀적 타입 한정을 활용해서 쓸 수 있음

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;

위의 비교 연산의 경우 그러면 Comparable이 연산하고자 하는 Wrapper Class로 한정이 되는 것임

  • 이를 본문의 NyPizza와 Calzone의 경우 Builder 상속재귀적 타입 한정으로 상속 받아서 사용되는 것임, 그러면 BuilderT가 자신을 포함하는 수식 Builder에 한정이 됨
public class NyPizza 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.requiredNonNull(size);
				}

				@Override public NyPizza build() {
						return new NyPizza(this);
				}
		
				@Override protected Builder self() { return this; }
		}
		
		private NyPizza(Builder builder) {
				super(builder);
				size = builder.size;
		}
}
public Class Calzone 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 Calzone build() {
						return new Calzone(this);
				}
			
				@Override protected Builder self() { return this; }
		}

		private Calzone(Builder builder) {
				super(builder);
				sauceInside = builder.sauceInside;
		}
}
제네릭

여기서 하나 더 짚고 넘어간다면 제네릭임, 제네릭의 정의는 데이터 형식의존하지 않고 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법임

이러면 제네릭클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것임

그러면 위에서 Comparable이나 Builder나 지정은 외부에서 해주는 것이어서 Comparable에서 Wrapper Class에서는 비교 연산이 안되기 때문에 비교 연산을 하고자 하는 Wrapper Class가 정해지는 것이고 Builder의 경우도 Builder를 구체적으로 구현하는 방향에 있어서 책에서 NyPizza, Calzone조건이 다르기 때문에 제네릭을 통해서 각각 맞춰서 Builder가 될 수 있도록 해줄 수 있었던 것임

시뮬레이트한 셀프 타입

여기서 시뮬레이트한 셀프 타입재귀적 타입 한정의 경우와 같이 쓰는 것인데 하위 클래스에서 형변화하지 않고 메서드 연쇄지원할 수 있는 것임

이 말은 위에서 PizzaBuilder의 추상 클래스를 가지고 self오버라이딩을 하는데 있어서 자기 자신 thisreturn함으로 형변환을 하지 않고 메서드 연쇄를 지원한다는 것임, 이를 코드로 자세히 본다면

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

   // 추상 클래스는 추상 Builder를 가진다. 서브 클래스에서 이를 구체 Builder로 구현한다.
   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();
   }

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

public class NyPizza 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 NyPizza build() {
         return new NyPizza(this);
      }
	  // self를 오버라이딩해서 NyPizza 자신을 return함, 형변환 하지 않아도 됨 
      @Override protected Builder self() { return this; }
   }

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

public class Calzone 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 Calzone build() {
         return new Calzone(this);
      }
	  // self를 오버라이딩해서 Calzone 자신을 return함, 형변환 하지 않아도 됨 
      @Override protected Builder self() { return this; }
   }

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

형변환을 하지 않는다는 것은 아래처럼 형변환을 생각하지 않고 빌더를 사용한다는 것을 의미함(열거 타입 상수는 임포트 했다는 가정)

public class Main {
    public static void main(String[] args) {
        NYPizza pizza = new NYPizza.Builder(SMALL)
                .addTopping(SAUSAGE)
                .addTopping(ONION)
                .build();

        Calzone calzone = new Calzone.Builder()
                .addTopping(HAM)
                .sauceInside()
                .build();
    }
}

공변 변환 타입

여기서 위와 같이 Pizza에서 self 구현시 BuilderCalzone, NyPizzareturn this;를 하기 때문에 반환형자기 자신으로 오버라이딩하는 경우인 것

하위 타입반환을 해도 문제가 없는 것이고 더 좁은 타입 즉, 하위 타입으로 교체될 수 있는 경우를 말하는 것임(Calzone, NyPizza라는 더 하위 타입으로 반환을 해서 교체가 된 것인데 문제가 없으므로)

일반적인 정의로 말하면 메서드오버라이딩될 때 더 좁은 타입으로 교체할 수 있다는 것임


  • 빌더를 이용하면 가변인수 매개변수를 여러 개 사용할 수 있음, 각각을 적절한 메서드로 나눠 선언하면 됨

  • 아니면 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수 있음

  • 빌더 패턴은 상당히 유연함, 빌더 하나로 여러 객체순회하면서 만들 수 있고 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있음, 객체마다 부여되는 일련번호와 같은 특정 필드빌더가 알아서 채우는 것

  • 단, 성능에 민감한 경우 문제가 될 수 있음, 매개변수가 4개 이상은 되야 값어치를 함

  • 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음, 애초에 빌더로 시작하는게 나을 수 있음

  • 생성자정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 나음

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글