본문 바로가기

개발/이펙티브 자바

Effective Java (이펙티브 자바)2장 - 객체 생성과 파괴

이 장은 객체의 생성과 파괴를 다룬다. 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법, 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법, 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령을 알아본다.

 

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


  클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 모든 프로그래머가 꼭 알아둬야 할 기법이 하나 더 있다. 클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다. 이 방식에는 장점과 단점이 모두 존재한다.

  먼저 장점 다섯 가지를 알아보자.

  • 첫 번째, 이름을 가질 수 있다. 
    • 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 
    • 하나의 시그니처로는 생성자를 하나만 만들 수 있다. 하지만, 정적 팩터리 메서드에는 이런 제약이 없다. 
    • 각각의 차이를 잘 드러내는 이름을 지어주자.
  • 두 번째, 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다. 
    • 불변 클래스(아이템17)는 인스턴스를 미리 만들어 놓거단 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 따라서 ( 특히 생성 비용이 큰 ) 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 준다.
    • 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다. 
    • 이런 클래스를 인스턴스 통제 클래스라 한다. 그렇다면 왜 통제를 하는 걸까? 
      • 인스턴스 통제를 통하여 클래스를 싱글턴으로 만들 수도, 인스턴스화 불가로 만들 수도 있다. 또한 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다( a == b일때만 a.equals(b)가 성립 ). 
  • 세 번째, 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. 
    • 이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다.
  • 네 번째, 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
    • 심지어 다름 릴리즈에서는 또 다른 클래스의 객체를 반환해도 된다. 
  • 다섯 번째, 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • 이런 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다. JDBC가 있다. 
    • JDBC의 DriveManager.getConnection이 서비스 접근 API, 즉 정적 팩터리 메서드와 같은 역할을 해준다.

브리지 패턴 등은 추가적인 공부가 필요할 것 같다.

 

  그 다음으로 단점을 알아볼 차례다.

  • 첫 번째, 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다. 
  • 두 번째, 정적 팩터리 메서드는 프로그래머가 찾기 어렵다. 이 일을 언젠가 자바독이 알아서 처리해 줬으면 좋겠으나, 그날이 오기까지는 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.

  다음은 정적 팩터리 메서드에 흔히 사용하는 명명 방식들이다.

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
    • Data 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);
  • create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
    • Object newArray = Array.newInstance(classObject, arrayLen);
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입이다.
    • FileStore fs = Files.getFileStore(path)
  • newType : newInstance와 같고, 위의 getType과 같은 경우.
  • type : getType, newType의 간결한 버전
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

 

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


  정적 팩터리와 생성자에는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 제약을 똑같이 갖고 있다. 

이러한 경우에는 자바빈즈 패턴을 통해 비어있는 객체를 만들고 setter 메서드를 통해서 설정할 수도 있다.

  하지만, 자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 생성자에서는 매개변수들이 유효한지를 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것이다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야만 한다. ( freeze 메서드 등의 방법이 있긴하다 )

 

다른 대안은 빌더 패턴이다. 클라이언트는 필요한 객체를 직접만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그런 다음 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다. 마지막으로 build 메서드를 호출해 드디어 우리에게 필요한 ( 보통은 불변인 ) 객체를 얻는다. 

 

잘못된 매개변수를 최대한 일찍 발견하려면 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하자. 

 

책에서의 Pizza 예제는 계층적인 빌더 패턴을 잘 보여주고 있다. 

 

빌더 패턴에 장점만 있는 것은 아니다. 객체를 만들려면, 그에 앞서 빌더부터 만들어야 한다. 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명심하자. 

 

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 

 

 

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


싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다. 

그런데 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다. 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다. 

 

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

  우선 public static 멤버가 final 필드인 방식을 살펴보자.

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

private 생성자는 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출된다. public 이나 protected 생성자가 없으므로 인스턴스가 하나뿐임이 보장된다.

 

단 하나의 예외는, 리플렉션 API인 AccessibleObject.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() { ... }
}

항상 같은 객체의 참조를 반환한다. ( 역시 리플렉션을 통한 예외는 똑같다 )

 

위의 방식의 큰 장점은 해당 클래스가 싱글턴임이 API에 명백히 드러난다는 것이다. 그리고 간결하다. 

 

아래 방식의 첫 번째 장점은 마음이 바뀌면 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점이다. 

두 번째 장점은 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다는 점이다 ( 아이템 30 ).

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

 

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

 

