아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라
정적 팩터리 메서드의 장점
1. 이름을 가질 수 있다.
2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
3. 반환 타입의 하위 타입 객체를 반환할 수 있다.
4. 입력 매개변수에 따라 매번 다른 객체를 반환할 수 있다.
5. 정적 팩터리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 된다.
정적 팩터리 메서드의 단점
1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
2. 저정 팩터리 메서드는 프로그래머가 찾기 어렵다.
아이넴 2. 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩터리와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.
· 이러한 문제의 대안으로 점층적 생성자 패턴(telescoping constructor pattern)과 자바빈즈 패턴이 등장했지만, 한계가 존재한다.
· 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴으로 이러한 문제를 해결할 수 있다.
- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자나 정적 팩터리를 호출해 빌더 객체를 얻고, 빌더 객체가 제공하는 세터 메서드들로 원하는 선택 매개변수를 설정하고, 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.
아이템 3. private 생성자나 열거 타입으로 싱글턴을 보증하라
· 싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
ex) 함수와 같은 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트
싱글턴의 문제점
· 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워 질 수 있다.
- 타입을 인터페이스로 정의한 후 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문이다.
1. public static 멤버가 final 필드인 방식
· 장점: 해당 클래스가 싱글턴임이 API에 명백히 드러난다. public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다.
· 문제점:
1. 권한이 있는 클라이언트에서 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출 할 수 있다.
2. 생성되는 시점을 조절할 수 없다. (클래스가 다른 자원(DB커넥션 등)에 의존해야 한다면 이용 불가능)
- 방어 방법: 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던진다.
2. 정적 팩터리 메서드를 public static 멤버로 제공하는 방식
· 장점:
1. API를 바꾸지 않고도 싱글턴이 아니게 변꼉할 수 있다.
ex) 유일한 인스턴스를 반환하던 팩터리 메서드가 호출하는 스레드별로 다른 인스턴스를 넘겨줄 수 있다.
2. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. (아이템30)
3.정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
ex) Elvis::getInstance를 Supplier<Elvis>로 사용할 수 있다. (아이템 43, 44)
· 문제점: 리플렉션을 통한 예외는 똑같이 적용된다.
· 두 방식 모두 생성자는 private로 감추고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련한다.
· 둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면, Serializable을 구현한다고 선언하는 것만으로는 부족 하다.
- 모든 인스턴스 필드를 transient로 선언하고, readResolve 메서드를 제공해야 한다. (아이템 89)
- 이렇게 하지 않으면, 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어 진다.
3. 원소가 하나인 열거 타입을 선언한다.
· 장점:
1. 간결하다.
2. 추가 노력 없이 직렬화할 수 있다.
3. 아주 복잡한 직렬화 상황이나 리플렉션 공격에도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
· 대부분의 상황에서 싱글턴을 만드는 가장 좋은 방법이다.
· 문제점: 싱글턴이 Enum 외의 클래스를 상속해야 한다면, 이 방법은 사용할 수 없다.
- 열거 타입이 다른 인터페이스를 구현하도록 선언할 수 없다.
아이템 4. 인스턴스를 막으려거든 private 생성자를 사용하라
· 정적 메서드와 정적 필드만 담은 클래스는 객체 지향적으로 사고하지 않는 사람들이 종종 남용하는 방식이지만, 나름의 쓰임새가 있다.
1. java.lang.Math, java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.
2. java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수 있다.
(자바 8 부터는 이런 메서드를 인터페이스에 넣을 수 있다)
3. final 클래스와 관련된 메서드를 모아 놓을 때 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가기 때문이다.
· 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다. 따라서 private 생성자를 추가해 클래스의 인스턴스화를 막아서 사용할 수 있다. (생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만든다)
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
· 많은 클래스가 하나 이상의 자원에 의존한다. 사용하는 자원에 따라 동작이 달라지는 클래스에 경우 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.
· 의존 객체 주입 패턴의 장점:
1. 자원이 몇 개든 의존 관계가 어떻근 상관없이 잘 동작한다.
2. 불변(아이템17)을 보장하여 여러 클라이언트가 의존 객체를 안심하고 공유할 수 있도록 한다.
· 의존 객체 주입은 생성자, 정적 팩터리 모두에 똑같이 응용할 수 있다.
· 이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 즉, 팩터리 메서드 패턴(Gamma95)을 구현하는 것이다.
- 팩터리: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
아이템 6. 불필요한 객체 생성을 피하라
· 똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다.
· 생성자 대신 정적 팩터리 매서드를 제공하는 불변 클래스에서는 불필요한 객체 생성을 피할 수 있다.
ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
· 가변 객체라 해도 수용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.
· 생성 비용이 아주 비싼 객체가 반복해서 필요하다면, 캐싱하여 재사용하길 권한다.
아이템 7. 다 쓴 객체 참조를 해제하라
아래 스택 클래스는 메모리 누수가 발생한다.
- 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.
- 드물긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료된다.
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);
}
}
}
· pop 메서드에서 메모리 누수가 발생한다.
- 스택이 커졌다가 줄어들 때, 스택에서 꺼내진 객체들은 프로그램에서 더 이상 사용하지 않더라도 가비지 컬렉터가 회수하지 않는다.
- 꺼내진 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.
- elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.
- 다쓴 참조: 앞으로 다시 쓰지 않을 참조
· 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다.
- 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 잠재적으로 성능에 악영향을 줄 수 있다.
해결 방법
· 참조를 다 썼을 때 null(참조 해제)한다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
· 다쓴 참조를 null 하면 프로그램 오류를 조기에 발견할 수도 있다.
- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.
- null 처리하지 않았다면, 아무 내색 없이 무언가 잘못된 일을 수행할 수 있다.
· 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.
- 변수의 범위를 최소로 되게 정의했다면(아이템 57) 이 일은 자연스럽게 이뤄진다.
- null 처리를 해야하는 경우는 앞선 코드의 스택 처럼 자기 메모리를 직접 관리하는 클래스이다. 비활성 영역에서 참조하는 객체가 더 이상 쓸모없다는 것을 프로그래머만 알지 가비지 컬렉터는 알지 못하기 때문이다. 이럴 때는 null 처리를 하여 가비지 컬렉터에 직접 알려야한다.
· 캐시 역시 메모리 누수를 일으키는 주범이다.
- 객체 참조를 다 쓴 뒤 함참을 그냥 놔두는 일을 자주 접할 수 있다.
- 해결 방법1: 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요하다면, WeakHashMap을 사용해 캐시를 만든다.
다 쓴 엔트리는 즉시 자동으로 제거된다.
- 해결 방법2: 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식이 흔히 사용된다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해야한다. ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행할 수 있다.
- LinkedHahshMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리한다.
- 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야 한다.
아이템 8. finalizer와 cleaner 사용을 피하라
· 자바는 finalizer, cleaner 두 가지 객체 소멸자를 제공한다.
· finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.
· 자바 9에서 finalizer가 deprecated되고, cleaner를 그 대안으로 소개했지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
아이템 9. try-finally보다는 try-with-resources를 사용하라
자바 라이브러이에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.
ex) InputStream, OutputStream, java.sql.Connection
· 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.
- 이런 자원 중 상당수는 안전망으로 finalizer를 활용하지만, finalizer는 믿음직하지 못하다.
· 전통적으로 자원을 닫는 수단으로 try-finally가 쓰였지만, 자원이 둘 이상이면 코드가 너무 지저분해진다.
// 자원 하나 회수
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
// 자원 복수개 회수
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
- 심지어 앞선 코드 또한 미묘한 결점이 있다.
· 자바7에서 등장한 try-with-resources은 try-finally의 결점을 해결한다.
- 이 구조를 사용하려면 하당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
- 단순히 void를 반환하는 close 메서드 하나만 정의한 인터페이스다.
· 다음은 앞선 코드를 try-with-resources를 사용해 재작성한 코드다.
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}
}
· 앞선 코드에서 확인할 수 있듯이 try-with-resources 버전이 짧고 읽기 수월하며, 문제를 진단하기도 훨씬 좋다.
- firstLineOfFile 메서드를 살펴보자. readLine과 (코드에는 나타나지 않는)close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다.
- 이렇게 숨겨진 예외들도 스택 추적 내역에 '숨겨졌다(suppressed)'는 꼬리표를 달고 출력된다.
- 자바 7에서 Throwable에 추가된 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있다.
· try-with-resources도 catch 절을 쓸 수 있다.
- 이를 통해 try 문을 더 중첩하지 않고 다수의 예외를 처리할 수 있다.
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}