이펙티브 자바 3판
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라
빌더
빌더는 인스턴스 생성 시에 제공해야할 선택적 매개변수가 많을 때 사용할 수 있는 방식입니다.
메소드를 연쇄적으로 실행하면서 파라마터 값을 받아오고, 최종적으로 build() 를 통하여 완성된 인스턴스를 반환합니다.
기존의 사용방식 ( 점층적 생성자 패턴, 자바빈즈 패턴 )
public class ComputerShop {
private String CPU;
private String Mainboard;
private String Ram;
private Long RamSize;
private String Cooler;
private String GraphicCard;
private String Case;
public ComputerShop(String CPU) {
this.CPU = CPU;
}
public ComputerShop(String CPU, String mainboard, String ram, Long ramSize) {
this.CPU = CPU;
Mainboard = mainboard;
Ram = ram;
RamSize = ramSize;
}
public ComputerShop(String CPU, String mainboard, String ram, Long ramSize, String cooler, String graphicCard, String aCase) {
this.CPU = CPU;
Mainboard = mainboard;
Ram = ram;
RamSize = ramSize;
Cooler = cooler;
GraphicCard = graphicCard;
Case = aCase;
}
}
기존의 public 생성자나 정적 팩토리 모두 입력받을 매개변수의 개수가 늘어나게되면, 필수 매개변수를 제외하고 매개변수의 개수만큼 생성자나 메소드의 개수 또한 늘어나게 됩니다.
이를 점층적 생성자 패턴이라고 부르는데, 아무래 이 방법은 가독성 면에서 좋지 않기 때문에 혼란을 야기할 수 있습니다.
가령 매개변수의 순서를 반대로 한다던가 하는 식의 실수가 발생하게되면 원하는대로 동작하지 않을 것 입니다.
public class ComputerShop {
private String CPU;
private String Mainboard;
private String Ram;
private Long RamSize;
private String Cooler;
private String GraphicCard;
private String Case;
public ComputerShop(){
}
public void setCPU(String CPU) {
this.CPU = CPU;
}
public void setMainboard(String mainboard) {
Mainboard = mainboard;
}
public void setRam(String ram) {
Ram = ram;
}
public void setRamSize(Long ramSize) {
RamSize = ramSize;
}
public void setCooler(String cooler) {
Cooler = cooler;
}
public void setGraphicCard(String graphicCard) {
GraphicCard = graphicCard;
}
public void setCase(String aCase) {
Case = aCase;
}
}
다른 방법으로는 일단 객체를 생성하고, 입맛에 맞게 선택 매개변수의 값을 setter를 통하여 지정해주는 자바 빈즈 방법이 있습니다.
이 방법의 경우 생성된 객체에 아직 매개변수가 할당되지 않았으므로, 여러줄에 걸쳐서 매개변수 값을 할당하게 됩니다.
값의 할당이 한군데 몰려있으면 모르겠지만, 코드의 흐름에 따라 매개변수의 값을 할당하게되면 특정 시점에서 버그가 발생 시 원인을 찾는데 많은 시간이 소모됩니다.
생성 시점에서 완성된 객체가 생성되는 것이 아니므로, 항상 일관적인 결과를 얻을 수 없다는 의미가 되겠네요.
이런 단점의 완화책으로 생성이 끝난 지점을 특정하고, 특정되기 이전에는 사용할 수 없도록 하는 방법도 있습니다.
빌더 ( Builder )
public class ComputerShop {
private final String CPU;
private final String Mainboard;
private final String Ram;
private final Long RamSize;
private final String Cooler;
private final String GraphicCard;
private final String Case;
public ComputerShop(Builder builder) {
CPU = builder.CPU;
Mainboard = builder.Mainboard;
Ram = builder.Ram;
RamSize = builder.RamSize;
Cooler = builder.Cooler;
GraphicCard = builder.GraphicCard;
Case = builder.Case;
}
public static class Builder{
private String CPU;
private String Mainboard;
private String Ram;
private Long RamSize;
private String Cooler;
private String GraphicCard;
private String Case;
public Builder CPU(String CPU) { this.CPU = CPU; return this; }
public Builder Mainboard(String mainboard) { Mainboard = mainboard; return this; }
public Builder Ram(String ram) { Ram = ram; return this; }
public Builder RamSize(Long ramSize) { RamSize = ramSize; return this; }
public Builder Cooler(String cooler) { Cooler = cooler; return this; }
public Builder GraphicCard(String graphicCard) { GraphicCard = graphicCard; return this; }
public Builder Case(String aCase) { Case = aCase; return this; }
public ComputerShop build(){
return new ComputerShop(this);
}
}
}
빌더는 앞선 두 방법과 달리, 가독성과 일관성을 모두 잡은 방법입니다.
Builder 클래스를 통해서 원하는 매개변수를 입력받고 build 메소드를 통하여 적합한 타입의 객체를 반환시켜주는 형태를 띄고있습니다.
ComputerShop computer = new ComputerShop.Builder()
.CPU("MyCPU")
.Case("MyCase")
.build();
이렇게 빌더가 적용된 클래스는 원하는 속성을 가독성있게 설정하고, 인스턴스를 얻는게 가능합니다.
사용 예시
package me.ddings;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
public abstract class Burger {
public enum Vegetable{ Tomato, Lettuce, Pickle, Onion}
public enum Source{Ketchup, Mayonnaise, Mustard}
final Set<Vegetable> VEGETABLE_SET;
final Set<Source> SOURCES;
Burger(Builder<?> builder){
VEGETABLE_SET = builder.vegetableSet.clone();
SOURCES = builder.sources.clone();
}
abstract static class Builder<T extends Builder<T>>{
EnumSet<Vegetable> vegetableSet = EnumSet.noneOf(Vegetable.class);
EnumSet<Source> sources = EnumSet.noneOf(Source.class);
public T addVegetable(Vegetable vegetable){
vegetableSet.add(Objects.requireNonNull(vegetable));
return self();
}
public T addSource(Source source){
sources.add(Objects.requireNonNull(source));
return self();
}
abstract Burger build();
protected abstract T self();
}
}
버거 추상 클래스
package me.ddings;
public class BigMac extends Burger{
public static class Builder extends Burger.Builder<Builder>{
@Override
BigMac build() {
return new BigMac(this);
}
@Override
protected Builder self() {
return this;
}
}
private BigMac(Builder builder){
super(builder);
}
}
빅맥 클래스
package me.ddings;
public class ShanghaiBurger extends Burger{
public static class Builder extends Burger.Builder<Builder>{
@Override
ShanghaiBurger build() {
return new ShanghaiBurger(this);
}
@Override
protected Builder self() {
return this;
}
}
ShanghaiBurger(Builder builder) {
super(builder);
}
}
상하이버거 클래스
BigMac bigMac = new BigMac.Builder()
.addSource(Burger.Source.Mustard)
.addVegetable(Burger.Vegetable.Lettuce)
.build();
객체 생성
빌더는 매개변수의 개수가 많을수록 그 효과가 커지는 기술입니다.
어쩔수 없이 점층적 생성자 패턴이나 자바빈즈 패턴에 비하여 코드가 더 길고, 빌더를 생성해야하는 만큼 생성비용이 들어갑니다.
하지만, 생성비용이 엄청 큰 것은 아니기때문에 성능에 민감하지 않는이상 빌더를 사용하는 것이 좋아보입니다.