둘 중 하나의 방법으로 만든 싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다. readResolve 메서드를 제공해야 한다 ( 아이템 89 ). 그렇지 않으면 역직렬화할 때 마다 새로운 인스턴스가 만들어진다. 

 

private Object readResolve() {
   // '진짜' Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
   return INSTANCE;
}

 

싱글턴을 만드는 세 번째 방법은 원소가 하나인 열거 타입을 선언하는 것이다.

 

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

조금 부자연스러워 보일 수는 있으나 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

 

 

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


이따금 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있을 것이다. 그리 곱게 보이지는 않지만, 분명 나름의 쓰임새가 있다. 이런 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니지만, 생성자를 명시하지 않으면 기본 생성자가 생긴다. 

  추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 하위 클래스를 만들어 인스턴스화하면 그만이다. 이를 본 사용자는 상속해서 쓰라는 뜻으로 오해할 수 있으니 더 큰 문제다. 

 

private 생성자를 추가해서 인스턴스화를 막자. 

 

이 방식은 상속을 불가능하게 하는 효과도 있다. 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막혀버린다.

 

 

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


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

 

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

 

  SpellChecker의 dictionary 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가할 수 있지만, 아쉽게도 이 방식은 어색하고 오류를 내기 쉬우며 멀티스레드 환경에서는 쓸 수 없다. 

 

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

 

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

의존 객체 주입 패턴은 아주 단순하다. 

 

이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 

 

의존 객체 주입은 의존성이 수천개나 되는 프로젝트에서는 코드를 어지럽게 만들기도 한다. Spring 같은 의존 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있다.

 

클래스가 내부적으로 하나 이상의 자원에 의존한다면, 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 대신 필요한 자원 혹은 팩터리를 생성자 ( 혹은 정적 팩터리나 빌더에 ) 넘겨주자. 

 

 

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


똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체는 언제든 재사용할 수 있다.

String s = new String("bikini"); // 좋지 못한 코드다.

String s = "bikini";

 

하나의 String 인스턴스를 사용한다.

 

불변객체가 아니더라도, 가볍 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

  생성 비용이 아주 비싼 객체도 더러 있다. 이런 '비싼 객체'가 반복해서 필요하다면 캐싱하여 재사용하길 권한다. 안타깝게도 자신이 만드는 객체가 비싼 객체인지를 매번 명확히 알 수는 없다. 다음은 정규표현식을 활용하여 주어진 문자열이 유효한 로마 숫자인지를 확인하는 메서드다.

static boolean isRomanNumeral(String s) {
   return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
           + "(X[CL]|L?X{0,3})(I{XV}|V?I{0,3})$");
}

String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다. 

이 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. 입력받은 정규식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다. 

 

성능을 개선하려면 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 생성해 캐싱해두고, 나중에 재사용한다.

static boolean isRomanNumeral(String s) {
   return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
           + "(X[CL]|L?X{0,3})(I{XV}|V?I{0,3})$");
}

public class RomanNumerals {
   private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})"
           + "(X[CL]|L?X{0,3})(I{XV}|V?I{0,3})$");
           
   static boolean isRomanNumeral(String s) {
      return ROMAN.matcher(s).matches();
   }
}

 

불필요한 객체를 만들어내는 또 다른 예로 오토박싱을 들 수 있다. 다음 메서드를 보자. 모든 양의 정수의 총합을 구하는 메서드로, int는 충분히 크지 않으니 long을 사용해 계산하고 있다.

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

끔찍이 느린 코드다. sum 변수를 long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 231개나 만들어진 것이다. 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토 박싱이 숨어들지 않도록 주의하자.

 

이번 아이템을 "객체 생성은 비싸니 피해야 한다"로 오해하면 안 된다. 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.  JVM이 요새는 잘 해준다.

거꾸로, 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 여러분만의 객체 풀(pool)을 만들지는 말자. 데이터베이스 연결 같은 경우 생성 비용이 워낙 비싸니 재사용하는편이 낫다. 

 

 

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


자바는 다 쓴 객체를 알아서 회수해간다. 그래서 자칫 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 절대 사실이 아니다. 

 

책에서 구현되어있는 코드 7-1 스택은 메모리 누수가 발생할 것이다. 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다. 이 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

 

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

 

해법은 간단하다. 해당 참조를 다 썼을 때 null 처리 (참조 해제)하면 된다.

public Object pop() {
   if (size == 0)
      throw new EmptystackException();
   Object result = elements[--size];
   elements[size] = null;
   return result;
}

모든 객체를 다 쓰자마자 일일이 null 처리하는 것은 좋은가? 그렇지 않다. 프로그램을 필요 이상으로 지저분하게 만들 뿐이다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 

