아이템 1. 생성자 대신 static 팩토리 메소드를 고려하라

// 예시 코드 1
public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
} 

클래스는 클라이언트에 public 생성자 대신 (혹은 생성자와 함께) 정적 팩터리 메서드를 제공할 수 있다.

장점 1. 이름을 가질 수 있다.

  • 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
  • 생성자는 시그니처에 제약이 있다. 똑같은 타입을 파라미터로 받는 생성자 두개를 만들 수 없기 때문에 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.

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

  • 매번 새로운 객체를 만들 필요가 없는 경우에 미리 만들어둔 인스턴스 또는 캐시해둔 인스턴스를 반환할 수 있다.
  • (예시 코드 1)에서 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다.

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

  • 리턴 타입은 interface로 하고, 실제 리턴은 그를 구현한 class의 인스턴스를 반환해주는 것으로 하면 구현 class의 상세를 숨길 수 있다.
  • API를 쓰는 사람으로 하여금 interface로 접근 하도록 유도할 수도 있고, 구현 class들을 숨기게 되므로 API 명세도 훨씬 간단해진다.
  • java.util.Collections는 45개에 달하는 인터페이스의 구현체의 인스턴스를 제공하지만 그 구현체들은 전부 non-public으로 인터페이스 뒤에 감춰서 public으로 제공해야 할 API를 줄였다.

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

  • API를 사용하는 클라이언트에서는 어떤 클래스가 반환되는지 관심 없고, 인터페이스로 접근하기 때문에 다음 릴리즈에서는 다른 클래스의 객체를 반환해도 된다.
  • 의존성이 낮아진다.

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

  • 장점 3, 4와 비슷한 개념이다. 이러한 유연성을 제공하는 static 팩토리 메소드는 서비스 프로바이더 프레임워크의 근본이다.
  • 대표적인 서비스 제공자 프레임워크로는 JDBC가 있다.
  • interface로 다루면 클래스가 없어도 되니까. DI 받을 때 interface로 받는 그런 맥락.

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

  • 불변 타입(아이템 17)인 경우나 상속 대신 컴포지션을 권장(아이템 18)하기 때문에 장점으로 볼 수도 있다.

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

  • 생성자는 명확한 반면 static factory는 좀 덜 명확하기 때문에 메서드명을 컨벤션을 따라서 잘 지어줘야 한다.

    // from : ~로 부터 반환. 형변환해서 반환.
    Date d = Date.from(instant);
    // of : 집계해서 반환.
    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
    // valueOf : from과 of의 더 자세한 버전
    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
    // instance 혹은 getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않음.
    StackWalker luke = StackWalker.getInstance(options);
    // get{Type} : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때.
    FileStore fs = Files.getFileStore(path);
    // new{Type} : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때.
    BufferedReader br = Files.newBufferedReader(path);
    // {type} : get{Type}과 new{Type}의 간결한 버전
    List<Complaint> litany = Collections.list(legacyLitany);
  • 생성자는 Javadoc 상단에 모아서 보여주지만 static 팩토리 메소드는 API 문서에서 특별히 다뤄주지 않는다. 따라서 클래스나 인터페이스 문서 상단에 팩토리 메소드에 대한 문서를 제공하는 것이 좋겠다.

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

생성자:

  • java같은 언어는 메서드 정의 시 파라미터 default값을 정의할 수 없어서, 이를 흉내내려면 생성자를 굉장히 overloading해야 한다.

자바빈:

  • 빈 생성자를 호출한 다음에, setter를 차례대로 호출해주는 방식
  • 최종적인 인스턴스를 만들기까지 여러번의 호출을 거쳐야 하기 때문에 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.
  • (게터와 세터가 있어서) 불변 클래스(아이템 17)로 만들지 못한다.
  • (쓰레드 간에 공유 가능한 상태가 있으니까) 쓰레드 안정성을 보장하려면 추가적인 수고 (locking 같은) 가 필요하다.

