엄범

 

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라
  • GoF에 나오는 팩터리 메서드와는 다르다.
  • public 생성자에 `` new``를 써서 객체를 만드는게 아니라, 아래 처럼 팩터리 메서드를 사용하는 것을 말함.
  • public 생성자와 static factory 메서드는 각자 장단이 있지만, 후자가 유리한 경우가 더 많다.

```java

public static Boolean valueOf(boolean b) {

    return b ? Boolean.TRUE : Boolean.FALSE;

}

```

 

  • 장점1. 이름을 가질 수 있다. (가독성 향상)
    • 복잡한 생성 시 되게 유용하다. 예를 들면, 리스트를 받아서 해당 요소들을 하나로 merge한 새로운 객체를 만들 때?
    • 값이 소수인 BigInteger를 반환한다? 후자가 더 가독성이 좋다.

```java

BigInteger(int, int, Random)    vs    BigInteger.probablePrime

// 여러 생성자가 필요한 상황이라면 static factory 메서드로 바꿀 단서가 될 지도 모른다.

```

  • 장점2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
    • 생성해두었던 인스턴스를 캐싱하여 재활용하거나, 싱글턴으로 계속 같은걸 반환해주거나 하는게 가능함.
  • 장점3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    • 리턴 타입은 interface로 하고, 실제 리턴은 그를 구현한 class의 인스턴스를 반환해주는 것으로 하면 구현 class의 상세를 숨길 수 있다.
    • API를 쓰는 사람으로 하여금 interface로 접근 하도록 유도할 수도 있고.
    • 구현 class들을 숨기게 되므로 API 명세도 훨씬 간단해진다. (e.g., java.util.Collections)

 

* 자바 8 이전에는 interface에 static 메서드를 선언할 수 없었기 때문에 이름이 "Type"인 인터페이스를 반환하는 static 메서드가 필요하면, "Types"라는 (인스턴스화 불가한) 동반 클래스(companion)를 만들고 그 안에 정의하는 것이 관례였다.

자바 8 부터는 interface가 static (public)메서드를 가질 수 있으므로 대체로 여기다 둔다.

 

  • 장점4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 역시 API를 사용하는 입장(클라이언트)에서는 어떤 클래스가 반환되는지 관심 없고, 인터페이스로 접근함. 때문에 다음 릴리즈 때 클래스를 아예 교체해도 된다. 자연히 의존성이 낮아진다.
  • 장점5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • interface로 다루면 클래스가 없어도 되니까. DI 받을 때 interface로 받는 그런 맥락.
  • 단점1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • 하지만 상속보다 컴포지션을 사용(아이템 18)하라는 관점에서는 장점으로 받아들일 수도 있음.
  • 단점2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
    • 생성자는 명확한 반면 static factory는 좀 덜 명확하지. 그래서 메서드명을 컨벤션을 따라 잘 지어줘야 한다.

```java

// 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);

```

 

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라 ( default값을 지정할 수 없을 때 )
  • java같은 언어는 메서드 정의 시 파라미터 default값을 정의할 수 없어서, 이를 흉내내려면 생성자를 굉장히 overloading해야 한다.
  • 또는 빈 생성자를 호출한 다음에, setter를 차례대로 호출해주는 방식으로 초기화해야 하는데 이 방법은 객체가 완전히 생성되기 전까지는 일관성이 깨진 상태라는 단점이 있다.
  • 그래서 이런 경우 빌더 패턴이 유리하다.
  • 또한. 빌더 패턴은 클라이언트가 매개변수의 순서를 잘못 전달하는 것도 방지할 수 있다는 장점이 있다.
  • 빌더는 생성할 클래스 안에 static member class로 만들어 두는게 보통이다.
  • item2/builder/NutritionFacts.java  
  • 뭔가 잘못되었다면 보통 `` IllegalArgumentException``을 던진다.
  • 빌더 패턴은 상속을 통해 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
  • item2/hierarchicalbuilder  
    • ``java Builder<T extends Builder<T>>`` 로 하위 타입을 반환할 수 있도록 했는데, 이런 것을 공변 반환 타이핑(covariant return typing)이라고 한다.
  • lombok의 ``java @Builder``를 많이 사용한다.

 

빌더 패턴의 단점은

  • 빌더를 생성해야 해서 성능이 떨어진다.(크지는 않다.)
  • 여러모로 매개변수가 4개 이상은 되어야 빌더 패턴을 쓰는 가치가 있다.
    • 그러나 API는 시간이 지날 수록 매개변수가 많아지는 경향이 있다.
    • 나중에 빌더 패턴으로 전환하게 되면, 애매한 상황이 생긴다. 그래서 애초에 빌더로 시작하는 편이 나을 때가 많다.

 

아이템 3. private 생성자나 enum 타입으로 싱글턴임을 보증하라
아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입(DI)을 사용하라

 

아이템 6. 불필요한 객체 생성을 피하라
  • 그러나 아주 무거운 객체가 아니면 사용자 정의 객체 풀(pool)을 만들지 말라.
  • DB connection 같은 경우 생성 비용이 워낙 비싸니 재사용하는 편이 낫지만, 일반적으로 사용자 정의 객체 풀은 코드를 헷갈리게 만들고 성능을 떨어트린다.
  • 요즘은 JVM 가비지 컬렉터 성능이 좋아서 웬만하면 직접 만든 객체 풀 보다 GC가 생성/삭제하도록 두는게 더 낫다.

 

아이템 7. 다 쓴 객체 참조를 해제하라.
  • 다 쓴 참조를 해제하는 가장 좋은 방법은 유효 범위(scope) 밖으로 벗어나는 것이다.
  • 그게 안되는 경우(전역이라거나.) 다 쓴 참조는 `` null``을 할당하여 더 이상 가리키지 않도록 해 GC가 삭제할 수 있도록 한다.
    • 특히 배열의 경우 많이 놓치게 되는데, ``java e[size] = null`` 따위로 해제해 줄 수 있도록 한다.
  • 메모리 누수가 자주 발생하는 케이스로는
    • 자기 메모리를 직접 관리하는 클래스
      • `` null``을 할당해준다.
    • 캐시
      • WeakHashMap 등의 사용을 고려하거나,
      • 캐시 엔트리의 가치를 점차 떨어뜨리고 주기적으로 청소.
    • Listener, Callback. 등록만 하고 해지하지 않는 경우
      • 약한 참조로 저장하면 GC가 즉시 수거해가도록 할 수 있다. `` WeakHashMap``에 키로 저장하면 약한 참조가 됨
    • 자료 구조 선택 가이드 : WeakHashMap

 

아이템 8. finalizer와 cleaner 사용을 피하라
  • C++에서는 직접 객체를 삭제해야 하니 객체가 사용하던 자원을 반환하는 destructor가 꼭 필요하지만, Java에는 GC가 객체를 삭제한다.
  • 그리고 GC가 어느 시점에 객체를 소멸시킬지 알 수 없기 때문에, finalizer의 호출 시점이 명확하지 않다.
    • 예를 들어 "파일 닫기" 같은 "자원 회수 작업"을 finalizer에게 맡기면, GC가 객체를 소멸시키기 전에 File Handle 개수가 꽉 차버릴 수 있다.
  • finalizer와 cleaner 대신, `` AutoCloseable``을 구현하고 클라이언트에서 객체를 다 쓰고 ``java close()``를 호출해주도록 한다. ( + try-with-resources로 자신을 닫도록 만든다.)

 

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