Link
Today
Total
10-17 08:33
Archives
관리 메뉴

초보개발자 긍.응.성

(이펙티브 자바 3) 7. 다 쓴 객체 참조를 해제하라 본문

책 정리/이펙티브 자바 3

(이펙티브 자바 3) 7. 다 쓴 객체 참조를 해제하라

긍.응.성 2020. 7. 10. 01:06
반응형

C와 Java의 가장 큰 차이점이라면 메모리 관리와 가비지 컬렉터라고 말할 수 있다. 가비지 컬렉터는 다 쓴 객체를 알아서 회수해가기에 메모리를 직접 관리할 필요가 없다. 그렇다고 메모리 관리에 더 이상 신경을 쓰지 않아도 된다고 생각할 수 있지만, 이번 아이템은 절대 그렇게 생각해서는 안된다는 것을 말하고 있다.

아래와 같이 구현한 Stack 클래스를 보자

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITAL_CAPACITY];
    }

    // push : size를 확인하여 push 가능하면 그대로 push, 그렇지 않다면 배열 크기를 두 배씩 늘려 공간을 확보한 후 push한다.

    public Object pop() {
        if (size == 0) {
            throws new EmptyStackException();
        }
        return element[--size];
    }
}

위의 코드에는 메모리 누수가 존재한다. 스택이 커졌다가 줄어들 때 스택에서 꺼내진 객체들은 가비지 컬렉터가 회수하지 않는다. 이 Stack 클래스가 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

가비지 컬렉션 언어에서의 메모리 누수

가비지 컬렉션 언어에서는 의도치 않게 객체를 살려두는 메모리 누수를 찾기 힘들다. 객체 참조가 살아있다면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체, 그리고 또 그 객체들이 참조하는 모든 객체를 회수하지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수하지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.

null 처리를 통한 참조 해제

Stack클래스의 문제의 해법은 해당 참조를 다 썼을 때 null 처리를 통한 참조 해제이다.

public Object pop() { // 참조를 사용하는 pop 메서드에 참조 해제가 들어가야 맞다.
    if (size == 0) {
        throws new EmptyStackException();
    }
    Object result = element[--size];
    element[size] = null; // 다 쓴 참조 해제
    return result;
}

다음과 같은 null 처리에는 또 다른 이점도 따라온다. 만약 null 처리한 참조를 실수로 사용하려 하면 프로그램은 NullPointerException을 던지며 종료된다. 이러한 오류를 통해 프로그램에 존재하는 잘못된 로직이나 문제를 조기에 파악할 수 있다.

null 처리 시기

객체 참조를 null 처리하는 방식은 간단한 참조 해제 방법이지만 이러한 경우는 예외적인 경우여야 한다. 모든 객체를 다 쓰자마자 일일이 null 처리하는 것은 꼭 필요하지 않은 곳에 부가적은 코드를 생성할 수 있으며, 프로그램을 필요 이상으로 지저분하게 만들 수 있다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 (scope) 밖으로 밀어내는 것이다.

그렇다면 Stack 클래스에서 null 처리를 해준 이유는 무엇일까? Stack클래스는 자기 메모리를 직접 관리하기 때문이다. elements 배열로 저장소 풀을 만들고 size를 통해 사용하는 활성 영역과 사용하지 않을 비활성 영역이 구분된다. 하지만 이 사실은 프로그래머만 알고 있을 뿐 가비지 컬렉터는 이 사실을 모른다. 그러므로 프로그래머는 비활성 영역이 되는 순간 null 처리를 통해 해당 객체를 더 이상 사용하지 않을 것임을 가비지 컬렉터에게 알려야 한다.

일반적으로 자기 메모리를 직접 관리하는 클래스라면 항시 메모리 누수에 주의해야 한다. 원소를 다 사용한 즉시 그 원소가 참조한 객체를 null 처리해주어야 한다.

캐시

캐시도 메모리 누수를 일으키는 주범이다. 객체 참조를 캐시에 놓고 해당 객체를 모두 사용하고도 한참을 그냥 놔두는 일이 발생할 수 있다. 이는 WeakHashMap을 사용한 캐시를 만들어 해결할 수 있다. 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.

캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치(우선순위)를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 청소해주기 위해 ScheduledThreadPoolExecutor와 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 존재한다.

리스너와 콜백

리스너와 콜백도 메모리 누수의 주범이다. 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 특별한 조치가 이루어지지 않는 한 콜백은 계속 쌓여갈 것이다. 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해가도록 하는 방법이 존재한다.

weak reference

java.lang.ref 패키지의 WeakReference를 통해 약한 참조를 만들 수 있다.

WeakReference<Integer> soft = new WeakReference<Integer>(prime);

prime == null이 되면 해당 객체를 가리키는 참조가 WeakReference 뿐일 경우 가비지 컬렉팅의 대상이 된다.

자바에서 가비지 컬렉팅(GC)의 대상이 되는 참조는 부드러운 참조(soft reference)와 약한 참조(weak reference)가 존재한다. 부드러운 참조는 메모리가 부족하지 않으면 굳이 GC를 하지 않는다. 그에 반해, 약한 참조는 무조건 GC의 대상으로 두어 다음 GC가 발생하는 시점에 무조건 없어지도록 한다.


정리

메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다. 또한 철저한 코드 리뷰나 디버깅 도구들을 동원해야만 발견할 수 있다. 그렇기에 이런 종류의 문제는 예방법을 익혀두어 애초에 문제가 발생하지 않도록 하는 것이 중요하다.

메모리 누수를 막는 방법으로는 null 참조와 weak reference를 활용하는 방법이 존재한다.

반응형
Comments