[Effective Java] Builder

dongbin_Shin·2022년 1월 22일
0

이펙티브 자바

목록 보기
2/5
post-thumbnail

생성자에 매개변수가 많다면 빌더를 고려하라

이번 포스팅에서는 인스턴스를 만드는 방법으로 3가지 방안을 비교하며 매개변수가 많을 때 Builder가 갖는 장점을 알아볼 것이다.

점층적 생성자 패턴

static factory method와 생성자로 인스턴스를 생성하는 방법 모두 한가지 단점이 있다. 인스턴스를 만들 때 필요한 매개변수가 많다면 적절히 대응하기 어렵다는 것이다.

바로 예시를 통해 알아보자

public class NameTag {
    private final int number; 	  	//사원 번호	필수
    private final String name; 	  	//이름		필수
    private final String team; 	  	//팀 이름		필수
    private final String phoneNumber; 	//휴대폰 번호	선택
    private final int age; 		//키		선택
	
    public NameTag(int number, String name, String team) {
    	this(number, name, team, "");
    }
    
    public NameTag(int number, String name, String team, String phoneNumber) {
    	this(number, name, team, phoneNumber, 0);
    }
    
    public NameTag(int number, String name, String team, String phoneNumber, int age) {
    	this.number = number;
        this.name = name;
        this.team = team;
        this.phoneNumber = phoneNumber;
        this.age = age;
    }
}

위의 예시처럼 생성자 매개변수의 갯수가 점층적으로 늘어나는 형태이다.

이런 방식은 사용자가 원하지 않는 매개변수까지 포함해야 하는 경우가 생긴다. 위의 예시에서 만약 age를 포함하고 싶다면 phoneNumber필드의 값을 무조건 채워주어야 하는 경우가 이에 해당한다.

결국 점층적 생성자 패턴은 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

사용자는 매개변수가 몇 개인지 주의해서 살펴보아야 하고, 타입이 같은 매개변수가 연달아 나오게 된다면 값이 바뀌어도 에러가 터지지 않아 런타임에 엉뚱한 동작을 하게 된다.

자바 빈즈 패턴

매개변수가 없는 생성자로 객체를 만든 후 setter메서드를 호출해 매개변수 값을 결정하는 방식이다.

public class NameTag {
    private final int number = -1; 	  //사원 번호	필수
    private final String name = "name";   //이름		필수
    private final String team = "team";   //팀 이름	필수
    private final String phoneNumber; 	  //휴대폰 번호	선택
    private final int age; 		  //키		선택
	
    public NameTag() {}
    
    //setter
    public void setNumber(int val) {number = val;}
    public void setName(String val) {name = val;}
    public void setTeam(String val) {team = val;}
    public void setPhoneNumber(String val) {phoneNumber = val;}
    public void setAge(int val) {age = val;}
//사용 코드
NameTag nameTag = new NameTag();
nameTag.setNumber(1);
nameTag.setName("홍길동");
nameTag.setTeam("1팀");
nameTag.setPhoneNumber("010-1111-2222-");
nameTag.setAge(20);

점층적 생성자 패턴의 단점들이 보이지 않게 된다.

하지만 이 패턴도 단점을 갖고 있다.

객체 하나를 만드려면 매서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태가 된다.

이런 문제때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻기 위해서는 추가적인 작업이 필요하다.

빌더 패턴 (Builder pattern)

빌더 패턴은 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 가진다.

필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻고, setter메서드로 선택 매개변수를 세팅한 후 build메서드를 이용해 객체를 얻는다.

public class NameTag {
    private final int number; 	  	//사원 번호	필수
    private final String name; 	  	//이름		필수
    private final String team; 	  	//팀 이름		필수
    private final String phoneNumber; 	//휴대폰 번호	선택
    private final int age; 		//키		선택
	
    public static class Builder {
    	//필수 매개변수
        private final int number;
        private final String name;
        private final String team;
        
        //선택 매개변수 - 기본 값으로 초기화
        private String phoneNumber = "";
        private int age = -1;
        
        public Builder(int number, String name, String team) {
            this.number = number;
            this.name = name;
            this.team = team;
        }
        
        public Builder phoneNumber(String val) {phoneNumber = val; return this;}
      	public Builder age(int val) {age = val; return this;}
        
        public NameTag build() {return new NameTag(this);}
    }
 
    private NameTag(Builder builder) {
        number = builder.number;
     	name = builder.name;
     	team = builder.team;
     	phoneNumber = builder.phoneNumber;
     	age = builder.age;
    }
}
//사용 코드
NameTag nameTag = new NameTag.Builder(1,"홍길동","1팀").phoneNumber("010-1111-2222").age(26).build();

빌더의 새터 메서드들은 빌더 자신을 반환하기 때문에 chaining 방식으로 호출이 가능하다.

메서드 호출이 흐르듯 연결된다 하여 Fluent API, Method Chaining이라고도 한다.

이 패턴을 이용하면 사용자는 코드를 쓰기 쉽고, 읽기 쉽다.

빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수(named optional parameters)를 흉내낸 것이다.

계층적 빌더

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

public abstract class Pizza {
    public enum Topping {HAM, ONOIN, 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();
        
        //하위 클래스는 이 메서드를 override하여 this를 반환해야 함
        protected abstract T self();
    }
    
    Pizza(Builder<?> builder) {toppings = builder.toppings.clone();}
}

Pizza.Builder 클래스는 재귀적 타입 한정<T extends Builder<T>> 을 이용하는 제네릭 타입이다.

또한 self메서드를 이용해 하위 클래스에서 형변환하지 않아도 메서드 연쇄를 지원할 수 있다.

Pizza에는 뉴욕 피자와 시카고 피자가 있다고 하자. 뉴욕 피자는 크기(size) 매개변수, 시카고 피자는 소스 안에 고기를 넣을지(sauceInside)를 필수로 받는다고 하자.

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;
    }
}
public class CgPizza 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 CgPizza build() {
            return new CgPizza(this);
        }
        
        @Override protected Builder self() {return this;}
    }
    
    private CgPizza(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}
//사용 코드
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(ONION).addTopping(PEPPER).build();
CgPizza cg = new CgPizza.Bilder().addTopping(HAM).sauceInside().build();

보통 매개변수가 4개 이상이 되어야 빌더 패턴이 장점을 발휘하지만 API는 시간이 지날수록 매개변수가 많아지는 것을 고려해야 한다.

profile
멋있는 백엔드 개발자

0개의 댓글