이펙티브 자바 3/E_객체생성과 파괴
by Gongdel
2장. 객체 생성과 파괴
1. 생성자 대신 정적 팩터리 메서드를 고려하라!
클래스의 인스턴스를 얻는 일반적인 방법은 public 생성자다. 하지만 꼭 알아야할 기법이 하나 더 있다.
클래스는 생성자와 별도로
정적 팩터리 메서드(static factory method)
를 제공할 수 있다.
정적 팩토리 메서드가 생성자보다 좋은 장점 다섯 가지
- 이름을 가질 수 있다.
- 생성자에 넘기는 매개변수와 생성자 자체만으로는 객체의 특성을 제대로 알지 못하지만, 정적 팩토리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사 가능하다.
어느 쪽이 '값이 소수인 BigInteger를 반환한다는' 의미를 더 잘 설명할까? 생성자: BigInteger(int, int, Random) 정적 팩터리 메서드: BigInteger.probablePrime
- 생성자에 넘기는 매개변수와 생성자 자체만으로는 객체의 특성을 제대로 알지 못하지만, 정적 팩토리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사 가능하다.
- 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
- 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아있게 할지를 철저히 통제가능
- 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
정적 팩토리 메서드의 단점 두 가지
- 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서는 오히려 장점으로 받아들일 수도 있다.
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
- 생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.
정적 팩터리 메서드의 흔한 명명 방식
+ 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 혹은 getIntance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
예) StackWalker luke = StackWalker.getInstance(options);
+ create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
예) Object newArray = Array.newinstance(classObject, arrayLen);
....
핵심정리
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.
2. 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩터리와 생성자에는 똑같은 제약이 하나 있다. 선택적 매개변수가 많을 때 적절히 대응하기 어렵다
는 점이다.
매개 변수가 많아지면 클래스용 생성자 혹은 정적 팩터리에서는 많은 프로그래머들이 ‘점층적 생성자 패턴’을 즐겨 사용한다.
하지만 점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
(코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 것이고, 매개변수가 몇 개인지도 주의해서 세어 보아야 할 것이다.)
또 다른 대안인 자바빈즈 패턴
은 어떨까?
- 장점:
- 점층적 생성타 패턴의 단점들을 보완
- 코드가 길어지긴 했지만 인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드 가능
- 점층적 생성타 패턴의 단점들을 보완
- 단점:
- 객체 하나를 만들려면 메서드를 여러 개 호출해야 한다.
- 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
- 점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것
- 일관성이 무너지는 문제 때문에
자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없다.
점층적 생성자 패턴과 자바빈즈 패턴의 장점만을 가졌다 - 빌더 패턴
빌더 패턴은 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비했다.
- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
- 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한 후, 매개 변수가 없는 build 메서드를 호출해 필요한(보통은 불변인) 객체를 얻는다.
- 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어 두는 게 보통이다.
핵심정리
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.
매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다.
빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.
빌더는 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.(생성 비용이 비교적 큰편)
하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명심하자. 그러니 애초에 빌더로 시작하는 편이 나을 때가 많다
3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 그런데 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
(타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문)
싱글톤을 만드는 방식은 보통 둘 중 하나다. 두 방식 모두 생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다.
우선 public static 멤버가 final 필드인 방식을 살펴보자.
코드 3-1 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 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
코드 3-1의 public 필드 방식의 큰 장점
- 해당 클래스가 싱글턴임이 API에 명백히 드러난다는 것이다.
- public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다.
- 간결함
싱글턴을 만드는 두 번째 방법에서는 정적 팩터리 메서드를 public static 멤버로 제공한다.
코드 3-2 정적 팩터리 방식의 싱글턴
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() {....}
}
코드 3-2의 정적 팩터리 방식의 큰 장점
- (마음이 바뀌면) API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
- 유일한 인스턴스를 반환하던 팩터리 메서드가 (예컨대) 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.
- 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
- Elvis::getInstance를 Supplier
로 사용하는 식
- Elvis::getInstance를 Supplier
이러한 장점들이 굳이 필요하지 않다면 public 필드 방식이 좋다.
코드 3-3 열거 타입 방식의 싱글턴 - 바람직한 방법
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {...}
}
public 필드 방식과 비슷하지만, 더 간결하고 추가 노력없이 직렬화할 수 있다.
심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
조금 부자연스러워 보일 수 있으나 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
(단 , 만드려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.))
4. 인스턴스화를 막으려거든 private 생성자를 사용하라
이따금 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있을 것이다.
객체 지향적으로 사고하지 않는 이들이 종종 남용하는 방식이기 때문에 그리 선호하지는 않지만, 분명 나름의 쓰임새가 있다.
(예를 들면, java.lang.Math, java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓거나, java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수도 있다.)
정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어 준다.
즉, 매개변수를 받지 않는 public 생성자가 만들어지며, 사용자는 이 생성자가 자동 생성된 것인지 구분할 수 없다.
실제로 공개된 API들에서도 이처럼 의도치 않게 인스턴스화할 수 있게 된 클래스가 종종 목격되곤 한다.
추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
하위 클래스를 만들어 인스턴스화하면 그만이다. 이를 본 사용자는 상속해서 쓰라는 뜻으로 오해할 수 있으니 더 큰 문제다.
다행히도 인스턴스화를 막는 방법은 아주 간단하다.
컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때 뿐이니
private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
코드4-1 인스턴스를 만들 수 없는 유틸리티 클래스
public class UtilityClass {
// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
private UtilityClass() {
throw new AssertionError();
}
... // 나머지 코드는 생략
}
명시적 생성자가 private이니 클래스 바깥에서는 접근할 수 없다. 꼭 AssertionError를 던질 필요는 없지만, 클래스 안에서 실수라도 생성자를 호출하지 않도록 해준다.
그런데 생성자가 분명 존재하는데 호출할 수는 없다니, 그다지 직관적이지 않다. 그러니 앞의 코드처럼 적절한 주석을 달아두도록 하자.
이 방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를 private으로 선언했으니 하위 클래스가 상위클래스의 생성자에 접근할 길이 막혀버린다.
5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
많은 클래스가 하나 이상의 자원에 의존한다. 가령 맞춤법 검사기는 사전(dictionary)에 의존하는데, 이런 클래스를 정적 유틸리티 클래스로 구현한 모습을 드물지 않게 볼 수 있다.
코드 5-1 정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 객체 생성 방지
private static boolean isValid(String word) {...}
public static List<String> suggestions(String typo) {...}
}
비슷하게, 싱글턴으로 구현하는 경우도 흔하다.
코드 5-2 싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.
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 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가할 수 있지만, 아쉽게도 이 방식은 어색하고 오류를 내기 쉬우며 멀티스레드 환경에서는 쓸 수 없다.
사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
대신 클래스(SpellChcker)가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원(dictionary)을 사용해야 한다. 이 조건을 만족하는 간단한 패턴이 있으니,
바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.
이는 의존 객체 주입의 한 형태로, 맞춤법 검사기를 생성할 때 의존 객체인 사전을 주입해주면 된다.
코드 5-3 의존 객체 주입은 유연성과 테스트 용이성을 높여준다.
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) {...}
}
의존 객체 주입은 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.
이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다.
팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다. 즉 팩터리 메서드 패턴을 구현한 것이다.
의존 객체 주입이 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 수천 개나 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다.(스프링과 같은 의존 객체 주입 프레임워크를 사용함으로써 해소가능)
핵심 정리
클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
이 자원들을 클래스가 직접 만들게 해서도 안 된다. 대신 필요한 자원을(혹은 그 자원을 만들어주는 팩터리를) 생성자에 (혹은 정적 팩터리나 빌더에) 넘겨주자.
의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해 준다.
6. 불필요한 객체 생성을 피하라
똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다. 특히 불변 객체는 언제든 재사용할 수 있다.
다음 코드는 하지 말아야 할 극단적인 예이니 유심히 한번 살펴보자.
String s = new String("bikini"); // 따라 하지 말 것 !!
이 문장은 실행될 때마다 String 인스턴스를 새로 만든다. 완전히 쓸데없는 행위다. 생성자에 넘겨진 “bikini” 자체가 이 생성자로 만들어내려는 String과 기능적으로 완전히 똑같다.
이 문장이 반복문이나 빈번히 호출되는 메서드 안에 있다면 쓸데없는 String 인스턴스가 수백만 개 만들어질 수도 있다. 개선된 버전을 보자.
String s = "bikini";
이 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다. 나아가 이 방식을 사용한다면 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.
생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.
예컨대 Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다. 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.
생성 비용이 아주 비싼 객체도 더러 있다. 이런 ‘비싼 객체’가 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
예를들어 주어진 문자열이 유효한 로마 숫자인지를 확인하는 메서드를 작성한다고 해보자.
다음은 정규 표현식을 활용한 가장 쉬운 해법이다.
코드 6-1 성능을 훨씬 더 끌어올릴 수 있다!
static boolean isRomanNumeral(String s) {
return s.matches(.....);
}
이 방식의 문제는 String.matches 메서드를 사용한다는 데 있다.
String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황해서 반복해 사용하기엔 적합하지 않다.
Why?
이 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다.
따라서 성능을 개선하려면 필요한 정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고,
나중에 isRomanNumeral 메서드가 호출될 떄마다 이 인스턴스를 재사용한다.
코드 6-2 값비싼 객체를 재사용해 성능을 개선한다.
public class Romannumerals {
private static final Pattern ROMAN = Pattern.compile(.....);
static boolean isRomanNumeral(String s) {
return ROMAN.macher(s).maches();
}
}
이렇게 개선하면 isRomanNumeral이 빈번히 호출되는 상황에서 성능을 상당히 끌어올릴 수 있다.
그밖에 불필요한 객체를 만들어내는 예
- 오토박싱
- 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return i;
}
문자 하나로 인해 제대로 구현했을 때보다 훨씬 느린 경우다.
sum 변수를 long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 약 231개나 만들어진 것이다….
교훈은 명확하다. 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토방식이 숨어들지 않도록 주의하자.
7. 다 쓴 객체 참조를 해제하라
자바에서 메모리는 가바지 컬렉터에 의해 관리된다. 그래서 자칫 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 절대 사실이 아니다.
스택을 간단히 구현한 다음 코드를 보자.
코드 7-1 메모리 누수가 일어나는 위치는 어디인가?
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 EmptyStackException();
}
return elements[--size];
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
꼭꼭 숨어있는 문제가 있다. 이는 바로 메모리 누수이다. 그렇다면 앞 코드에서 메모리 누수는 어디서 일어날까?
이 코드에서는 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
이 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다. 여기서 다 쓴 참조란 문자 그대로 앞으로 다시 쓰지 않을 참조를 뜻한다. 앞의 코드에서는 elements 배열의 ‘활성 영역’ 밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.
가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체..)를 회수해 가지 못한다.
그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.
해법은 간단하다. 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다.
코드 7-2 제대로 구현한 pop 메서드
public Object pop() {
...
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
하지만 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 밀어내는 것이다.(변수 범위를 최소가 되게 정의하자)
메모리 누수를 주의하자.
- 일반적으로 자기 메모리를 직접 관리하는 클래스라면 항시 메모리 누수에 주의해야 한다.
- 캐시 역시 메모리 누수를 일으키는 주범이다. (객체 참조를 캐시에 넣고 사용하지 않는 경우)
- 해법
- 캐시 외부에서 키(key)를 참조하는 동안만(값이 아니다) 엔트리가 살아있는 캐시가 필요한 상황
- WeakHashMap을 사용해 캐시를 만들자. 다 쓴 엔트리는 그 즉시 자동으로 제거된다.
- 캐시 외부에서 키(key)를 참조하는 동안만(값이 아니다) 엔트리가 살아있는 캐시가 필요한 상황
- 해법
- 리스너(Listener) 또는 콜백(Callback)
핵심 정리
메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러같은 디버깅 도구
를 동원해야한 발견되기도 한다.
그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.
8. finalizer와 cleaner 사용을 피하라
자바는 두 가지 객체 소멸자를 제공한다. 그중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.(따라서 자바9에서 deprecated)
cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
finalizer와 cleaner는 실행되기까지 얼마나 걸릴지 예측할 수 없기 때문에, 제때 실행되어야 하는 작업은 절대 할 수 없다.
또한 자바 언어 명세는 finalizer나 cleaner의 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다.
따라서 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안된다.
(예를들면, 데이터베이스 같은 공유 자원의 영구 락 해제를 finalizer나 cleaner에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다.)
9. try-finally보다는 try-with-resource를 사용하라
자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많은데, 클라이언트가 이를 놓칠 경우 예측할 수 없는 성능 문제로 이어지기도 한다.
전통적으론 try-finally를 사용했지만, close해야할 자원이 둘 이상이면 이는 너무 지저분한 코드가 된다.
try-with-resource의 이점
- 짧고 읽기 수월하다. 즉 가독성이 좋다.
- 문제를 진단하기가 try-finally보다 수월하다.
- try-catch 문을 중첩하지 않고, catch를 사용할 수 있다.
핵심 정리
꼭 회수해야 하는 자원을 다룰 떄는 try-finally 말고 try-with-resources를 사용하자.
예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다.
try-finally로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있다.
Subscribe via RSS