자바에서는 두 가지 객체 소멸자를 제공한다.

  • finalizer: 예측 불가능하고, 상황에 따라 위험할 수 있어 자바 9에서 deprecated API로 지정되었다.
  • cleanerfinalizer를 대체하는 덜 위험한 API이지만, 여전히 예측할 수 없고 느리다.

사용하지 말아야 할 이유

수행 시점

finalizer와 cleaner가 수행되기까지 얼마나 걸릴지 알 수 없으므로 제때 실행되어야 하는 작업을 맡기면 오류를 낼 수 있다. 객체 소멸자가 동작하는 시점은 GC 알고리즘에 달렸으며 그 구현 방법에 따라 천차만별이므로 애초에 사용하지 않는 게 좋다.

만약 클래스에 finalizer를 사용한다면 인스턴스 자원 회수가 지연되어 OutOfMemory를 내며 프로그램이 죽을 수 있다.

수행 여부

finalizer와 cleaner는 객체 소멸 수행 여부를 보장하지 않으므로 데이터베이스 공유 락 해제와 같이 상태를 수정해야 하는 작업에는 사용하면 안된다.

보완 클래스가 제 역할을 못함

System.gc나 System.runFinalization 메서드를 사용하면 finalizer와 cleaner가 실행될 가능성을 높일 수는 있으나 보장하진 않아 사용하지 않는 게 낫다.

낮은 성능

finalizer가 객체를 소멸시키면 AutoClosable 객체를 사용해서 가비지 컬렉터가 수거하도록 할 때보다 50배 정도 느리다.

보안 취약

객체 생성을 막으려면 원래는 생성자에서 예외를 던지면 되지만 finalizer는 다른 방식으로 동작한다. 만약 객체 직렬화 화정에서 예외가 발생하면, 생성되던 객체에서 finalizer가 수행될 수 있고 일그러진 객체가 만들어진다. final 클래스는 하위 클래스를 만들 수 없으니 만약 final이 아닌 클래스를 공격에 안전하게 만들려면 아무 일도 하지 않는 finalizer 메서드를 만들고 final로 선언해야 한다.(ybs 님의 좋은 예시!)

대안

파일이나 스레드 등 종료해야 하는 자원을 담고 있는 클래스에서 finalizercleaner를 대체하는 방법은 AutoClosable을 구현하고 close 메서드를 호출하는 것이다.

 

그럼 finalizer와 cleaner는 대체 언제 쓰는가?

close 메서드 안전망

자원을 가진 클라이언트가 close 메서드를 호출하지 않는 경우를 대비해, cleaner와 finalizer를 객체를 수거하는 안전망으로 사용할 수 있다.

네이티브 피어와 연결된 객체

자바 객체가 네이티브 메서드를 통해 네이티브 객체를 생성하면 가비지 컬렉터는 이 객체의 존재를 알지 못한다. 네이티브 객체를 회수하려면 성능 저하를 감수하고 close 메서드를 사용할 수 있다.

Java의 가비지 컬렉터는 다 쓴 객체를 알아서 회수해주지만, 여전히 클라이언트는 메모리를 관리해야 한다.

특정 조건으로 인해 가비지 컬렉터가 다 쓴 객체 메모리를 회수하지 않으면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하될 것이다. 성능 저하가 심해진다면 디스크 페이징이나 OutOfMemoryError를 일으켜 예기치 않게 프로그램이 종료된다.

메모리 누수가 일어나는 대표적인 원인을 살펴보자.

 

메모리를 직접 관리하는 클래스

아래 Stack 클래스는 element 메모리를 직접 관리한다. 나쁜 예시 pop() 메서드에서는 스택이 줄어들 때 인덱스가 size보다 큰 배열 요소를 GC하지 못한다. 따라서 좋은 예시 pop()과 같이 다 쓴 참조를 null 처리하는 과정이 필요하다.

public class Stack {
  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  public Stack() {
    elements = new Object[DEFAULT_INITIAL_CAPACITY];
  }

  public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
  }

  // 나쁜 예시
  public Object pop() {
    if (size == 0) throw new EmptyStacException();
    return elements[--size];
  }

  // 새로 들어올 원소를 위한 공간 확보
  private void ensureCapacity() {
    if (elements.length == size)
      elements = Arrrays.copyOf(elements, 2 * size + 1);
  }
}
// 좋은 예시
public Object pop() {
  if (size == 0) throw new EmptyStacException();
  Object result = elements[--size];
  element[size] = null; // 참조 해제
  return result;
}

그러나 이렇게 객체 참조를 수동으로 null 처리하는 일은 예외적인 경우에만 발생해야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것으로, 변수의 범위를 최소로 정의했다면 이 일은 자연스럽게 이뤄진다.

일반적으로 자기 메모리를 스스로 관리하는 클래스라면 클라이언트는 항상 메모리 누수에 주의해야 한다.

 

캐시

객체 참조를 캐시에 넣고 클라이언트가 잊는다면 메모리 누수가 날 수 있다.

만약 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있어야 한다면, WeakHashMap을 사용해서 캐시를 만들면 된다. 다 쓴 엔트리가 자동으로 제거될 것이다.

일반적으로는 캐시 엔트리 유효 기간을 알 수 없으므로 쓰지 않는 엔트리를 가끔 청소한다. ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 메서드를 호출해 부수 작업으로 수행할 수 있다.

 

콜백 리스너

클라이언트가 콜백을 등록만 하고 해지하지 않는 경우 콜백이 쌓여 메모리 누수가 날 수 있다.

이 경우 콜백을 WeakHashMap과 같은 약한 참조로 저장하면 가비지 컬렉터가 메모리를 수거한다.

똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 게 나을 때가 있다. 아래는 객체를 재사용하는 방향으로 개선할 수 있는 코드 예시이다.

 

동일한 문자열 반복 생성

아래 코드는 "IU is the best"라는 문자열 객체를 String 생성자에 넘겨 완전히 똑같은 객체를 하나 더 생성한다. 반복문이나 자주 사용되는 메서드에 아래 코드가 있다면, 쓸모없는 객체가 대량으로 만들어질 것이다. 좋은 예시는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스만 사용하기 때문에 같은 VM에서 똑같은 문자열을 사용하는 모든 코드가 같은 객체를 재사용하는 것이 보장된다.

// 나쁜 예시
String s = new String("IU is the best");

// 좋은 예시
String s = "IU is the best";

 

비싼 객체 반복 생성

생성 비용이 비싼 객체인지 매번 명확히 알 수는 없지만, 성능이 갑자기 떨어진다면 비싼 객체를 생성했는지 의심할 수 있다. 정규표현식을 사용해 문자열 매칭을 확인하는 아래 코드가 그 예시이다. String.matches는 정규표현식으로 문자열 형태를 확인하는 쉬운 방법이지만 정규표현식에 사용하는 Pattern 인스턴스는 한 번 사용하고 버려지기 때문에 불변으로 생성하는 것이 좋다. Pattern을 static final로 끄집어내 이름을 지으면 캐싱된 인스턴스를 재사용할 수 있고 코드 의미도 훨씬 잘 드러난다.

// 나쁜 예시
static boolean isRomanNumeral(String s) {
  return s.matches( ... );
}

// 좋은 예시
static boolean isRomanNumeral(String s) {
  private static final Patten ROMAN = Pattern.compile( ... );

  static boolean inRomanNumeral(String s) {
    return ROMAN.matcher(s).matches();
  }
}

 

불필요한 어댑터

일반적으로는 객체가 불변이면 재사용해도 안전하다. 그러나 어댑터(뷰)는 뒷단 객체 외에는 관리할 상태가 없어서, 한 종류의 객체당 어댑터 하나씩만 만들어지면 된다. (== 가변 객체 한 개로 뒷단 객체를 다루어도 된다.)

예를 들어, Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set뷰를 반환한다. 이 Set은 단순히 뷰 역할을 하므로, 매번 같은 객체를 반환하도록 동작한다. 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀌어야 하고 모두가 똑같은 Map을 대변해야 한다. 따라서 직관과는 다르게 keySet이 반환하는 Set은 가변 객체이더라도 재사용해도 안전하다.

 

클라이언트가 keySet을 두 번 호출해서 받은 setA, setB라는 어댑터(뷰)가 있다고 하자. 이때 setA에 remove 연산을 하면 setB에도 자동으로 적용된다. 둘은 같은 객체(이자 뷰이자 어댑터라고도 부르는 것)를 참조하고 있기 때문이다. 이 어댑터를 수정하면 어댑터에 연결된 Map 객체에도 영향을 미친다. Set뷰라는 가변 객체를 재사용해도 안전한 예시이다. (참고로 keySet은 add, addAll등의 메서드를 지원하지 않고, remove, removeAll, retainAll 등만 지원한다.)

 

오토 박싱

0부터 Integer.MAX_VALUE까지의 합을 출력하는 아래 프로그램은 long에서 Long으로의 언박싱 때문에 불필요한 Long 인스턴스가 만들어진다. 박싱 된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토 박싱을 주의해야 한다.

private static long sum() {
  Long sum = 0;
  for (long i = 0; i <= Integer.MAX_VALUE; i++)
    sum += i;

  return sum
}

자원을 직접 명시하지 않아야 하는 경우

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴이 적합하지 않다.

자원을 정적으로 명시해두는 것이 부자연스러울 때가 종종 있다. 예를 들어, 맞춤법을 검사하는 프로그램을 아래와 같이 만들었다고 생각해보자. 현실 세계에서는 언어가 바뀔 수도, 특수한 사전을 사용할 수도 있지만 이 코드는 단 하나의 사전만 자원으로 사용한다.

만약 final 한정자를 지우고 사전을 바꿀 수 있는 메소드를 추가한다면 여러 사전을 사용할 수 있겠지만 thread-safe하지 않다.

// 정적 유틸리티 사용
public class SpellChecker {
  private static final Lexicon dictionary = ...;

  private SpellChecker() {}
  ...
}
// 싱글턴 사용
public class SpellChecker {
  private final Lexicon dictionary = ...;

  private SpellChecker() {}
  public static SpellChecker INSTANCE = new SpellChecker(...);
  ...
}

 

자원 명시가 필요한 경우

클라이언트가 명시하는 자원을 클래스가 사용해야 한다면 의존성을 주입해야 한다. 이 경우 클래스의 유연성, 재사용성, 테스트 용이성이 크게 향상된다.

클래스(SpellChecker)가 클라이언트가 원하는 자원(dictionary)을 사용해야 한다면 아래 코드와 같이 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨줘야 한다.

의존 객체 주입 패턴을 사용하면, 불변을 보장하여 같은 자원을 사용하려는 여러 클라이언트가 의존 객체를 공유할 수 있고 생성자, 정적 팩토리, 빌더 모두에 똑같이 응용할 수 있다.

public class SpellChecker {
  private final Lexicon dictionary;

  public SpellChecker(Lexicon dictionary) {
    this.dictionary = Object.requireNonNull(dictionary);
  }
  ...
}

이 패턴을 변형해서 생성자에 특정 타입 인스턴스를 반복해서 넘겨주는 자원 팩터리를 넘길 수 있다. 한정적 와일드카드 타입을 사용해 팩토리 타입 매개변수를 제한하면 클라이언트는 자신이 명시한 타입의 하위 타입 객체를 생성할 수 있는 팩토리를 넘길 수 있다.

 

책 스터디를 하며 한분이 객체 자체를 넘기는 것이 아닌 Supplier를 넘기는 이유에 대해 어떤 분이 질문을 주셨다. 논의해본 결과, 인스턴스를 직접 넘겨준다면 생성자에서 인스턴스를 여러 개 필요로 하는 상황에 대처하기 어려울 수 있다는 결론이 나왔다. 스터디 최고....

Apartment create(Supplier<? extends house> houseFactory) { ... }

 

정적 멤버만 담은 유틸리티 클래스는 인스턴스화를 막기 위해 private 생성자를 만들어야 한다.

종종 정적 메서드와 정적 필드만 존재하는 클래스를 만들어야 할 때가 있다.

 

  • 기본 타입 값이나 배열 관련 메서드를 모을 때
    • java.util.Arrays, java.lang.Math
  • 특정 인터페이스를 구현하는 객체를 생성하는 정적 (팩터리) 메서드를 모을 때
    • java.util.Collections
  • final 클래스와 관련된 메서드를 모을 때
    • final 클래스는 상속이 불가능하므로 하위 클래스에 메서드를 생성하지 못하기 때문

정적 멤버만 담은 유틸리티 클래스에 생성자를 명시하지 않으면, 컴파일러가 자동으로 기본 생성자를 만든다. 즉, 매개변수 없는 public 생성자가 만들어져 의도치 않게 API를 공개하게 될 수 있다.

 

아래 예시처럼 명시적으로 private 생성자를 만들어서 객체가 생성될 가능성을 없애면 인스턴스화를 막을 뿐 아니라 하위 클래스가 생성되는 것도 막을 수 있다. 다만 원래 private 생성자의 목적대로 쓰이고 있지 않기 때문에 따로 주석을 달아두는 것이 좋다.

public class UtilityClass {
  // 인스턴스화 방지
  private UtilityClass() {
    throw new AssertionError();
  }
}

싱글턴(singleton)은 인스턴스를 오직 하나만 생성할 수 있는 클래스이다. 설계상 유일하게 존재해야 하는 시스템 컴포넌트는 싱글턴으로 구현되어야 하고 무상태 객체(함수 등)도 싱글턴으로 구현할 수 있다.

 

public static final 필드

Iu를 생성하는 private 생성자는 public static final 필드인 Iu.INSTANCE를 초기화할 때 한 번만 호출된다. public, protected 접근제한자인 다른 생성자가 없으므로 Iu 클래스를 초기화할 때 만들어진 인스턴스가 전체 시스템에서 단 하나뿐임이 보장된다.

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

  public void leaveTheBuilding() { ... }
}

이 방법은 API에 public static 필드가 final임이 명백하게 드러나 싱글턴이라는 것을 쉽게 확인할 수 있고 코드가 간결하다는 장점이 있다.

 

정적 팩토리 메서드

Iu.getInstance는 항상 같은 객체의 참조를 반환하므로 여전히 Iu는 유일하게 존재한다.

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

  public void leaveTheBuilding() { ... }
}

이 방법은 API를 변경하지 않고도 싱글턴이 아니도록 변경할 수 있다. 정적 팩터리 메서드가 내부적으로 다른 인스턴스를 반환하도록 변경할 수 있고 원한다면 제네릭 싱글턴 팩터리로도 만들 수 있다. 마지막으로 정적 팩터리 메서드 참조를 공급자로 사용할 수 있다. 이러한 장점이 필요없다면 간결한 public 필드 방식을 사용하는 것이 낫다.

위 두 방법 중 한 방법을 사용하려면, 싱글턴 클래스 직렬화에 주의해야 한다.

 

원소가 하나인 열거타입

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

이 방법은 간결하고 복잡한 직렬화 상황이나 리플렉션 공격에도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

public enum Iu {
  INSTANCE;

  public void leaveTheBuilding() { ... }
}
정적 팩토리 메서드와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다. 빌더 패턴이 나오기까지의 과정을 살펴보자.

생성자에 매개변수가 많을 때 사용할 수 있는 방법

1. 점층적 생성자 패턴

점층적 생성자 패턴을 사용할 때 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 의미를 파악하기 어렵다.

점층적 생성자 패턴은 필수 매개변수만 받는 생성자를 기반으로 선택 매개변수를 하나씩 늘린 생성자를 만드는 패턴이다.

Circles 클래스에서 필수 매개변수인 radius와 count를 받는 생성자를 기본으로 선택 매개변수인 colorsolidLineshadow를 하나씩 추가한 생성자를 만들었다. 이 클래스를 사용하기 위해서는 원하는 선택 매개변수를 모두 포함한 생성자 중 가장 짧은 생성자를 호출하게 되는데, 이 방법은 사용하지 않는 매개변수도 결국 채워진다는 점에서 비효율적이다. 자료형이 같은 값이 연속되면 각 값의 의미도 헷갈릴 것이다.

public class Circles {
  private final int radius;          // 반지름       (필수)
  private final int count;           // 원 개수      (필수)
  private final String color;        // 원 색깔      (선택)
  private final boolean solidLine;   // 실선 여부    (선택)
  private final boolean shadow;       // 그림자 여부 (선택)
  
  public Circles (int radius, int count) {
    this(radius, count, "");
  }

  public Circles (int radius, int count, Stirng color) {
    this(radius, count, color, 0);
  }

  public Circles (int radius, int count, String color, boolean solidLine) {
    this(radius, count, color, solidLine 0);
  }

