Java/이펙티브자바

빌더

이펙티브 자바 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();

객체 생성

 

 

 

 

 

 

 

 

 

빌더는 매개변수의 개수가 많을수록 그 효과가 커지는 기술입니다.

 

어쩔수 없이 점층적 생성자 패턴이나 자바빈즈 패턴에 비하여 코드가 더 길고, 빌더를 생성해야하는 만큼 생성비용이 들어갑니다.

 

하지만, 생성비용이 엄청 큰 것은 아니기때문에 성능에 민감하지 않는이상 빌더를 사용하는 것이 좋아보입니다.