본문 바로가기
Language/Java

이펙티브 자바 - Builder 패턴

by ocwokocw 2021. 9. 16.

- 이 글은 Effective Java 를 기반으로 작성되었습니다.

- Builder 패턴

정적 팩토리 메소드나 생성자는 선택적 인자가 많을 때 사용하기가 난감하다. 빌더 패턴은 이런 문제를 해결하기 위해 사용하는 패턴인데 예를 통해서 알아보도록 하자.


- 점층적 생성자 패턴

만약 성분표에 필수 항목으로 총 제공량, 1회 제공량이 있고, 선택적 항목에는 칼로리, 지방, 나트륨, 탄수화물이 있다고 가정해보자.

 

만약 필수적인 항목과 선택적인 항목으로도 생성자를 사용할 수 있게 하려면 생성자를 여러 개 정의 해야 한다. 점층적 생성자 패턴이라고 불리우는 방식을 통해 이 문제를 해결하려고 하면 코드는 아래와 같이 될 것이다.

 

public class NutritionFactsTelescoping {

	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 NutritionFactsTelescoping(int servingSize, int servings) {
		this(servingSize, servings, 0);
	}
	
	public NutritionFactsTelescoping(int servingSize, int servings, int calories) {
		this(servingSize, servings, calories, 0);
	}

	public NutritionFactsTelescoping(int servingSize, int servings, int calories, int fat) {
		this(servingSize, servings, calories, fat, 0);
	}

	public NutritionFactsTelescoping(int servingSize, int servings, int calories, int fat, int sodium) {
		this(servingSize, servings, calories, fat, sodium, 0);
	}

	public NutritionFactsTelescoping(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
		super();
		this.servingSize = servingSize;
		this.servings = servings;
		this.calories = calories;
		this.fat = fat;
		this.sodium = sodium;
		this.carbohydrate = carbohydrate;
	}
}

 

이렇게 생성자를 정의해놓고 만약 이 생성자들을 통해 객체를 생성한다면 아래와 같은 코드를 작성할 수 있다.

 

public class BuilderTest {

	NutritionFactsTelescoping nutrition1 = new NutritionFactsTelescoping(1, 2, 3);
	NutritionFactsTelescoping nutrition2 = new NutritionFactsTelescoping(1, 2, 3, 4);
}

 

선택적인 인자가 많을 경우 점층적 생성자 패턴을 사용하면 약간의 문제가 있다. 우선 생성자를 정의한 부분부터 살펴보면 모든 인자로 속성을 설정하는 마지막 생성자가 호출되기까지 객체를 생성하기 위해서 값으로 설정하지 않아도 되는 0 이라는 값을 계속 넘겨주어야 한다.

 

또한 BuilderTest 에서 해당 클래스를 사용할 때 1 번째 인자, 2 번째 인자, 3 번째 인자의 값이 어떤 값인지를 기억하고 사용해야 한다. 그리고 실수로 값을 뒤바꿔서 인자로 넘길때에도 알아차리기가 상당히 곤란하다.


- 자바 빈 패턴

클래스에서 어떤 속성을 설정할 때 setter 메소드를 이용하는 경우가 많다. 위에서 선택적인 항목을 설정하는 부분을 setter 메소드 설정을 통해 해결할 수가 있다.

 

public class NutritionFactsSetter {

	private int servingSize = -1;
	private int servings = -1;
	private int calories = 0;
	private int fat = 0;
	private int sodium = 0;
	private int carbohydrate = 0;
	
	public NutritionFactsSetter() {
		super();
	}

	public void setServingSize(int servingSize) {
		this.servingSize = servingSize;
	}

	public void setServings(int servings) {
		this.servings = servings;
	}

	public void setCalories(int calories) {
		this.calories = calories;
	}

	public void setFat(int fat) {
		this.fat = fat;
	}

	public void setSodium(int sodium) {
		this.sodium = sodium;
	}

	public void setCarbohydrate(int carbohydrate) {
		this.carbohydrate = carbohydrate;
	}
}

 

Setter 메소드가 정의된 클래스를 사용해보자.

 

public class BuilderTest {

	public static void main(String[] args) {
		
		NutritionFactsSetter nutrition1 = new NutritionFactsSetter();
		
		nutrition1.setServingSize(1);
		nutrition1.setServings(2);
		nutrition1.setCalories(3);
		nutrition1.setSodium(33);
	}
}

 

점층적 생성자 패턴에 비해 코드가 상당히 깔끔해졌다. 가독성도 훨씬 좋아져서 어떤 속성을 설정하려는지도 명료하다.

개선된 점이 있음에도 여전히 문제점이 존재하는데 우선 1 회의 함수 호출로 객체 생성을 끝낼 수 없으므로 객체의 일관성에 문제가 생긴다.

 

또한 setter 를 public 메소드로 정의해야하면 변경 불가능(immutable) 객체를 생성할 수 없기 때문에 만약 이 객체가 다중 쓰레드에서 속성값이 변경되어야 하는 문제가 발생하면 이를 제어하기 위해 처리해야하는 문제가 많아진다.


- 빌더 패턴

점층적 생성자 패턴과 자바빈 패턴에서 등장한 문제를 해결하기 빌더 패턴을 이용할 수 있다. 코드를 한번 살펴보자.

 

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 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 setCalories(int calories) {
			this.calories = calories;
			return this;
		}
		
		public Builder setFat(int fat) {
			this.fat = fat;
			return this;
		}

		public Builder setSodium(int sodium) {
			this.sodium = sodium;
			return this;
		}
		
		public Builder setCarbohydrate(int carbohydrate) {
			this.carbohydrate = carbohydrate;
			return this;
		}
		
		public NutritionFacts build() {
			return new NutritionFacts(this);
		}
	}
	
	private NutritionFacts(Builder builder) {
		this.servingSize = builder.servingSize;
		this.servings = builder.servings;
		this.calories = builder.calories;
		this.fat = builder.fat;
		this.sodium = builder.sodium;
		this.carbohydrate = builder.carbohydrate;
	}
}

 

객체 생성시 Builder 를 이용해서 생성해야 하므로 Builder 는 static 이어야 한다. servingSize 와 servings 는 필수적인 항목이므로 final 로 선언하여 생성자에서 2 항목에 대한 인자를 필수적으로 받도록 한다. 그리고 setter 부분에서는 자기자신인 Builder 를 반환해서 체이닝 메소드 방시으로 선택 항목들을 설정할 수 있도록 한다. 모든 항목이 설정되었으면 마지막 build 를 통해서 자기자신을 객체의 생성자 인자로 넘긴다.

 

NutritionFacts 의 생성자가 private 접근자인 이유는 외부에서 해당 생성자로 속성들을 설정하는 것을 막고 오직 Builder 에 의해서만 객체를 생성할 수 있도록 하기 위함이다.

 

빌더 패턴은 build() 를 명시적으로 호출 해야 객체의 생성 과정이 끝나게되므로 불변식을 적용할 수 있다. 만약 불변식을 위반하면 IllegalStateException 예외를 던져야 한다.

 

빌더 패턴도 단점이 없는 것은 아닌데, 객체를 생성하려면 빌더 객체부터 생성을 해야한다. 이런 빌더 객체가 얼마나 자원을 소비하고 오버헤드를 발생시킨다고 그런것까지 고려하느냐고 생각할 수 있지만 그런 점이 오버헤드가 되느냐 아니냐의 판단여부는 어플리케이션의 특성을 따라가므로 알 수 없는 부분이다.

 

빌더 패턴이 위와 같은 장점을 지녔다고 해서 인자도 별로 없는게 무조건 빌더 패턴을 사용해야 한다. 라는 생각을 하지 않는 것도 중요하다. 언제나 원툴에 의존한 사고방식은 상황에 따른 적절한 대처를 하는 능력을 현저히 떨어뜨린다.

'Language > Java' 카테고리의 다른 글

이펙티브 자바 - 불필요한 객체 생성  (0) 2021.09.18
이펙티브 자바 - 싱글턴 패턴  (0) 2021.09.18
이펙티브 자바 - 정적 팩토리 메소드  (0) 2021.09.05
[Java 8] 날짜 API - 2  (0) 2021.02.11
[Java 8] 날짜 API - 1  (0) 2021.02.11

댓글