[Effective Java] Item7. 다 쓴 객체 참조를 해체하라

2022년 03월 04일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

자바에서는 사용하지 않는 객체의 경우 GC(Garbage Collection)에서 알아서 회수를 해주며 메모리 관리를 해줍니다. 하지만 GC가 메모리 관리를 해준다고해서 우리가 완전히 신경을 쓰지 않아도 되는건 아닙니다. 가비지 컬렉션에서는 특정 상황에서 메모리 누수가 발생하고, 우리는 그러한 상황을 인지하고 대응해줘야합니다.

메모리 누수는 언제 발생할까?

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

    public Stack() {
        elements = new Object[DEFAULT_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을 하였을 때 스택에서 꺼내진 객체들에게서 발생합니다. 객체들이 pop을 통해 꺼내졌다 하더라도 실제 코드를 보면 elements[--size]를 통해 Index만 이동시켰을 뿐 실제 객체의 참조값은 스택이 갖고 있습니다. 이렇게 스택이 다 쓴 참조(obsolete reference)를 갖고 있을 경우 해당 참조값들은 GC가 처리하지 못하게 됩니다.

이러한 메모리 누수를 관리하지 않을 경우 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하됩니다. 또한 심할경우 디스크 페이징이나 OutOfMemoryError가 발생하여 프로그램이 종료될 수 있습니다.

메모리 누수 해결방법

메모리 누수를 해결하는 방법은 해당 참조를 다 사용하였을 때 null로 변경해주면 됩니다. 참조 값을 null로 변경할 경우 해당 참조 값은 더이상 사용되는 곳이 없다면 GC에서 처리를 해주게 됩니다.

위의 pop()메서드를 올바르게 고치면 다음과 같습니다.

    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        Object popObject = elements[size];
        elements[size--] = null; // 참조 해제
        return popObject;
    }

이와 같이 다 쓴 참조를 null로 변경할 경우 해당 참조에 접근하게 될 때 NullPointException을 발생시킬 수 있다는 장점도 있습니다.

null로는 항상 바꿔야하나?

사용이 끝난 객체 참조를 null로 변경하는 것은 예외적인 경우에만 사용하는 것이 좋으며, 다 쓴 객체 참조를 해제하는 가장 좋은 방법은 해당 객체 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것입니다.

그렇다면 null로 변경을 하는 메모리 관리는 언제 해야할까요? 일반적으로는 자신의 메모리를 직접 관리하는 클래스를 사용할 때 항시 주의를 해야합니다.

메모리 누수를 발생시키는 3개의 주범

  1. 앞서 말한 Stack과 같이 자신의 메모리를 직접 관리하는 클래스
  2. 캐시 메모리

    • 객체 참조를 캐시에 넣고 한참을 낮으면 누수가 일어난다.
    • 해결방법 1: 캐시 외부에서 key를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황이면 WaekHachMap을 사용한다.
    • 해결방법 2: 엔트리 유효 기간이 정의되지 않아 시간이 지남에 따라 엔트리 가치를 떨어뜨리는 방식일 경우 백그라운드 스레드 또는 캐시에 새 엔트리를 추가할 때 추가적인 작업을 해준다.
  3. 리스너(listener) or 콜백(callback)

    • 클라이언트가 콜백을 등록만하고 해지하지 않는다면 콜백은 계속 쌓여갈 것입니다.
    • 해결방법: 콜백을 약한 참조(weak reference)로 저장하면 컬렉터가 즉시 수거해갑니다.

핵심 정리

메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 존재합니다. 이런 누수는 철저한 코드리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 한다. 그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh