본문 바로가기

(35)

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

많은 클래스가 하나 이상의 자원에 의존한다. 예를들어 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스로 구현한 모습을 드물지 않게 볼 수 있다. public class SpellChecker { private static final Lexicion dictionary = ...; private SpellChecker() { } // 객체 생성 방지 public static boolean isValid(String word) { ... } public static List suggestions(String typo) { ... } } 비슷하게 싱글턴으로 구현하는 경우도 흔하다. public class SpellChecker { private final Lexicon dictionar..

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

이 책을 사주며 꼭 빨리 성장하라고 했던 선배(라고 부르고 은인 + 스승이라 부른다)의 당부를 잊고 벌써 몇개월을 소비한 것인가... 빨리 읽어야지 종종 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있다. 객체 지향적으로 사고하지 않는 이들이 종종 남용하는 방식이지만, 분명 나름 쓰임새가 있다. 예를들어 java.lang.Math와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있고, java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(or 팩터리)를 모아놓을 수도 있다(자바8부터는 이런 메서드를 인터페이스에 넣을 수 있다). 마지막으로, final 클래스와 관련한 메서드들을 모아놓을 때도 사용..

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

(이번 장은 잘 이해하지 못하는 부분이 많았다. 2회독 3회독을 하면서 이 부분이 점점 익숙해지기를 바란다..) 싱글톤(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 싱글턴의 대표적인 예는 함수와 같은 무상태(stateless) 객체나 (설계상 유일해야하는) 시스템 컴포넌트가 있다. 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다. 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다. 싱글턴을 만드는 방식은 보통 둘 중 하나이다. 두 방식 모두 생성자는 private로 감춰두고, 유일한 인스턴스에 접근하는 수단으로 public static 멤버를 하나 만든다. 첫번째 방법. public static 멤버가 final 필..

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

아이템 1에서 언급한 정적 팩터리와 생성자에게는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 공통적인 제약사항이 있다. 식품 포장의 영양정보를 포함하는 클래스를 생각해보자. 영양정보는 1회 내용량, 총 n회 제공량, 1회 제공량당 칼로리같은 필수 항목 몇개와 총 지방, 트랜스지방, 포화지방, 콜레스테롤, 나트륨 등 총 20개가 넘는 선택 항목으로 이뤄진다. 그런데 대부분 제품은 이 선택 항목 중 대다수의 값이 0이다. 이런 클래스용 생성자 혹은 정적 팩터리는 어떤 모습일까? 프로그래머들은 이럴 때 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다. 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, 선택 매개변수를 2개까지..

아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다. (클래스의 인스턴스를 반환하는 단순한 정적 메서드를 말하는게 맞다!) 다음 코드는 boolean 기본 타입의 박싱 클래스인 Boolean에서 발췌한 간단한 예다. public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; } 클래스는 클라이언트에 public 생성자 대신 정적 팩터리 메서드를 제공할 수 있다고 했다. (같이 제공할수도 있다) 이 방식에는 장점, 단점이 모두 존재한다. 먼저 장점 다섯 가지에 대해 알아본다. 장점 1. 이름을 가질 수 있다. 생성자에 넘기..

개발서적/이펙티브 자바

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

728x90

 

많은 클래스가 하나 이상의 자원에 의존한다. 예를들어 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스로 구현한 모습을 드물지 않게 볼 수 있다. 

public class SpellChecker {
    private static final Lexicion dictionary = ...;
    
    private SpellChecker() { } // 객체 생성 방지
    
    public static boolean isValid(String word) { ... }
    public static List<String> suggestions(String typo) { ... }
}

비슷하게 싱글턴으로 구현하는 경우도 흔하다. 

public class SpellChecker {
    private final Lexicon dictionary = ...;
    
    private SpellChecker(...) {}
    public static SpellChecker INSTANCE = new SpellChecker(...);
    
    public boolean isValid(String word) {...}
    public List<String> suggestions(String typo) {...}
}

두 방식 모두 사전을 단 하나만 사용한다고 가정한다는 점에서 그리 훌륭해 보이지 않다. 실전에서는 사전이 언어별로 따로 있고 특수 어휘용 사전을 별도로 두기도 한다. 심지어 테스트용 사전도 필요할 수 있다. 사전 하나로 이 모든 쓰임에 대응할 수 있기를 바라는 건 너무 어리석은 생각이다. 

SpellChecker가 여러 사전을 사용할 수 있도록 만들어보자. 간단히 dictionary 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가할 수 있지만, 아쉽게도 이 방식은 어색하고 오류를 내기 쉬우며 멀티스레드 환경에서는 쓸 수 없다. 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다. 

대신 클래스(SpellChecker)가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원(dictionary)을 사용해야 한다. 이 조건을 만족하는 간단한 패턴이 있으니, 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다. 이는 의존 객체 주입의 한 형태로, 맞춤법 검사기를 생성할 때 의존 객체인 사전을 주입해주면 된다. 

public class SpellChecker {
    private final Lexicon dictionary;
    
    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNoneNull(dictionary);
    }
    
    public boolean isValid(String word) {... }
    public List<String> suggestions(String typo) {...}
}

