https://codegarden-farmjun.tistory.com/81
[JAVA] 생성자 대신 팩토리 메서드를 고려하라
전통적으로 클라이언트가 클래스의 인스턴스를 얻는 방법은 public 생성자이다. 하지만, 클래스는 생성자와 별도로 정적 팩토리 메서드(static factory method)를 제공할 수 있다. 정적 팩토리 메서드는
codegarden-farmjun.tistory.com
이전 글에서 생성자와 정적 팩토리 메서드에 대하여 알아봤다.
이전 글에서 언급하지는 않았지만 생성자와 정적 팩토리 메서드 모두 공통된 단점이 있다. 바로 선택적 매개변수의 개수가 많으면 대응하기 어렵다는 것이다.
public class SubwaySandwich {
// 필수 요소
private String name;
private String bread;
// 선택 요소
private String cheese;
private String first_vegetable;
private String second_vegetable;
private String third_vegetable;
private String first_source;
private String second_source;
private String third_source;
}
다들 알고 있는 서브웨이 샌드위치를 표현하는 클래스입니다.
메뉴 이름과 빵은 필수적이며, 치즈, 야채, 소스는 선택적입니다. 실제 서브웨이는 야채와 소스를 더욱 다양하게 선택할 수 있지만, 간단한 예시를 들기 위해 세 개씩만 선택할 수 있도록 했습니다.
이 클래스의 생성자와 정적 팩토리 메서드는 어떤 모습일까요?
1. 점층적 생성자 패턴 (telescoping constructor pattern)
public class SubwaySandwich {
// 필수 요소
private String name;
private String bread;
// 선택 요소
private String cheese;
private String firstVegetable;
private String secondVegetable;
private String thirdVegetable;
private String firstSource;
private String secondSource;
private String thirdSource;
public SubwaySandwich(String name, String bread) {
this.name = name;
this.bread = bread;
this.cheese = "";
this.firstVegetable = "";
this.secondVegetable = "";
this.thirdVegetable = "";
this.firstSource = "";
this.secondSource = "";
this.thirdSource = "";
}
public SubwaySandwich(String name, String bread, String cheese) {
this.name = name;
this.bread = bread;
this.cheese = cheese;
this.firstVegetable = "";
this.secondVegetable = "";
this.thirdVegetable = "";
this.firstSource = "";
this.secondSource = "";
this.thirdSource = "";
}
public SubwaySandwich(String name, String bread, String cheese, String firstVegetable) {
this.name = name;
this.bread = bread;
this.cheese = cheese;
this.firstVegetable = firstVegetable;
this.secondVegetable = "";
this.thirdVegetable = "";
this.firstSource = "";
this.secondSource = "";
this.thirdSource = "";
}
...
public SubwaySandwich(String name, String bread, String cheese, String firstVegetable,
String secondVegetable, String thirdVegetable, String firstSource, String secondSource, String thirdSource) {
this.name = name;
this.bread = bread;
this.cheese = cheese;
this.firstVegetable = firstVegetable;
this.secondVegetable = secondVegetable;
this.thirdVegetable = thirdVegetable;
this.firstSource = firstSource;
this.secondSource = secondSource;
this.thirdSource = thirdSource;
}
먼저 점층적 생성자 패턴을 알아보자.
필수 매개변수만 받는 생성자, 필수 매개변수 + 선택 매개변수 1개를 받는 생성자, 필수 매개변수 + 선택 매개변수 2개를 받는 생성자
...
필수 매개변수 + 선택 매개변수 모두를 받는 생성자까지 늘려가는 것이다.
// 플랫 브래드 스파이시 바베큐 샌드위치
// 치즈 : 없음 / 야채 : 없음 / 소스 : 없음
SubwaySandwich spicyBBQ = new SubwaySandwich("스파이시 바베큐", "플랫");
// 허니오트 브래드 스파이시 바베큐 샌드위치
// 치즈 : 아메리카 / 야채 : 없음 / 소스 : 랜치
SubwaySandwich roastedChicken = new SubwaySandwich("로스티 치킨", "허니오트", "아메리카", "", "", "", "랜치");
// 위트 브래드 에그마요 샌드위치
// 치즈 : 모짜렐라 / 야채 : 토마토, 양상추, 피클 / 소스 : 랜치, 스위트 어니언, 칠리
SubwaySandwich eggMayo = new SubwaySandwich("에그마요", "위트", "모짜렐라", "토마토", "양상추", "피클", "랜치", "스위트 어니언", "칠리");
다음과 같이 원하는 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 적은 매개변수를 받는 생성자를 통해 생성하면 된다. 이런 생성자는 보통 사용자가 원치 않은 매개변수까지 값을 지정해줘야 한다.
위 코드의 스파이시 바베큐 샌드위치에서는 야채를 아무것도 선택하고 싶지 않지만, 소스를 선택해야 해서 firstVegetable, secondVegetable, thirdVegetable에 ""을 넘겼다. 이 예시에서는 매개변수가 최대 9개까지 밖에 없어서 괜찮아 보일 수 있지만, 매개변수가 증가하면 걷잡을 수 없게 된다.
코드를 읽을 때, 값들의 의미가 무엇인지 헷갈릴 수 있다. 예를 틀어 야채의 토마토인지, 토마토소스인지 충분히 헷갈릴 가능성이 있다.
또, 매개변수가 몇 개인지도 주의해야 한다.
가장 큰 문제점은 클라이언트의 실수로 매개변수의 순서를 바꿔서 건네줘도 매개변수의 타입만 일치한다면 컴파일러는 이를 발견할 수 없고, 런타임에 에러가 발생하게 된다. 이는 치명적이다.
2. 자바빈즈 패턴(JavaBeans pattern)
자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 통해 원하는 매개변수를 설정하는 방식이다.
public class SubwaySandwich {
// 필수 요소
private String name;
private String bread;
// 선택 요소
private String cheese = "";
private String firstVegetable = "";
private String secondVegetable = "";
private String thirdVegetable = "";
private String firstSource = "";
private String secondSource = "";
private String thirdSource = "";
public SubwaySandwich() {
}
public void setName(String name) {
this.name = name;
}
public void setBread(String bread) {
this.bread = bread;
}
public void setCheese(String cheese) {
this.cheese = cheese;
}
public void setFirstVegetable(String firstVegetable) {
this.firstVegetable = firstVegetable;
}
public void setSecondVegetable(String secondVegetable) {
this.secondVegetable = secondVegetable;
}
public void setThirdVegetable(String thirdVegetable) {
this.thirdVegetable = thirdVegetable;
}
public void setFirstSource(String firstSource) {
this.firstSource = firstSource;
}
public void setSecondSource(String secondSource) {
this.secondSource = secondSource;
}
public void setThirdSource(String thirdSource) {
this.thirdSource = thirdSource;
}
}
// 플랫 브래드 스파이시 바베큐 샌드위치
// 치즈 : 없음 / 야채 : 없음 / 소스 : 없음
SubwaySandwich spicyBBQ = new SubwaySandwich();
spicyBBQ.setName("스파이시 바베큐");
spicyBBQ.setBread("플랫");
// 허니오트 브래드 스파이시 바베큐 샌드위치
// 치즈 : 아메리카 / 야채 : 없음 / 소스 : 랜치
SubwaySandwich roastedChicken = new SubwaySandwich();
roastedChicken.setName("로스티 치킨");
roastedChicken.setBread("허니오트");
roastedChicken.setCheese("아메리카");
roastedChicken.setFirstSource("랜치");
// 위트 브래드 에그마요 샌드위치
// 치즈 : 모짜렐라 / 야채 : 토마토, 양상추, 피클 / 소스 : 랜치, 스위트 어니언, 칠리
SubwaySandwich eggMayo = new SubwaySandwich();
eggMayo.setName("에그마요");
eggMayo.setBread("위트");
eggMayo.setCheese("모짜렐라");
eggMayo.setFirstVegetable("토마토");
eggMayo.setSecondVegetable("양상추");
eggMayo.setThirdVegetable("피클");
eggMayo.setFirstSource("랜치");
eggMayo.setSecondSource("스위트 어니언");
eggMayo.setThirdSource("칠리");
자바빈즈 패턴에서는 점층적 생성자 패턴의 단점들이 발견되지 않는다. 코드가 더 길어지긴 했지만, 인스턴스를 만들기 쉽고, 가독성 또한 좋다. 하지만 자바빈즈 패턴은 치명적인 단점이 있다.
1. 객체 하나를 만들려면 여러 개의 메서드를 호출해야 한다.
2. 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓인다.
만약에 개발자가 필수 요소인 이름과 빵을 설정하는 과정에서 setName() 메서드만 호출하고, setBread() 메서드는 깜빡하고 호출하지 않았다고 생각해 보자. 이 샌드위치는 샌드위치의 필수 요소인 빵이 없는 샌드위치다. 즉, 일관성이 무너진 상태이다.
3. 클래스를 불변(immutable)으로 만들 수 없다.
Setter 메서드는 객체를 처음 생성할 때 필요한 메서드이다. 하지만, 객체 생성 이후에도 외부에서 setter 메소드를 호출하고 있다. 따라서 언제든 누군가 setter 메서드를 통해 객체를 변경할 수 있다.
클라이언트가 주문한 샌드위치가 만들어졌는데, 클라이언트가 먹으려는 순간 빵이 사라질 수도 있고, 사용자에게 알레르기 반응을 일으킬 수 있는 재료가 추가될 수도 있다. 즉, 불변함을 보장할 수 없다.
이처럼 일관성이 무너진 단점 때문에 자바빈즈 패턴은 클래스를 불변으로 만들 수 없고, 스레드 안정성을 보장받으려면 프로그래머가 추가적인 작업을 해줘야 한다.
3. 빌더 패턴(Builder pattern)
다행히도 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴이란 것이 존재한다.
빌더 패턴은 일반적으로 다음과 같다.
1. 클라이언트가 필요한 객체를 필수 매개변수만으로 생성자 or 정적 팩토리를 호출해 빌더 객체를 얻는다.
2. 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수를 설정한다.
3. 매개변수가 없는 build 메서드를 호출해 필요한 객체(일반적으로 불변)를 얻는다.
빌더는 보통 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다.
public class SubwaySandwich {
// 필수 요소
private String name;
private String bread;
// 선택 요소
private String cheese;
private String firstVegetable;
private String secondVegetable;
private String thirdVegetable;
private String firstSource;
private String secondSource;
private String thirdSource;
public static class Builder {
// 필수 요소
private String name;
private String bread;
// 선택 요소
private String cheese = "";
private String firstVegetable = "";
private String secondVegetable = "";
private String thirdVegetable = "";
private String firstSource = "";
private String secondSource = "";
private String thirdSource = "";
public Builder(String name, String bread) {
this.name = name;
this.bread = bread;
}
public Builder cheese(String cheese) {
this.cheese = cheese;
return this;
}
public Builder firstVegetable(String firstVegetable) {
this.firstVegetable = firstVegetable;
return this;
}
public Builder secondVegetable(String secondVegetable) {
this.secondVegetable = secondVegetable;
return this;
}
public Builder thirdVegetable(String thirdVegetable) {
this.thirdVegetable = thirdVegetable;
return this;
}
public Builder firstSource(String firstSource) {
this.firstSource = firstSource;
return this;
}
public Builder secondSource(String secondSource) {
this.secondSource = secondSource;
return this;
}
public Builder thirdSource(String thirdSource) {
this.thirdSource = thirdSource;
return this;
}
public SubwaySandwich build(){
return new SubwaySandwich(this);
}
}
private SubwaySandwich(Builder builder){
name = builder.name;
bread = builder.bread;
cheese = builder.cheese;
firstVegetable = builder.firstVegetable;
secondVegetable = builder.secondVegetable;
thirdVegetable = builder.thirdVegetable;
firstSource = builder.firstSource;
secondSource = builder.secondSource;
thirdSource = builder.thirdSource;
}
}
SubwaySandwich 클래스는 불변이고, 모든 매개변수의 기본값들을 한 곳에 모았다.
빌더의 메서드들은 this로 빌더 자기 자신을 반환하기 때문에 연쇄적인 호출이 가능하다. 이를 메서드 연쇄(method chaining) 혹은 메서드 호출이 흐르듯 연결된다는 의미로 플로언트 API(fluent API)라고 한다.
// 플랫 브래드 스파이시 바베큐 샌드위치
// 치즈 : 없음 / 야채 : 없음 / 소스 : 없음
SubwaySandwich spicyBBQ = new SubwaySandwich
.Builder("스파이시 바베큐", "플랫")
.build();
// 허니오트 브래드 스파이시 바베큐 샌드위치
// 치즈 : 아메리카 / 야채 : 없음 / 소스 : 랜치
SubwaySandwich roastedChicken = new SubwaySandwich
.Builder("로스티 치킨", "허니오트")
.cheese("아메리카")
.firstSource("랜치")
.build();
// 위트 브래드 에그마요 샌드위치
// 치즈 : 모짜렐라 / 야채 : 토마토, 양상추, 피클 / 소스 : 랜치, 스위트 어니언, 칠리
SubwaySandwich eggMayo = new SubwaySandwich.
Builder("에그마요", "위트")
.cheese("모짜렐라")
.firstVegetable("토마토")
.secondVegetable("양상추")
.thirdVegetable("피클")
.firstSource("랜치")
.secondSource("스위트 어니언")
.thirdSource("칠리")
.build();
어떠한가? 빌더 패턴을 사용하여 객체를 생성하니, 안정성과 가독성 두 마리 토끼를 모두 잡을 수 있다.
잘못된 매개변수는 빌더의 생성자와 메서드에서 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하면 발견할 수 있다. 잘못된 부분이 발견되면 어떤 매개변수가 잘못되었는지 알려주는 메시지를 포함시킨 IllegalArgumentException을 던지면 된다.
완벽해 보이는 빌더 패턴에도 단점은 존재한다. 객체를 생성하기 전에 빌더 코드부터 모두 작성해야 하는 것이다. 하지만, API는 시간이 지날수록 매개변수가 증가하는 경향이 있다. 그러니 생성자와 정적 팩터리 메서드보단 애초에 빌더로 시작하는 편이 좋을 때가 많다.
또, 빌더 코드를 미리 작성해야 하는 문제점도 Lombok의 @Builder 어노테이션을 통해 간단히 해결할 수 있다. 즉 빌더 패턴의 단점은 굉장히 사소하다고 할 수 있다.
@Builder
public class SubwaySandwich {
// 필수 요소
private String name;
private String bread;
// 선택 요소
private String cheese;
private String firstVegetable;
private String secondVegetable;
private String thirdVegetable;
private String firstSource;
private String secondSource;
private String thirdSource;
private static SubwaySandwichBuilder builder(String name, String bread) {
return builder(name, bread);
}
}
Lombok 의존성을 추가한 다음 클래스 위에 @Builder 어노테이션을 붙이자. 필수 매개변수는 builder 메서드를 재정의하여 반드시 설정될 수 있도록 하자.
// 플랫 브래드 스파이시 바베큐 샌드위치
// 치즈 : 없음 / 야채 : 없음 / 소스 : 없음
SubwaySandwich spicyBBQ = SubwaySandwich
.builder("스파이시 바베큐", "플랫")
.build();
// 허니오트 브래드 스파이시 바베큐 샌드위치
// 치즈 : 아메리카 / 야채 : 없음 / 소스 : 랜치
SubwaySandwich roastedChicken = SubwaySandwich
.builder("로스티 치킨", "허니오트")
.cheese("아메리카")
.firstSource("랜치")
.build();
// 위트 브래드 에그마요 샌드위치
// 치즈 : 모짜렐라 / 야채 : 토마토, 양상추, 피클 / 소스 : 랜치, 스위트 어니언, 칠리
SubwaySandwich eggMayo = SubwaySandwich
.builder("에그마요", "위트")
.cheese("모짜렐라")
.firstVegetable("토마토")
.secondVegetable("양상추")
.thirdVegetable("피클")
.firstSource("랜치")
.secondSource("스위트 어니언")
.thirdSource("칠리")
.build();
*이 포스팅은 제가 "이펙티브 자바 3/E"를 읽고 공부한 내용을 정리한 것입니다. 내용에 문제가 있으면 알려주시면 감사하겠습니다! *