본문 바로가기

도서/이펙티브 자바

[이펙티브 자바] 2. 생성자에 매개변수가 많다면 빌더를 고려하라

생성자, 정적 팩터리 메서드가 처리해야할 매개변수가 많다면 빌더 패턴을 선택하자

 

선택 매개변수가 많을 때 생성자를 하나씩 다 만들어야할까? (점층적 생성자)

혹은 하나만 만들고 세터로 값을 설정해야할까? (빈즈)

이러한 문제를 해결할 수 있는 방식인 빌더 패턴에 대해 알아보자

 

점층적 생성자 패턴

필수 생성자만 입력받는 생성자를 기본적으로 만들고, 선택 매개변수를 n개 받는 생성자를 추가로 만드는 방식

이 경우에는 필요한 생성자를 모두 만들어야하기 때문에 매개변수가 많아지면 가독성이 떨어진다.

 

class Computer {
    private String processor;
    private int ram;
    private int storage;

    // 첫 번째 생성자 (기본값만)
    public Computer(String processor) {
        this.processor = processor;
    }

    // 두 번째 생성자 (RAM 추가)
    public Computer(String processor, int ram) {
        this(processor);  // 첫 번째 생성자 호출
        this.ram = ram;
    }

    // 세 번째 생성자 (RAM, Storage 추가)
    public Computer(String processor, int ram, int storage) {
        this(processor, ram);  // 두 번째 생성자 호출
        this.storage = storage;
    }
}

 

자바 빈즈 패턴

매개변수가 없는 생성자로 객체를 만들고, setter로 원하는 매개변수 값을 설정하는 방식

점층적 생성자 패턴보다 인스턴스를 만들기 쉽고 가독성도 더 나아졌다.

 

하지만 객체 하나를 만들기 위해서 여러 setter 메서드를 호출해야하고,

완전히 생성되기 전까지는 일관성을 충족하지 못한다.

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

	// 기본 생성자
    public Person() {
    }

    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) {
        // 자바 빈즈 객체 생성
        Person person = new Person();
        
        // Setter를 통해 속성 설정
        person.setName("java");
        person.setAge(30);
    }
}

 

빌더 패턴

필수 매개 변수만 사용하여 빌더 객체를 얻고, 빌더 객체가 제공하는 setter로 선택 매개변수를 설정한 후

build() 메서드를 호출해 불변 객체를 얻는 방식

 

1 . 필수 매개 변수만 포함하는 빌더 객체를 생성

new Computer.Builder("Intel i7") // 필수

 

2. 빌더 클래스 내에서 생성한 선택 매개변수 setter들 호출

.ram(16)               // 선택
.storage(512)          // 선택

 

3. build() 메서드 호출하며 불변 객체 생성

public class Computer {
    private String processor;
    private int ram;
    private int storage;
    
    // 기본 생성자
    private Computer(Builder builder) {
        this.processor = builder.processor;
        this.ram = builder.ram;
        this.storage = builder.storage;
    }

    // 빌더 클래스 
    public static class Builder {
        private String processor; // 필수 
        private int ram; // 선택 
        private int storage; // 선택
         
        // 필수
		public Builder(String processor) {
            this.processor = processor; 
        }
        
        // 선택 
        public Builder ram(int ram) {
            this.ram = ram;
            return this;
        }
		
        // 선택 
        public Builder storage(int storage) {
            this.storage = storage;
            return this;
        }

        // 객체 반환 메서드
        public Computer build() {
            return new Computer(this);
        }
    }

    // 메인 메서드
    public static void main(String[] args) {
        // 빌더 패턴 사용
        Computer computer = new Computer.Builder("Intel i7") // 필수
                .ram(16)               // 선택
                .storage(512)          // 선택
                .build(); 
    }
}

 

이러한 구조를 통해 세터 메서드를 통해 매개변수 값을 설정할 수 있고, 불변 객체를 최종적으로 만들 수 있다. 

 

빌더 패턴 장점 -  계층적으로 설계된 클래스와 함께 사용하기 좋다.

추상 클래스와 구체 클래스가 있을 때, 각 클래스 위치에 빌더를 생성한다. 

각 구체 클래스에서 빌더를 만드는 방식을 사용하면, 필수 매개변수가 달라도 유연하게 사용할 수 있다.

이러한 구조를 계층적 빌더라고 한다. 

 

Pizza 추상 클래스

  • 추상 클래스의 Builder은 제네릭 타입으로 생성한다.
  • 추상 메서드 self를 추가하여, 구체 클래스에서 메서드 체이닝을 사용할 수 있게 하였다. 
public abstract class Pizza {
    public enum Topping {HAM, ONINON, PEPPER}
    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.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();
        protected abstract T self();
    }
    
    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();
    }
}

 

NyPizza 구체 클래스

  • size 매개 변수를 필수로 하는 빌더를 생성한다. 
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);
        }
        @Override protected Builder self() { return this; }
    }
    
    private NyPizza(Builder builder){
    	super(builder);
        size = builder.size;
    }
}

 

 

 

Calzone 구체 클래스

  • 소스를 필수 매개변수로 하는 빌더를 생성한다. 
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;
    }
}

 

 

사용 코드

NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(HAM).addToping(ONION).build();

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

 

 

빌더 패턴 단점 - 객체를 만들기 위해 빌더를 만들어야한다.

빌더 생성 비용이 크진 않으나, 다른 패턴보다 코드가 길어질 수 있어 매개변수가 4개 이상일 때 좋다.

하지만 매개 변수가 늘어나는 경우가 많으니, 초반부터 빌더를 적용하는 것이 낫다.

 

반응형