의존 객체 주입 패턴은 아주 단순하여 수많은 프로그래머가 이 방식에 이름이 있다는 사실도 모른 채 사용해왔다. 예에서는 dictionary라는 딱 하나의 자원만 사용하지만, 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 작동한다. 또한 불변을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있기도 하다. 의존 객체 주입은 생성자, 정적 팩터리, 빌더, 모두에 똑같이 응용할 수 있다. 

이 패턴의 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다. 즉, 팩터리 메서드 패턴(Factory Method pattern)을 구현한 것이다. Supplier<T> 인터페이스가 팩터리를 표현한 완벽한 예다. Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입(bounded wildard type)을 사용해 팩터리의 타입 매개변수를 제한해야 한다. 이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다. 예컨대 다음 코드는 클라이언트가 제공한 팩터리가 생성한 타일(Tile)들로 구성된 모자이크(Mosaic)를 만드는 메서드다. 

Mosaic create(Supplier<? extends Tile> tileFactory> {... }

의존 객체 주입의 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 수천 개나 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다. 대거(Dagger), 주스(Guice), 스프링(Spring) 같은 의존 객체 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있다. 이런 프레임워크들은 의존 객체를 직접 주입하도록 설계된 API를 알맞게 응용해 사용하고 있음을 언급한다.

더보기

핵심 정리

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들은 클래스가 직접 만들게 해서도 안 된다. 대신 필요한 자원들(혹은 그 자원을 만들어주는 팩터리를) 생성자에 (혹은 정적 팩터리나 빌더에) 넘겨주자. 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.  

 

728x90
개발서적/이펙티브 자바

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

728x90

이 책을 사주며 꼭 빨리 성장하라고 했던 선배(라고 부르고 은인 + 스승이라 부른다)의 당부를 잊고 벌써 몇개월을 소비한 것인가... 빨리 읽어야지 

 

종종 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있다. 객체 지향적으로 사고하지 않는 이들이 종종 남용하는 방식이지만, 분명 나름 쓰임새가 있다. 예를들어 java.lang.Math와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있고, java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(or 팩터리)를 모아놓을 수도 있다(자바8부터는 이런 메서드를 인터페이스에 넣을 수 있다). 마지막으로, final 클래스와 관련한 메서드들을 모아놓을 때도 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가능하기 때문이다. 

정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다. 즉, 매개변수를 받지 않는 public 생성자가 만들어지며, 사용자는 이 생성자가 자동 생성돈 것인지 구분할 수 없다. 실제로 공개된 API들에서도 이처럼 의도치 않게 인스턴스화할 수 있게 된 클래스가 종종 목격되곤 한다. 

추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 하위 클래스를 만들어 인스턴스화하면 그만이다. 이를 본 사용자는 상속해서 쓰라는 뜻으로 오해할 수 있으니 더 큰 문제다. 다행히도 인스턴스화를 막는 방법은 아주 간단하다. 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다. 

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

명시적 생성자가 private이니 클래스 바깥에서 접근할 수 없다. 꼭 AssertionError를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 해준다. 이 코드는 어떤 환경에서도 클래스가 인스턴스화되는 것을 막아준다. 그런데 생성자가 분명 존재하는데 호출할 수는 없다니, 그다지 직관적이지 않다. 그러지 앞의 코드처럼 적절한 주석을 달아주도록 하자, 이 방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를 private로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 없다. 

 

728x90
개발서적/이펙티브 자바

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

728x90

(이번 장은 잘 이해하지 못하는 부분이 많았다. 2회독 3회독을 하면서 이 부분이 점점 익숙해지기를 바란다..)

싱글톤(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 싱글턴의 대표적인 예는 함수와 같은 무상태(stateless) 객체나 (설계상 유일해야하는) 시스템 컴포넌트가 있다.

클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다. 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다. 

싱글턴을 만드는 방식은 보통 둘 중 하나이다. 두 방식 모두 생성자는 private로 감춰두고, 유일한 인스턴스에 접근하는 수단으로 public static 멤버를 하나 만든다.

첫번째 방법. public static 멤버가 final 필드인 방식 

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

private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출된다. public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 하나뿐임이 보장된다. (리플렉션 API인 AccesibleObject.setAccessible을 사용해서 private 생성자를 호출할 수 있지만.... 이런 방법을 막을려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다. 

두번째 방법. 정적 팩터리 메서드를 public static 멤버로 제공

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

Elvis.getInstance는 항상 같은 객체의 참조를 반환하므로 인스턴스가 하나뿐임을 보장한다. 

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

반면에 두번째 방식의 장점은 간결함이다. 정적 팩터리 방식의 장점은 1) API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다. 2) 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. 3) 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다는 점이다. (Elvis::getInstance를 Supplier<Elvis>로 사용하는 식이다)

