본문 바로가기

Language & Framework/Java

이펙티브 자바 (7) item8,9 : try-with-resources를 사용하라, finalizer, cleaner를 사용하지 말아라.

 

오늘 다룰 부분은 Item 8 : Finalizer와 Cleaner 사용을 피하라 Item 9 : try-final보다는 try-with-resources를 사용하라이다.

하나하나 모두 짚고 넘어가기엔 이 책만 1년 동안 정리해야 할 것 같아서 Finalizer나 Cleaner 같은 사실상 거의 볼 일 없는 녀석들은 눈으로만 읽고 넘어가려고 했는데, try-with-resources와 함께 다루면 좋을 것 같아 짚고 넘어가려고 한다.

 

우선, Finalizer와 Cleaner는 현재 사용 중인 자원을 반환하는 것에 그 목적이 있으나 실제로 이를 사용해야하는 경우는 거의 없다고 보면 된다.

거의 없는 것을 넘어서 Finalizer는 아예 사용하면 안된다. 이것이 남아있는 것은 오직 호환성 하나 때문이다.

둘이 공통적으로 가지고 있는 단점을 꼽자면, 첫 번째로는 불확실성이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class EffectiveFinalizer {
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
 
    }
 
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        int i=0;
        for (;;) {
            i++;
            new EffectiveFinalizer();
 
            if ((i % 1000000)==0) {
                Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
                Field queue = finalizerClass.getDeclaredField("queue");
                queue.setAccessible(true);
                ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queue.get(null);
 
                Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
                queueLengthField.setAccessible(true);
                long l = (long) queueLengthField.get(referenceQueue);
                System.out.format("thehre are %d references in the queue %n", l);
            }
        }
    }
}
cs

 

 

말 그대로 불확실하다. 이 코드를 실행할 경우 값이 시시때때로 1000000을 넘어가는 것을 확인할 수 있다.

Finalizer와 Cleaner는 적시에 호출될 수도, 호출되지 않을 수도 있다.

즉, 이 두 녀석들을 믿고 명시적으로 자원을 해제해주지 않는다면 그것은 메모리 누수로 이어질 것이고, 우리의 어플리케이션은 서서히 죽어갈 것이다.

 

두 번째는 비용이 비싸다. 저자인 조슈아 블로크의 환경을 기준으로 하자면 AutoCloseable에 비해 120배 가량의 성능 차이를 보였다고 한다.

웃기는 사실은, 이 둘은 비용이 비싼데 Garbage Collector는 자원에 여유가 있을 때만 이 녀석들의 부탁을 들어준다는 것이다.

리소스를 정리하고 싶어서 해당 클래스를 사용하는 건데 정작 비용이 비싸서 필요한 순간에는 작동할 수 없다? 정말 존재 자체가 기믹이라고 볼 수 있다.

 

이후로는 Finalizer의 단점이다. Finalizer는 개발자의 사소한 실수로 인해 참조 관계에 문제가 생긴다면 객체를 무한증식시킬 수 있다.

지정된 순서대로 실행되지도 않으며, Finalizer가 작동하는 시점에서 해당 코드는 즉시 중단되고 예외가 발생해도 무시당한다.

제일 중요한 내용을 가장 아래에 아무렇지 않게 적어놓으셨네..

멀티쓰레드 환경에서는 순서가 보장되지 않는 문제로 여러 쓰레드에서 자원에 접근하여 교착 상태를 유발할 수 있으며, 무엇보다 가장 심각한 문제는 Finalizer attack이라는 보안 취약점이 존재한다.

 

이 정도 했으면 더 이상 Finalizer에 대한 설명이 필요하지 않으리라 생각한다.

게다가 Finalizer는 이미 Deprcated 딱지까지 붙어있으니, 굳이 이걸 사용하는 사람은 없을 것이다. 

 

그러면 그나마 양호해보이는 Cleaner는 어떨까?

Cleaner는 "절대 쓰지마!" 수준을 벗어난 정도라고 보면 된다.

여전히 예측할 수 없으며, 잘못된 참조 관계 설정 시 일어나는 문제점들도 여전하다.

아래는 Cleaner의 예시 코드이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
 
    private static class State implements Runnable {
        int num;
 
        State(int num) {this.num = num;}
 
        @Override
        public void run() {
            System.out.println("Cleaning room");
            num = 0;
        }
    }
 
    private final State state;
 
    private final Cleaner.Cleanable cleanable;
 
    public Room(int num) {
        state = new State(num);
        cleanable = cleaner.register(this, state);
    }
 
    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}
 
cs

 

우선 AutoCloseable은 무시하자.

이 Room이라는 객체는 생성 시 스스로를 cleaner에 등록하고 내부에 cleaner를 가지고 있다.

 

그러면 이 객체를 생성해서 System.gc()를 실행하면 어떻게 될까?

정답은 "실행해서 결과를 보기 전까지는 아무도 모른다"이다.

 

어플리케이션에서 이런 불확실성은 굉장히 치명적이기 때문에 Cleaner 또한 존재 자체가 애매한 클래스라고 볼 수 있다.

다만 저자는 Cleaner를 AutoCloseable을 구현한 클래스의 보조 안전망 정도로 사용할 수 있다고 이야기 한다.

 

즉, try-with-resources 형태로 사용하라고 만들어놓은 객체를 사용자가 try문 없이 사용할 때 혹시나 운이 좋으면(..) 자원이 반환될 수 있으니, 그 정도 용도로 사용하라는 것이다.

 

그러면 try-with-resources는 대체 무엇이길래 권장하는 것일까?