  public Circles (int radius, int count, String color, boolean solidLine, boolean shadow){
    this.radius = radius;
    this.count = count;
    this.color = color;
    this.solidLine = solidLine;
    this.shadow = shadow;
  }
}

 

2.  자바빈즈 패턴

자바빈즈 패턴을 사용하면 객체 하나를 만들기 위해 여러 메소드를 호출해야 하고 객체가 완전히 생성되기 전에 일관성이 무너진다.

자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드를 호출해 원하는 매개변수 값을 설정하는 패턴이다. 반지름이 3이고 그림자가 있는 노란색 원을 7개 만들려면 아래 예시와 같이 생성하면 된다.

이 방법은 인스턴스를 만들기 쉽고 각 매개변수를 따로 설정하기 때문에 읽기에도 쉽다. 그러나 circles 객체를 하나 만들기 위해 세터 메서드를 5개나 호출해야 하고 모든 메서드 호출이 완료되기 전까지는 객체 일관성이 깨진다는 단점이 있다. 객체 일관성이 깨지면 스레드 안정성을 얻기 위해 수동으로 객체를 얼리고 녹여야 하는데 이 방법은 런타임 오류에 취약하므로 사용하기 어렵다.

Circles circles = new Circles();
circles.setRadius(3);
circles.setCount(7);
circles.setColor("yellow");
circles.setShadow(true);
public class Circles {
  // 기본값으로 초기화
  private int radius = 1;
  private int count = 1;
  private String color = "white";
  private boolean solidLine = true;
  private boolean shadow = false;
  
  public Circles() { }

  // 세터 메서드
  public void setRadius(int val)          { radius = val; }
  public void setCount(int val)           { count = val; }
  public void setColor(String val)        { color = val; }
  public void setSolidLine(boolean val)   { solidLine = val; }
  public void setShadow(boolean val)      { shadow = val; }
}

 

3. 빌더 패턴

빌더 패턴은 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 둘 다 가진다. 생성자나 정적 팩터리 메서드가 처리할 매개변수가 많다면 빌더 패턴을 사용하는 것이 좋다.

빌더 패턴에서 클라이언트는 필요한 객체를 직접 만들지 않고, 필수 매개변수를 사용해 생성자(혹은 정적 팩토리 메서드)를 호출해 빌더 객체를 얻는다. 빌더 객체는 세터 메서드로 선택 매개변수를 설정하고 build()를 호출해 객체를 만든다.

아래 예제를 보면 빌더의 세터 메서드는 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이 코드는 점층적 생성자 패턴보다 읽고 쓰기 쉽다.

잘못된 매개변수가 넘어오는 것을 발견하려면 빌더의 생성자와 메서드에서 매개변수 유효성을 검사하고, 여러 매개변수에 걸친 오류는 build()에서 불변식 여부를 검사해야 한다. 잘못된 점이 있다면 IllegalArgumentException을 던지면 된다.
Circles circle = new Circles.Builder(3, 7).color("yellow").shadow(true);
public class Circles {
  private final int radius;
  private final int count;
  private final String color;
  private final boolean solidLine;
  private final boolean shadow;

  public static class Builder {
    // 필수 매개변수
    private final int radius;
    private final int count;

    // 선택 매개변수 (기본값으로 초기화)
    private String color = "white";
    private boolean solidLine = true;
    private boolean shadow = false;

    public Builder(int radius, int count) {
      this.radius = radius;
      this.count = count;
    }
    public Builder(String val) { color = val; return this; }
    public Builder(boolean val) { solidLine = val; return this; }
    public Builder(boolean val) { shadow = val; return this; }

    public Circles build() {
      return new Circles(this);
    }
  }

  private Circles(Builder builder) {
    radius = builder.redius;
    count = builder.count;
    color = builder.color;
    solidLine = builder.solidLine;
    shadow  =builder.shadow;
  }
}

단, 빌더 객체를 만들려면 다른 방법에 비해 장황한 빌더부터 만들어야 한다. 따라서 매개변수가 4개 이상인 경우에 빌더 사용을 고려하는 것이 낫다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 빌더 클래스에는 재귀적 타입 한정을 이용하는 제네릭 타입을 반환 타입으로 사용할 수 있고, 여기에 추상 메서드인 self를 더해 하위 클래스 형변환 없이 메서드 연쇄를 지원할 수 있다.