이러한 장점들이 필요하지 않다면 public 필드 방식이 좋다. 

둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현하고 선언하는 것만으로는 부족하다. 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 한다. 이렇게 하지 않으면 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어진다. 두번째 방법의 코드를 예로 들자면 가짜 Elvis가 탄생한다는 뜻이다. 가짜 Elvis 탄생을 예방하고 싶다면 Elvis 클래스에 다음과 같이 readResolve 메서드를 추가하자. 

private Object readResolve() {
    return INSTANCE;
}

 위 코드는 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다. 

세번째 방법. 원소가 하나인 열거 타입을 선언하는 방식

public enum Elvis {
    INSTANCE;
    
    public void leaveTheBuilding() { ... }
}

public 필드 방식과 비슷하지만, 더 간결하고, 추가 노력 없이 직렬화할 수 있고, 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다. 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다. 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다).

 

728x90
개발서적/이펙티브 자바

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

728x90

아이템 1에서 언급한 정적 팩터리와 생성자에게는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 공통적인 제약사항이 있다. 

식품 포장의 영양정보를 포함하는 클래스를 생각해보자. 영양정보는 1회 내용량, 총 n회 제공량, 1회 제공량당 칼로리같은 필수 항목 몇개와 총 지방, 트랜스지방, 포화지방, 콜레스테롤, 나트륨 등 총 20개가 넘는 선택 항목으로 이뤄진다. 그런데 대부분 제품은 이 선택 항목 중 대다수의 값이 0이다. 

이런 클래스용 생성자 혹은 정적 팩터리는 어떤 모습일까? 

프로그래머들은 이럴 때 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다. 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, 선택 매개변수를 2개까지 받는 생성자, ... 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려가는 방식이다. 아래의 코드가 그 예제이다.

public class NutritionFacts {
    private final int servingSize; //(ml, 1회 제공량) 필수
    private final int servings; // (회, 총 n회 제공량) 필수
    private final int calories; // (1회 제공량당) 선택
    private final int fat; // (g/1회 제공량) 선택
    private final int sodium; // (mg/1회 제공량) 선택 
    private final int carbohydrate; // (g/1회 제공량) 선택
    
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }
    
   public NutritionFacts(int servingSize, int servings, int calories,
           int fat) {
       this(servingSize, servings, calories, fat, 0);
   }
   
   public NutritionFacts(int servingSize, int servings, int calories,
           int fat, int sodium) {
       this(servingSize, servings, calories, fat, sodium, 0);
   }
   
   public NutritionFacts(int servingSize, int servings, int calories,
           int fat, int sodium, int carbohydrate) {
       this.servingSize = servingSize;
       this.servings = servings;
       this.calories = calories;
       this.fat = fat;
       this.sodium = sodium;
       this.carbohydrate = carbohydrate;
   }
}
   
   

이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다. 

NutritionFacts cocaCola = new NewtritionFacts(240, 8, 100, 0, 35, 27);

 

보통 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수 없이 그런 매개변수에도 값을 지정해줘야 한다. 앞 코드에서는 지방(fat)에 0을 넘겼다. 이 예에서는 매개변수가 6개뿐이라 나쁘지 않아 보였지만, 수가 더 늘어난다면 이 방식은 문제가 있다. 즉 점층적 생성자 패턴은 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 또한 코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 것이고, 매개변수가 몇 개인지도 주의해서 세어 보아야 할 것이다. 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어질 수 있다. 클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 된다. 

 

이번에는 선택 매개변수가 많을 때 활용할 수 있는 두 번째 대안인 자바빈즈패턴(JavaBeans pattern)을 살펴본다. 매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다. 

public class NutritionFacts {
    // 매개변수들은 (기본값이 있다면) 기본값으로 초기화된다.
    private int servingSize = -1; // 필수; 기본값 없음
    private int servings = -1; // 필수; 기본값 없음
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;
    
    public NutritionFacts() { }
    // 세터 메서드들
    public void setServingSize(int val) { servingSize = val; }
    public void setServings(int val) { servings = val; }
    public void setCalories(int val) { calories = val; }
    public void setFat(int val) { fat = val; }
    public void setSodium(int val) { sodium = val; }
    public void setCarbohydrate(int val) { carbohydrate = val; }
}

 

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

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

 

하지만 자바빈즈는 치명적인 단점을 가지고 있다. 자바빈즈패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다. 

점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라졌다. 일관성이 깨진 객체가 만들어지면, 버그를 심은 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅도 쉽지 않다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야만 한다. 

더보기