우선 위에서 언급했던 AutoCloseable를 살펴보자.

 

 

책의 저자인 조슈아 블로크님이 작성한 API 명세다.

위 내용보다는 close() 메서드에 적혀있는 내용이 중요하다.

 

 

 

혹시나 나보다 더 영어가 약할 누군가를 위해 발번역을 적겠지만, 내 영어 실력은 처참하기 때문에 되도록 직접 읽어보자.

해당 리소스를 닫고, 기반이 되는 모든 리소스를 닫는다.
try-with-resources statement에 의해 관리되는 개체에서  이 메서드는 자동적으로 호출된다.

이 인터페이스 메서드가 Exception을 throw하도록 선언되었지만, 구현자는 더욱 강하게 연관된(명시적인) exception을 선언하거나 닫기 작업이 실패할 수 없는 경우에는 exception을 throw하지 않도록 구성하는 것이 좋다.

닫기 작업이 실패할 수 있는 경우 구현자의 주의가 요구된다. 리소스를 닫고 그것을 표기하는 것이 좋다, exception을 던지기 전에.
close method는 두 번 이상 호출되지 않으며, 때문에 리소스는 제 때 해제된다.
게다가 이것은 리소스가 다른 리소스를 래핑하거나 래핑되어있을 때 발생할 수 있는 경우의 문제 발생 가능성을 감소시킨다.

또한 인터페이스의 구현자는 InterruptedException을 thorw하지 않는 것이 좋다.
이 예외는 쓰레드의 인터럽트 상태와 상호작용한다. 그리고 런타임 misbehavior(?)이 발생할 수 있다. 만약 interruptedException이 억제된다면(?)
보다 일반적인 경우에서, 만약 예외에 대한 문제가 억제된다면 AutoCloseable은 예외를 throw하지 말아야 한다.
(Suppressed Exception은 예외가 여러개(혹은 연쇄적으로) 발생하는 과정에서 잡아먹히는 예외를 의미한다. 나의 부족한 자바 지식과 독해 실력으로 인해 해당 문장을 원활하게 해석할 수 없으나 InterruptedException은 쓰레드의 인터럽트 상태와 상호작용하는 중요한 역할을 가지고 있기 때문에 아무렇게 던지거나(throw) suppressed되면 안되며 적절하게 처리해야 한다는 뜻인 것 같다.
또한 Suppressed된 Exception을 throw할 필요가 없다는 말은 아래 예시를 보면 이유를 알 수 있다.)

참고로 이 메서드는 java.io.Closeavle의 close() 메서드와 다르다. 이 close 메서드는 멱등성을 요구받지 않는다.
다른 말로 하자면, 해당 메서드에 대한 다수의 요청은 부수 효과를 가질 수 있다. 
이와 다르게 Closeable의 close는 이것이 여러 번 호출되더라도 어떤 효과도 가지지 않을 것을 요구 받는다.

그러나, 구현자는 이 close method도 멱등성을 가지도록 구현하는 것을 강하게 권장한다.

 

음.. 생각보다 길어서 본론과 관계 없는 쓸데없는 내용이 너무 많이 들어가버렸다.

아무튼 해당 AutoCloseable을 상속 받아 해당 메서드를 구현하면 try-with-resources문에서 자동으로 close()가 호출되어 자원을 정리해준다고 한다.

 

자동으로 close해준다는 것은 엄청난 장점일 수 밖에 없는 게, 개발자가 매번 직접 close()를 호출해주는 것은 비효율적이고 귀찮은 것을 넘어서 신뢰도가 굉장히 떨어지는 일이기 때문이다.

그나마 하나의 try-finally statement에서 하나의 개체만 닫는다면 다행이지만 여러개가 포함된다면 말이 달라진다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    static void copy(String src, String dst) throws IOException {
 
        InputStream in = new FileInputStream(src);
        try {
            FileOutputStream 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();
        }
    }
cs

 

고작 2개의 자원을 안전하게 닫기 위한 try-finally문이다.

그러면 try-with-resources는 뭐 얼마나 잘났길래?

 

 

1
2
3
4
5
6
7
8
    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)) >= 0out.write(buf, 0, n);
        }
    }
cs

 

그렇다, 많이 잘났다.

그냥 try문에서 객체를 선언하기만 한다면 끝나는 시점에 알아서 다 close해주니 저런 끔찍한 코드를 작성할 일이 없다.

게다가 이것은 Java7 기준 코드고, Java9부터는 try 밖에서 초기화한 자원도 try문 내부로 가지고 들어와서 사용할 수 있다.

 

그리고 try-with-resources는 내부에서 suppressed된 exception들을 모두 stack trace에서 보여준다.

try-finally문은 만약에 여러 자원을 사용할 경우 마지막으로 문제가 발생한 자원의 예외만을 보여주는데 try-with-resources문은 모든 예외를 보여주는 것이다.

(단, try-finally도 가능은 하다. 안 그래도 더러운 코드가 더 더러워질 뿐.)

 

개발자의 실수도 막아주고, 코드 가독성도 높여주며 디버깅까지 유리한데 이걸 사용하지 않을 이유가 없다.

이래도 try-with-resources 안쓰고 try-finally 쓰겠다고?

아.. 그게 아니라 springBoot가 다 해주고 파일도 클라우드 서버에 올리기 때문에 그런 자원 쓸 일이 딱히 없다구요..?

그럼 어쩔 수 없는 일이다..

 

그래도 알아두면 언젠가 쓸 일이 있지 않을까?

즐겁게 배웠다면 그것으로 만족한다. 아무튼 오늘도 여기서 끝이다.