본문 바로가기

OOP, FP/이펙티브 자바

이펙티브 자바 05. 만기 참조와 메모리 누수

유효기간이 지난 객체 참조


프로그래머가 직접 메모리를 제어해야하는 C와 같은 언어와 달리, 자바는 가비지 컬렉션(Garbage Collection, 이하 GC)으로 메모리를 관리한다. 자바는 개발자가 메모리를 명시적으로 해제하지 않기 때문에 가비지 컬렉터(Garbage Collector)가 더 이상 필요 없는 객체를 찾아 지우는 작업을한다. 그래서 결국에는 메모리 관리가 필요하다는 사실을 망각하곤 한다. 본인 또한 GC에 대해 잘 알지 못하며, 메모리를 관리하지 않는 일이 습관이 되어버렸다.


그헣다면 우리가 객체 수집에 직접 관여해야할 상황은 어떤 상황일까? 바로 자체적으로 메모리를 관리하는 코드를 작성할 경우다.

코드를 살펴보자.


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
31
32
33
34
35
package exfire;
 
import java.util.Arrays;
import java.util.EmptyStackException;
 
public class Stack {
 
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INTERNAL_CAPACITY = 16;
    
    public Stack() { elements = new Object[DEFAULT_INTERNAL_CAPACITY]; };
    
    public void push (Object e) {
        ensureCapacity();
        elements[size++= e;
    }
    
    public Object pop() throws EmptyStackException {
        if (size == 0
            throw new EmptyStackException();
        return elements[--size];
    }
    
    
    /**
     * 적어도 하나 이상의 원소를 담을 공간을 보장한다.
     * 배열의 길이를 늘려야 할 때마다 두 배씩 늘린다.
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
 
cs


Stack 클래스는 배열로 이루어진 LIFO 컬렉션이다. 위 코드에는 메모리 누수 문제가 있다. 그 결과로, 가비지 컬렉터의 작업량이 많아져 성능이 저하되거나 메모리 요구량이 증가하면서 OutOfMemoryError가 던져지면서 프로그램이 종료될 위험이 존재한다.


만기 참조


그러한 메모리 누수 문제는 어디서 발생할까? 스택을 사용하면서 크기가 커졌다가 줄어들면서 제거한 객체들을 처리하지 못해서 생긴다. 스택이 더 이상 그 객체들을 사용하지 않아서 문제가 없을 것 같은데도 말이다. pop() 메소드의 구현을 살펴보자. 단순히 size를 줄이면서 객체를 반환하기만 한다. 그렇다면 pop() 메소드의 수행결과로 반환된 객체의 원래 참조는 제거된 것일까? 간단하게 말하면 제거되지 않는다. 이처럼 다시 이용되지 않을 참조를 만기 참조라고 한다. 즉, 첨자 값이 size보다 작은 곳에 있는 요소들은 실제로 쓰이는 참조들이지만, size보다 큰 곳에 있는 참조들은 그렇지 않으며, 만기 참조이다.


자바와 같이 자체적으로 쓰레기 수집하는 언어에서 발생하는 메모리 누수 문제(의도치 않은 객체 보유: unintentional object retention)는 찾아내기 어렵다. 만약 만기 참조의 실제 객체가 다른 참조를 포함하고 있으면, 그 객체들도 쓰레기 수집에서 제외된다. 따라서 만기 참조 몇 개로 인해 굉장히 많은 객체가 쓰레기 수집에서 제외될 가능성이 있다.


위의 클래스의 경우, 문제 해결은 간단하다. 쓸 일 없는 객체의 참조는 무조건 null로 바꾸면 된다. 사용하지 않는 참조들을 즉시 null로 만들어버리면 쓰레기 수집기는 해당 객체가 반환해도 좋은 것인지 바로 알 수 있다.

스택 클래스의 pop() 메소드를 수정해서 메모리 누수를 없애보자.


1
2
3
4
5
6
7
public Object pop() throws EmptyStackException {
    if (size == 0
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}
cs


만기 참조가 될 elements[size] 의 참조를 null로 변경하면서, 만기 참조가 될 위험을 없앴다. 이전에는 쓰레기 수집기가 실제 사용되는 객체와 반환해야 되는 객체를 알 방법이 없었지만, 참조를 null로 변경하면서 해당 참조가 유효하지 않음을 알 수 있게 되었다. 만기 참조를 null로 만들 경우 생기는 장점은 성능에만 있지 않다. 나중에 실수로 만기 참조를 사용하더라도 NullPointerException이 발생한다. 프로그램은 오작동하는 대신 바로 종료된다는 장점이 있다.


이런 메모리 누수에 관련 오류를 한 번 접하고 나면, 객체 사용이 끝나면 즉시 null로 참조를 변경해야 한다는 강박관념에 사로잡힐 수도 있다. 하지만 그럴 필요도 없고 바람직하지 않다. 프로그램만 난잡해지기 때문이다. 객체 참조를 null처리하는 것은 규칙이라기보단 예외적인 조치가 되어야한다. 만기 참조를 제거하는 가장 좋은 방법은 해당 참조가 보관된 변수가 유효범위(scope)를 벗어나도록 두는 것이다. 변수를 정의할 때 그 유효범위를 최대한 좁게 만들면 자연스럽게 해결된다. 본문의 처음에도 말했듯이, 개발자가 직접 참조를 null 처리하는 상황은 자체적으로 메모리를 관리하는 저장공간 풀이 있을 경우다.


* 유효범위를 좁게 만들면 해결된다는 부분이 무슨 소린가?

자바는 정적 블록 스코프를 갖는다. 즉 변수는 블록을 벗어나도록 두라는 것이다. 블록 안에서 객체를 생성하고 블럭이 종료되게 두면 해당 블록 안에서 생성된 객체는 GC의 대상이 된다.


정리


자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야 한다. 메모리 누수의 주 원인인 만기 참조는 반드시 null처리를 해줘야 의도치 않은 객체를 보유하는 상황을 예방할 수 있다.