빌더

  • 빌더 패턴은 만들려는 객체를 바로 만들지 않고 클라이언트는 빌더(생성자 또는 static 팩토리)에 필수적인 매개변수를 주면서 호출해 Builder 객체를 얻은 다음 빌더 객체가 제공하는 세터와 비슷한 메소드를 사용해서 부가적인 필드를 채워넣고 최종적으로 build라는 메소드를 호출해서 만들려는 객체를 생성한다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
  • 빌더는 꽤 유연해서 빌더 하나로 여러 객체를 생성할 수도 있고 매번 생성하는 객체를 조금씩 변화를 줄 수도 있다. 만드는 객체에 시리얼 번호를 증가하는 식으로.
  • 빌더 패턴은 클라이언트가 매개변수의 순서를 잘못 전달하는 것도 방지할 수 있다는 장점이 있다.
  • 빌더는 생성할 클래스 안에 static member class로 만들어 두는게 보통이다.
  • item2/builder/NutritionFacts.java
  • 뭔가 잘못되었다면 보통 IllegalArgumentException을 던진다.
  • 빌더 패턴은 상속을 통해 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
  • item2/hierarchicalbuilder
    • Builder<T extends Builder<T>> 로 하위 타입을 반환할 수 있도록 했는데, 이런 것을 공변 반환 타이핑(covariant return typing)이라고 한다.
    • lombok의 @Builder를 많이 사용한다.
    • 빌더 애너테이션을 사용하면서 어떤 필드에 =으로 값을 초기화하는 경우 에러가 발생하므로, 초기화할 대상 필드에는 @Builder.Default를 붙여준다.
  • 단점으로는 객체를 만들기 전에 먼저 빌더를 만들어야 하는데 성능에 민감한 상황에서는 그점이 문제가 될 수도 있다. 그리고 생성자를 사용하는 것보다 코드가 더 장황하다. 따라서 빌더 패턴은 매개변수가 많거나(4개 이상?) 또는 앞으로 늘어날 가능성이 있는 경우에 사용하는것이 좋다.

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

싱글톤: 오직 한 인스턴스만 만드는 클래스

  • 함수 같은 Stateless 객체(아이템 24) 또는 본질적으로 유일한 시스템 컴포넌트에 사용
  • 싱글톤을 사용하는 클라이언트 코드를 테스트 하는게 어렵다. 싱글톤이 인터페이스를 구현한게 아니라면 mock으로 교체하는게 어렵기 때문이다.

public static final 필드 방식의 싱글톤

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    ...
}
  • 리플렉션을 사용해서 private 생성자를 호출할 수도 있는 단점이 있다.
    • 이 방법을 막고자 생성자 안에서 카운팅하거나 flag를 이용해서 예외를 던지게 할 수도 있다.
  • 이런 API 사용이 정적 팩토리 메소드를 사용하는 방법에 비해 더 명확하고 더 간단하다.

정적 팩터리 방식의 싱글턴

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

    public static Elvis getInstance() {
        return INSTANCE;
    }
    ...
}
  • API를 변경하지 않고 처음엔 싱글톤으로 쓰다가 나중엔 쓰레드당 새 인스턴스를 만드는 등 클라이언트 코드를 고치지 않고도 변경할 수 있다.
  • 필요하다면 Generic 싱글톤 팩토리(아이템 30)를 만들 수도 있다.
  • 정적 팩토리 메소드를 Supplier<Elvis>에 대한 메소드 레퍼런스로 사용할 수도 있다.

직렬화 (Serialization)

위에서 살펴본 두 방법 모두, 직렬화에 사용한다면 역직렬화 할 때 같은 타입의 인스턴스가 여러개 생길 수 있다. 그 문제를 해결하려면 모든 인스턴스 필드에 transient를 추가 (직렬화 하지 않겠다는 뜻) 하고 readResolve 메소드를 다음과 같이 구현하면 된다. (객체 직렬화 API의 비밀)

private Object readResolve() {
    return INSTANCE;
}

enum 타입 방식의 싱글턴 - 바람직한 방법

public class Elvis {
    INSTANCE;
    ...
}
  • 직렬화/역직렬화 할 때 코딩으로 문제를 해결할 필요도 없고, 리플렉션으로 호출되는 문제도 고민할 필요없는 방법이다.
  • 이 방법은 Enum 말고 다른 상위 클래스를 상속해야 한다면 사용할 수 없다. (인터페이스는 구현할 수 있다)

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

static 메서드와 static 필드를 모아둔 클래스를 만든 경우 해당 클래스를 abstract로 만들어도 상속 받아서 인스턴스를 만들 수 있다.

아무런 생성자를 만들지 않은 경우 컴파일러가 기본적으로 아무 인자가 없는 pubilc 생성자를 만들어 주기 때문에 명시적으로 private 생성자를 추가해야 한다.

