Programming/JAVA

이펙티브 자바 - 2장 빌더, 싱글톤, private 생성자

긍정왕웹서퍼 2022. 3. 13. 23:21
728x90

아이템 2 - 생성자에 매개변수가 많다면 빌더를 고려하라

정적 팩토리와 생성자에는 똑같은 제약이 하나 있다. 선택적 매개변수가 많을 경우 적절한 대응을 하기가 어렵다는 점이다.

처음 자바를 공부할 때, 사용자가 필요로하는 경우의 수가 많을수록 생성자의 가짓수도 많아지는 경우를 경험해보았을 것이다.

 

1. 점충적 생성자 패턴
public class Camping {
	private final int tent;
    private final int clothes;
    private final int backpack;
    private final int food;
    private final int camper;
    
    public Camping(int tent, int food) {
	    this(tent, food);
    }
    public Camping(int food, int camper) {
    	this(food, camper);
    }
    public Camping(int clothes, int backpack) {
    	this(cloths, backpack);
    }
    public Camping(int tent, int clothes, int backpack, int food, int camper) {
    	this.tent = tent;
        this.clothes = clothes;
        this.backpack = backpack;
        this.food = food;
        this.camper = camper;
    }
}

이처럼 점충적 생성자 패턴도 쓸 수는 있지만, 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 

 

 

2. 자바빈즈 패턴
public class Camping {
	private final int tent = 1;
    private final int clothes = 3;
    private final int backpack = 1;
    private final int food = 6;
    private final int camper = 2;
    
    public Camping() {}
    public void setTent(int val) {tent = val;}
    public void setClothes(int val) {clothes = val;}
    public void setBackpack(int val) {backpack = val;}
    public void setFood(int val) {food = val;}
    public void setCamper(int val) {camper = val;}
}​

 

점충적 생성자 패턴의 단점들이 자바빈즈 패턴에서는 더 이상 보이지 않는다. 인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드가 되었다.

하지만 자바빈즈에도 심각한 단점이 있다. 자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.  일관성이 깨진 객체가 만들어지면, 버그를 심은 코드와 그 버그때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅도 힘들어진다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 안정성을 얻기위해 프로그래머는 추가 작업을 해야한다. 

 

 

3. 빌더 패턴
public class Camping {
	private final int tent;
    private final int camper;
    private final int clothes;
    private final int backpack;
    private final int food;
    
    public static class Builder {
    	// 필수 매개변수 
    	private final int tent;
        private final int camper;
        
        // 선택 매개변수 
        private int clothes = 3;
        private int backpack = 1;
        private int food = 2;
        
        public Builder(int tent, int camper) {
        	this.tent = tent;
            this.camper = camper;
        }
        
        public Builder clothes(int val) {
        	clothes = val;  return this; 
        }
        public Builder backpack(int val) {
        	backpack = val;  return this;
        }
        public Builder food(int val) {
        	food = val;  return this;
        }
        
        private Camping(Builder builder) {
        	tent = builder.tent;
            camper = builder.camper;
            clothes = builder.clothes;
            backpack = builder.backpack;
            food = builder.food;
        }
    }
}

불변 클래스를 만들고, 모든 매개변수의 기본값들을 모아뒀다. 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이런 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 플루언트API (Fluent API) , 메서드 연쇄(method chaining)이라 한다. 

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

 

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하면, 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 하는 것 이다. 

 

이렇게 다양한 방법으로 빌더 패턴을 구현하여 사용하면 많은 장점들이 있지만, 당연히 단점 또한 있다. 우선 빌더 패턴은 객체를 만들기 위해 먼저 빌더부터 만들어야 한다. 생성 비용이 큰편은 아니지만 성능에 민감한 상황에서는 문제가 될 수 있고 점층점 생성자 패턴보다 코드가 장황하고 길어져 매개변수가 4개 이상인 경우에나 그 값어치를 한다. 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있다. 

 

정리 

생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더패턴을 선택하는 게 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다. 

 

 

아이템 3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라 

싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다. 싱글턴을 생성하는 방식은 private 로 감춰두고 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다. 

 

첫번째는 public static final 필드 방식의 싱글턴

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    
    public void leaveTheBuilding() {...}
}

 

두번째는 정적 팩터리 방식의 싱글턴

public class Elvis{
	private static final Elvis INSTANCE = new Elvis();
    private Elvis(){...}
    public static Elvis getInstance() { return INSTANCE; }
    
    public void leaveTheBuilding(){...}
}

 

첫번째 방식은 public static 방식의 큰 장점은 해당 클래스가 싱글턴임이 API에 명백히 드러나고, public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다. 두번째 장점은 간결함이다. 

두번째 방식은 정적 팩터리 방식의 첫 번째 장점은 API를 바꾸지 않고도 싱글턴 패턴이 아니게 변경할 수 있다는 점이고,

두번째 장점은 원한다면 정적 팩터리를 제네릭 싱글 메서드 참조를 공급자로 사용할 수 있다는점이다.

세 번째는 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다는 점이다. 

 

정리

대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다. 

 

 

 

아이템 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라 

가끔, 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있다. 이때 다양한 방식으로 구현하지만 정적 멤버만을 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본생성자를 만들게 된다. 매개변수를 받지 않는 public 생성자가 만들어지며, 사용자는 이 생성자가 자동 생성된 것인지 구분할 수 없다. 

추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 하위 클래스를 만들어 인스턴스화 하면 그만이다. 하지만 인스턴스화를 막는 방법이 있다. 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다. 

 

public class UtilityClass {
// 기본 생성자가 만들어지는 것을 막는다. 인스턴스화 방지용 
	private UtilityClass() {
    	throw new AssertionError();
    }
}

이렇게 기본 생성자가 만들어지는 것을 막을 순 있지만, 명시적으로 표현되는 코드가 아니다 보니 주석처리를 통해 알려야 한다.

이 방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를 private 로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막히게 된다.