정적 팩토리 메서드로 인스턴스 생성 시 장점

1. 이름 사용

: 메서드 명에 클라이언트에 반환할 객체 특성을 나타낼 수 있어서 이름이 없는 생성자에 비해 개발자들이 각 팩토리 메서드가 어떤 역할을 하는지 이해하기 쉽다.

 

2. 인스턴스 통제

: 호출될 때마다 동일한 객체가 반환되도록 해서 인스턴스를 통제할 수 있고 싱글톤 혹은 인스턴스화 불가하게 만들어서 성능을 끌어올릴 수 있다.

 

3. 하위 객체 반환

: 정적 팩토리 메서드를 사용하면 반환 타입의 하위 객체를 반환할 수 있다. 이 특징은 API를 더 유연하고 가볍게 만든다.

 

[예시] EnumSet

클라이언트는 팩터리가 전달하는 객체가 어떤 클래스의 인스턴스인지 알 필요 없다. 단지 EnumSet의 하위 클래스이기만 하면 된다.

EnumSet은 public 생성자 대신 정적 팩토리 메서드를 사용한다. 아래 noneOf 메서드를 보면 universe.length가 64보다 큰지에 따라 EnumSet의 하위 클래스인 RegularEnumSet 또는 JumboEnumSet을 반환할 수 있다.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
  Enum<?>[] universe = getUniverse(elementType);
  if (universe == null)
      throw new ClassCastException(elementType + " not an enum");

  if (universe.length <= 64)
      return new RegularEnumSet<>(elementType, universe);
  else
      return new JumboEnumSet<>(elementType, universe);
}

 

[예시] Collections

정적 팩토리 메서드를 응용해 구현 클래스를 숨기면 API를 논리, 물리적으로 가볍게 만들 수 있다. 인터페이스 기반 프레임워크의 핵심 기술이다.

java.util.Collections는 java.util.Collection 인터페이스의 구현체이며 동반 클래스(companion class)라고도 한다. unmodifiableSet 반환 타입은 인터페이스이고, 실제로 반환하는 것은 인터페이스 하위 객체인 UnmodifiableSet의 인스턴스이다. 여기서 클라이언트는 인스턴스가 어떻게 구현되었는지 알 필요가 없으므로 API를 사용하기 위해 인터페이스 정보만 알면 된다.

public class Collections {

  // 정적 팩토리 메서드
  public static <T> Set<T> unmodifiableSet(Set<? extends T> s) {
        return new UnmodifiableSet<>(s);
    }

  // 구현 클래스
  static class UnmodifiableSet<E> extends UnmodifiableCollection<E> implements Set<E>, Serializable{/* 중략 */}
}

단, Java8 이전에는 인터페이스에 static 메서드를 사용할 수 없었으므로 동반 클래스를 사용했다는 점에 유의한다.

 

[예시] Collections

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

서비스 제공자 프레임워크는 작성 시점에 반환할 객체의 클래스가 존재하지 않아도 된다는 정적 팩토리 메서드의 유연성을 기반으로 만들어진다.

예를 들어, 대표적인 서비스 제공자 프레임워크인 JDBC는 구현체를 클라이언트에 제공해 클라이언트를 구현체로부터 분리하는 역할을 한다. 서비스 제공자 프레임워크를 구성하는 서비스 접근 API 컴포넌트는 클라이언트가 서비스의 인스턴스를 얻을 때 사용된다. 클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있고, 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체를 돌아가며 반환할 수 있다.

 

정적 팩토리 메서드로 인스턴스 생성 시 단점

1. 하위 클래스 생성 불가

: 생성자 대신 정적 팩토리 메서드만 사용한다면 하위 클래스를 만들 수 없다. 상속하기 위해서는 상위 클래스에 public이나 protected 생성자가 필요하기 때문이다. (상속보다 컴포지션 사용을 권장한다는 점에서 장점일 수 있다.)

 

2. 객체를 생성하는 메서드인지 알기 어려움

: 생성자와 비교해보면 정적 팩토리 메서드는 API 설명에서 객체를 생성하는 역할을 하는지 알기 어렵다.

+ Recent posts