이러한 단점을 완화하고자 생성이 끝난 객체를 수동으로 얼리고(freezing), 얼리기 전에는 사용할 수 없도록 하기도 한다. 하지만 이 방법은 다루기 어려워서 실전에서는 거의 쓰이지 않는다. 이 방법을 쓴다고 하더라도 객체 사용전에 프로그래머가 freeze 메서드를 확실히 호출해줬는지를 컴파일러가 보증할 방법이 없어서 런타임 오류에 취약하다.

 

다행히 이번 포스팅에서 소개할 세 번째 대안이 있다. 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattern)이다. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다. 그런 다음 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다. 마지막으로 매개변수가 없는 build 메서드를 호출해 우리에게 필요한 객체를 얻는다. 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 게 보통이다.

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    
    public static class Builder {
        // 필수 매개변수
        private final int servingSize;
        private final int servings;
        
        // 선택 매개변수 - 기본값으로 초기화한다. 
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
        
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        
        public Builder calories(int val) { calories = val; return this; }
        
        public Builder fat(int val) { fat = val; return this; }
        
        public Builder sodium(int val) { sodium = val; return this; }
        
        public Builder carbohydrate(int val) { carbohydrate = val; return this; }
        
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

 

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

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100).sodium(35).carbohydrate(27).build();

이 클라이언트 코드는 쓰기 쉽고, 무엇보다도 읽기 쉽다. 빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 선택적 매개변수(named optional parameters)를 흉내낸 것이다. 

잘못된 매개변수를 최대한 일찍 발견하려면 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식(invariant)을 검사하자. 공격에 대비해 이런 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들을 검사해야 한다. 검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IllegalArgumentException을 던지면 된다. 

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

빌터 패턴에 장점만 있는 것은 아닏다. 객체를 만들려면, 그에 앞서 빌더부터 만들어야 한다. 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 또한 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명심하자. 생성자나 정적 팩터리 방식으로 시작했다가 나중에 매개변수가 많아지면 빌더 패턴으로 전환할 수도 있지만, 이전에 만들어둔 생성자와 정적 팩터리가 걸리적 거릴 것이다. 그러니 애초에 빌더로 시작하는 편이 나을 때가 많다. 

핵심 정리

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

 

728x90
개발서적/이펙티브 자바

아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라

728x90

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다. (클래스의 인스턴스를 반환하는 단순한 정적 메서드를 말하는게 맞다!)

다음 코드는 boolean 기본 타입의 박싱 클래스인 Boolean에서 발췌한 간단한 예다.

public static Boolean valueOf(boolean b) {
	return b ? Boolean.TRUE : Boolean.FALSE;
}

 

클래스는 클라이언트에 public 생성자 대신 정적 팩터리 메서드를 제공할 수 있다고 했다. (같이 제공할수도 있다)
이 방식에는 장점, 단점이 모두 존재한다. 먼저 장점 다섯 가지에 대해 알아본다.

장점


1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체만으로 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으며 반환될 객체의 특성을 쉽게 묘사할 수 있다.

2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

이 덕분에 불편클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 따라서 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려준다. 플라이웨이트 패턴도 이와 같은 기법이라 할 수 있다.
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다. 이런 클래스를 인스턴스 통제(instance-controlled) 클래스라 한다. 

더보기

인스턴스를 통제하는 이유는 무엇일까? 인스턴스를 통제하면 클래스를 싱글턴으로 만들 수도, 인스턴스화 불가로 만들 수도 있다. 또한 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.

 

3. 반환 타입의 하위 객체를 반환할 수 있는 능력이 있다.

이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다. API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 유지할 수 있다. 그리고 결과적으로 API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮춰준다.

 

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다. 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수 도 없고 알 필요도 없다.

 

5. 정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않아도 된다.

이런 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다. (ex. JDBC )
서비스 제공 프레임워크에서 제공자는 서비스의 구현체다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다

  1. 서비스 인터페이스(service interface) : 구현체의 동작을 정의
  2. 제공자 등록 API (provider registration API) : 제공자가 구현체를 등록할 때 사용함
  3. 서비스 접근 API (service access API) : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
  4. 서비스 제공자 인터페이스(service provider interface) : 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명한다.
    (3개의 핵심 컴포넌트와 더불어 종종 서비스 제공자 인터페이스가 같이 쓰인다)

 

단점


1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

컬렉션 프레임워크의 유틸리티 구현 클래스들을 상속할 수 없다는 이야기다. (어찌 보면 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.)

2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다

생성자처럼 API설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.

아래는 정적 팩터리 메서드에 흔히 사용하는 명명 방식들을 소개한다

 

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
  • valueOf : from과 of의 더 자세한 버전
  • instance or getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
  • create or newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.
  • type : getType과 newType의 간결한 버전

 

정리

정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작성 public 생성자를 제공하던 습관이 있다면 고치자.

 

728x90