아이템 8 finalizer와 cleaner 사용을 피하라
Java 2가지 객체 소멸자: finalizer, cleaner
-> 예측 불가능, 성능 저하, 동시성 문제 유발
- finalizer: Java 9 deprecated API로 지정, 위험성 ↑
- cleaner: `finalizer`보다 덜 위험 But, 여전히 느리고, 불필요, 예측 X
!= C++의 파괴자(destructor)
(생성자 대척점) 객체 소멸될 때 그와 연결된 자원(메모리 + 비메모리) 자동 회수
vs Java: GC) 접근할 수 없게 된 객체 회수(메모리만 관리)
-> 비메모리 자원은 `try-with-resources`와 `try-finally` 사용해 해결
finalizer와 cleaner 피해야 하는 이유
1) 실행 시점 보장 X
- GC가 객체 회수한 후 언제 실행될지 모름 (수행 시점, 수행 여부 보장 X)
- 즉시 실행 필요한 작업(ex. 파일 닫기, DB 연결 종료)
or 프로그램의 생애주기와 상관 X 상태를 영구적으로 수정하는 작업에서 절대 사용 X- ex. 파일 열고 닫지 않으면 시스템 자원 고갈
- 시스템) finalizer, cleaner 실행 늦게 해서 파일 계속 열어둠 => 새로운 파일 열지 X (프로그램 실패)
- ex. 데이터베이스 같은 공유 자원의 영구 락(lock) 해제 -> 분산 시스템 전체 서서히 멈출 것
- ex. 파일 열고 닫지 않으면 시스템 자원 고갈
- GC 알고리즘, JVM 구현 -> 실행 시점 달라짐
- 특정 JVM에서 동작해도 고객 시스템에서 전혀 실행 X or 심각한 지연 발생 O
`System.gc` or `System.runFinalization` 메서드: 실행될 가능성 ↑ But, 보장 X
-> 보장 메서드: `System.runFinalizaersOnExit`, `Runtime.runFinalizersOnExit` But, 심각한 결함 O
2) 예외 처리 문제
- finalizer: 동작 중 예외 발생 -> 무시, 경고 출력 X
- (일반) 잡지 못한 예외: 스레드 중단, 스택 추적 내역 출력
- 스레드) 객체 완전 X 상태로 남은 객체 사용 -> 예측 X 동작
- cleaner: 별도 스레드에서 동작 -> 이런 문제 덜하지만 여전히 불완전
3) 심각한 성능 저하
- finalizer 사용한 객체의 생성/소멸 비용 최대 50배 느려질 수 있음
- cleaner도 클래스의 모든 인스턴스를 수거하는 방식으로 사용하면 성능 비슷
4) 보안 문제: finalizer 공격
- 공격 원리
- 생성자 or 직렬화 도중 예외가 발생 -> 객체 완전히 생성되기 전 GC 대상 될 수 있음
-> 악의적인 하위 클래스의 `finalizer()` 호출될 수 있음
-> 아직 완전히 초기화 X 일그러진 객체가 공격자에게 노출- 객체 정적 필드에 자신을 저장해 생존
- 허용 X 작업 수행 (ex. 시스템 자원 접근)
- 생성자 or 직렬화 도중 예외가 발생 -> 객체 완전히 생성되기 전 GC 대상 될 수 있음
- => (-) 실패한 객체도 살아남을 수 있음 ((일반) 생성자에서 예외 -> 객체 생성 실패)
- 방어 방법
- 해당 클래스 자체 -> `final`로 선언
- 하위 클래스에서 `finalize()` 오버라이딩 방지
- `final` 클래스: 누구도 하위 클래스 만들 수 X
- `final`이 아닌 클래스: 아무 일도 하지 X finalizer 메서드 만들고 `final`로 선언
- 해당 클래스 자체 -> `final`로 선언
대안: `AutoCloseable` + `try-with-resources`
`AutoCloseable` 구현, `close()` 메서드로 자원 해제
예외 발생해도 `try-with-resources` ~> 안전하게 종료 O
- 각 인스턴스는 닫힘 여부 추적
- `close` 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록
- 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 `IllegalStateException` 던지는 것
finalizer와 cleaner 적절한 사용 사례
- 안전망 역할: 자원의 소유자가 `close` 메서드를 호출하지 않는 것 대비
- 둘다 즉시 (or 끝까지) 호출된다는 보장 X, 클라이언트가 하지 않은 자원 회수 늦게라도 해줌 (안하는 것보다 남)
- 자바 클래스의 라이브러리 일부 클래스: 안전망 역할의 finalizer 제공
- ex. `FileInputStream`, `FileOutputStream`, `ThreadPoolExecutor`
- 네이티브 피어(native peer)와 연결된 객체
네이티브 피어: 일반 자바 객체가 네이티브 메서드 ~> 기능 위임한 네이티브 객체
- 자바 객체 X -> GC) 존재 모름 => 자바 피어 회수할 때 네이티브 객체까지 회수 X
- But, 성능 저하 감당, 네이티브 피어가 심각한 자원 가지고 있지 않을 때만 사용
- 성능 저하 감당 X or 네이티브 피어가 사용하는 자원 즉시 회수해야 함 -> `close` 메서드 사용
cleaner 예시: 안전망 구현
방(room) 자원 수거하기 전에 반드시 청소(clean)해야 한다고 가정
-> `Room` 클래스는 `AutoCloseable` 구현
내부 구현 방식 -> cleaner 사용 여부 -> cleaner: 클래스의 public API에 나타나지 X (vs `finalizer`)
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// 청소가 필요한 자원/ Room 절대 참조 X
private static class State implements Runnable {
int numJunkPiles; // Room 안의 쓰레기 수
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
@Override public void run() {
System.out.println("방 청소");
numJunkPiles = 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();
}
}
- `static` 중첩 클래스 `State`: cleaner가 방을 청소할 때 수거할 자원 담고 있음
- 방 안의 쓰레기 수 `numJunkPiles`
-> 네이티브 피어를 가리키는 포인터를 담은 `final long` 변수여야 함 - `Runnable` 구현 -> `cleanable`가 자원 회수할 때 `run()` 메서드 호출
- 방 안의 쓰레기 수 `numJunkPiles`
- `cleanable` 객체
- `Room` 생성자에서 `Cleaner.register(this, state)` 호출해서 얻음
- `Cleaner`에 `Room`과 `State` 등록
- `run()` 호출되는 2가지 상황
- 직접 자원 해제
: 클라이언트) `Room.close()` 호출 -> 내부에서 `cleanable.clean()` 호출 -> `State.run()` 실행 - GC에 의한 회수 (최후의 안전망 역할)
: 클라이언트) `close()` 호출 X -> `Room`이 GC 대상 되면 -> cleaner) `State.run()` 호출
- 직접 자원 해제
- `Room` 생성자에서 `Cleaner.register(this, state)` 호출해서 얻음
- `State`: 절대로 `Room` 참조 X
- IF. 참조 O -> 순환참조 생김 => GC) `Room` 인스턴스를 회수해갈 (-> 자동 청소될) 기회 X
- `State`: 정적 중첩 클래스
- 정적 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 됨
- 람다 역시 바깥 객체의 참조 갖기 쉬워 사용하지 X 것이 좋음
- `Room`의 `cleaner`: 안전망으로 사용, 자원 회수의 주 책임 -> `close()`
- 클라이언트) 모든 `Room` 생성을 `try-with-resources` 블록으로 감싸면 자동 청소 필요 X
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("안녕");
}
}
}
-> `안녕` 출력 후 `방 청소` 출력
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("아무렴");
}
}
-> `아무렴` 출력 후 `방 청소` 출력 X
`cleaner` 명세
`System.exit`을 호출할 때의 `cleaner` 동작은 구현하기 나름/ 청소가 이뤄질지는 보장 X
명세에서 명시 X 일반적인 프로그램 종료도 마찬가지
`Teenager`의 `main` 메서드에 `System.gc()`를 추가하는 것으로 종료 전에 방 청소 출력 O
But, 모든 컴퓨터 그런다는 보장 X
핵심 정리
- `finalizer` 사용 X, `cleaner`: 최후의 안전망 역할 (중요 X 네이티브 자원 회수용)
- -> 불확실성, 성능 저하 주의
- 자원 회수: `AutoCloseable` + `try-with-resources`로 직접 관리
아이템 9 try-finally보다는 try-with-resources를 사용하라
자바 라이브러리에는 `close` 메서드를 호출해 직접 닫아줘야 하는 자원 ↑
ex. `InputStream`, `OutputStream`, `java.sql.Connection`
- 자원 닫기: 클라이언트가 놓치기 쉬움 -> 예측 X 성능 문제로 이어짐
- `finalizer`를 활용할 수 O But, 신뢰성, 성능 문제로 권장 X
전통적인 자원 관리: try-finally
자원이 반드시 닫히도록 보장
static STring firstLineOfFile(String path) throws IOException {
BufferdReader 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();
}
}
}
- 코드가 지저분하고 중첩 깊어짐
- 예외가 `try` 블록과 `finally` 블록 양쪽에서 발생 -> 두 번째 예외가 첫 번째 예외를 덮어버림
- ex. `readLine()` 실패 -> `close()` 실패 => 첫 번째 예외 사라져 디버깅 어려움
- 스택 추적 내역에 첫 번재 예외에 관한 정보 X
해결: try-with-resources (Java 7+)
- 닫아야 할 자원이 `AutoCloseable`를 구현해야 사용 가능
- `AutoCloseable`: `void close()` 하나만 정의한 인터페이스
- 대부분의 자바 표준 라이브러리 클래스들은 이미 `AutoCloseable`를 구현 O
- -> 직접 자원을 다루는 클래스를 만들 때도 반드시 `AutoCloseable` 구현
복수의 자원 처리
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);
}
}
- 코드 짧고 읽기 쉬움
- 예외 발생 -> 가장 중요한 예외만 보존
/ `close()`에서 발생한 예외 -> 숨겨진 예외(suppressed exception)로 기록 - `Throwable.getSuppressed()` 메서드: 숨겨진 예외 추적 O
catch 절과 함께 사용
중첩 X -> 다수의 예외 처리 O
ex. 파일 열거나 데이터 읽지 못했을 때 예외 던지는 대신 기본값 반환
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
핵심 정리
- 꼭 회수해야 하는 자원 -> `try-finally` 말고, `try-with-resources` 사용
- 코드 길이 ↓, 명확해짐, 예외 정보 더 유용하게 제공
- 정확하고 안전한 자원 회수 O
'💻 > Java' 카테고리의 다른 글
[이펙티브 자바] 2장 객체 생성과 파괴 - 아이템 7 다 쓴 객체 참조를 해제하라 (3) | 2025.07.25 |
---|---|
[이펙티브 자바] 2장 객체 생성과 파괴 - 아이템 6 불필요한 객체 생성 피하라 (1) | 2025.07.25 |
[이펙티브 자바] 2장 객체 생성과 파괴 - 아이템 5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (2) | 2025.07.25 |
[이펙티브 자바] 2장 객체 생성과 파괴 - 아이템 3 private 생성자나 열거 타입으로 싱글턴임을 보증하라 / 아이템 4 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2025.07.21 |