다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다. 변수의 범위를 최소가 되게 정의했다면(아이템 57) 이 일은 자연스럽게 이뤄진다. 

 

Stack 클래스가 메모리 누수에 취약한 이유는, 자기 메모리를 직접 관리하기 때문이다. 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.

 

캐시 역시 메모리 누수를 일으키는 주범이다. 

 

 

 

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


자바는 두 가지 객체 소멸자를 제공한다. 그중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다. 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다. 나름의 쓰임새가 몇 가지 있긴 하지만, 기본적으로 '쓰지 말아야' 한다. 그래서 자바 9에서는 finalizer를 deprecated로 지정하고 cleaner를 그 대안으로 소개했다. cleaner는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다. 

 

C++의 파괴자와는 다른 개념이다. finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 객체에 접근할 수 없게 된 후 실행되기까지 얼마나 걸릴지 알 수 없다. 즉, 제때 실행되어야 하는 작업은 절대 할 수 없다. 예를 들면 파일 닫기를 finalizer나 cleaner에 맡기면 중대한 오류를 일으킬 수 있다. 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문이다. 새로운 파일을 열지 못해 프로그램이 실패할 수 있다. 

  얼마나 신속하게 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸고, 구현마다 천차만별이다. finalizer 스레드는 다른 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못한 것이다.

 

  또한 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 따라서 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다. 예를 들어 데이터베이스 같은 공유 자원의 영구 락 해제를 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다. 

 

  finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다. 

  

  둘은 심각한 성능 문제도 동반한다. AutoCloseable 객체를 생성하고 GC가 수거하기까지는 12ns가 걸린반면, 둘은 550ns나 걸렸다. 

finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다. 생성자나 직렬화 과정에서 예외가 발생하면, 이 생성 되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다. ( 이 부분은 어려운 내용이다 )...

 

이에 대한 묘안은 AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.

구체적인 구현법과 관련하여 알아두면 좋을 게 하나 있다. 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다. 다시 말해, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지는 것이다.

 

이쯤이면 cleaner / finalizer는 대체 어디에 쓰는 물건인지 궁금해진다. 적절한 쓰임새가 아마도 두 가지 있다고 말한다. 하나는 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할이다. 

  두 번째 예는 네이티브 피어와 연결된 객체에서다. .... 이 부분은 잘 모르겠다.

 

public class Room implements AutoCloseable {
   private static final Cleaner cleaner = Cleaner.create();
   
   // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
   private static class State implements Runnable {
      int numJunkPiles;
      
      State(int numJunkPiles) {
         this.numJunkPiles = numJunkPiles;
      }
      
      // close 메서드나 cleaner가 호출된다.
      @Override public void run() {
         System.out.println("방 청소");
         numJunkkPiles = 0;
      }
   }
   
   // 방의 상태. cleanable과 공유하다.
   private final State state;
   
   // cleanable 객체. 수거 대상이 되면 방을 청소한다.
   private final Cleaner.Cleanable cleanable;
   
   public Room(int numJunkPiles) {
      state = new State(numJunkPiles);
      cleanable = cleaner.register(this, state);
   }
   
   @Override public void close() {
      cleanable.clean();
   }
}

State 인스턴스는 절대로 Room 인스턴스를 참조해서는 안 된다. 순환참조가 생겨 가비지 컬렉터가 Room 인스턴스를 회수해갈 기회가 오지 않는다. 그래서 State가 정적 중첩 클래스인 이유가 여기에 있다. 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되기 때문이다. 이와 비슷하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.

 

cleaner(자바 8까지는 finalizer)는 안전망 역할이나 중요하지 않는 네이티브 자원 회수용으로만 사용하자. 

 

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


자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. InputStream, OutputStream, java.sql.Connection 등이 좋은 예다. 이런 자원 중 상당수가 안전망으로 finalizer를 활용하고는 있지만 그리 믿을만하지 못하다 (아이템 8).

 

  전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다. 예외가 발생하거나 메서드에서 반환되는 경우를 포함해서 말이다.

 

try-finally의 문제들은 try-with-resources 덕에 모두 해결되었다. 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다. 단순히 void를 반환하는 close 메서드 하나만 덩그러니 정의한 인터페이스다. 

 

보통의 try-finally에서처럼 try-with-resources에서도 catch 절을 쓸 수 있다. 

꼭 회수해야 하는 자원을 다룰 때는 try-finall 말고, try-with-resources를 사용하자. 예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다. try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있다.