// 인스턴스를 만들 수 없는 유틸리티 클래스
public class UtilityClass {
    // 기본 생성자가 만들어지는 것을 막는다. (인스턴스화 방지용)
    private UtilityClass() {
        throw new AssertionError();
    }
    ...
}
  • AssetionError를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 해준다.
  • 생성자가 분명 존재하는데 호출할 수가 없다니, 그다지 직관적이지 않기 때문에 위에 코드처럼 주석을 추가하는 것이 좋다.
  • 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를 private로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막힌다.

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

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
대신 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다. 이 조건을 만족하는 간단한 패턴은 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.

public class SpellChecker {
    private final Lexicon dictionary = ...;
    private SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }
    ...
}
  • 의존 객체 주입 패턴은 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 작동한다.

  • 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.

  • 이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. (팩터리 메서드 패턴 구현)

  • Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입(아이템 31)을 사용해 팩터리의 타입 매개변수를 제한해야 한다. 이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.

  • 클라이언트가 제공한 팩터리가 새성한 타일들로 구성된 모자이크를 만드는 메서드:

      Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
  • 의존 객체 주입이 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 수천 개나 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다.

  • 대거, 주스, 스프링 같은 의존 객체 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있다.

  • 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.

아이템 6. 불필요한 객체 생성을 피하라

똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다.

  • 생성 비용이 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하자
  • 메서드가 처음 호출될 때 필드를 초기화하는 지연 초기화(아이템 83)로 불필요한 초기화를 없앨 수는 있지만, 권하지는 않는다. 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을 때가 많기 때문이다(아이템 67).
  • 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자. (오토박싱을 하면서 불필요한 인스턴스를 만들게 되는 경우가 있다)
  • 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨린다. 요즘 JVM의 가비지 컬렉터는 상당히 잘 최적화되어서 가벼운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.

대조적인 방어적 복사를 다루는 아이템 50

  • 이번 아이템은 방어적 복사를 다루는 아이템 50과 대조적이다. 이번 아이템이 "기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라"라면, 아이템 50은 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라"다.
  • 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자. 방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 보안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 준다.

아이템 7. 다 쓴 객체 참조를 해제하라.

가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다.
객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체...)를 회수해가지 못한다.

해당 참조를 다 썼을 때 null 처리(참조 해제)

  • 프로그래머는 객체가 배열의 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다.
  • 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.
  • 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.
  • 우리가 변수의 범위를 최소가 되게 정의했다면(아이템57) 이 일은 자연스럽게 이뤄진다.
  • 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.
    • 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해줘야 한다.

캐시

  • 객체 참조를 캐시에 넣고 나서 다 쓴 뒤에도 잊어버리는 경우가 있다.
  • 해법은 여러가지이다. (예: WeakHashMap을 사용)

리스너 혹은 콜백

  • 클라리언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다. 이럴 때 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해간다. (예: WeakHashMap에 키로 저장하면 된다)

아이템 8. finalizer와 cleaner 사용을 피하라

자바는 finalizer와 cleaner 두 가지 객체 소멸자를 제공한다.

그중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

그 대안으로 소개된 cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

  • 객체에 접근 할 수 없게 된 후 finalizer와 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없기 때문에 이들로는 제때 실행되어야 하는 작업은 절대 할 수 없다.
  • finalizer나 cleaner를 얼마나 신속히 수행할지는 가비지 컬렉터 구현마다 천자만별이다.
    • 테스트한 JVM에서는 완벽하게 동작하던 프로그램이 고객의 시스템에서는 엄청난 문제가 생길 수도 있다.
  • 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.
    • 데이터베이스 같은 공유 자원의 영구 락 해제를 finalizer나 cleaner에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다.
  • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.
  • final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.
  • 파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 finalizer나 cleaner 대신 AutoCloseable을 구현해주고, 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다. (try-with-resources를 사용해야 한다. 아이템 9)
  • cleaner는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 물론 리얼ㄴ 경우라도 불확실성과 성능 저하에 주의해야 한다.

아이템 9. try-finally 보다는 try-with-resources를 사용하라.

자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.

  • try-finally 문이 중첩될 경우 두 번째 예외가 첫 번째 예외를 집어삼켜 버려서 디버깅이 어렵다.
  • try-finally에서처럼 try-with-resources에서도 catch 절을 쓸 수 있어서 try 문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있다.
  • 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자.

참고
https://github.com/keesun/study/blob/master/effective-java/item3.md
https://umbum.dev/1016

+ 따끈한 최근 게시물