아이템 7. 다 쓴 객체 참조를 해제하라

문법식·2022년 3월 4일
0

Effective Java 3/E

목록 보기
7/52

자바처럼 가비지 컬렉터를 갖춘 언어더라도 메모리 관리에 신경을 안 쓰면 안된다.

메모리 직접 관리

메모리 누수가 발생하는 스택 코드

//메모리 누수가 일어나는 스택
public class Stack {
    private Object[] elements;
    private int size=0;
    private static final int DEFAULT_INITIAL_CAPACITY=16;

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

    public void push(Object e){
        ensureCapacity();
        elements[size++]=e;
    }

    //pop() 코드에서 실제로 객체를 지우는 것이 아니라 size만 변경하고 있다. 필요없는 객체를 스택 안에 여전히 존재하는 것이다.
    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()의 코드를 보면 size만 조절할 뿐 더 이상 사용하지 않는 객체를 여전히 참조하고 있다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체...)를 회수해가지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.

메모리 누수 해결 코드

public Object pop(){
	if(size==0)
    	throw new EmptyStackException();
    Object result = this.elements[--size];
    this.elements[size]=null;

	return result;
}

해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다. 다 쓴 참조를 null처리하면 다른 이점도 있다. 만약 null처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.

주의사항

public void Example(){
	Object age=13;
    age=null;
}

주의해야 할 점은 메모리 관리를 한다고 위의 코드같이 모든 객체를 다 쓰자마자 일일이 null처리하려는 점이다. 그럴 필요도 없고 바람직하지도 않다. 프로그램을 필요 이상으로 지저분하게 만들 뿐이다. 객체 참조를 null처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope)밖으로 밀어내는 것이다.

public void Example(){
	Object age=13;
}

즉, 위의 코드처럼 Example()이 외부에서 호출되고 나면 Example()범위 내에 있는 age객체는 가비지 컬렉터의 대상이 된다. 객체를 일일이 null처리하려 하지말고 이런 식으로 메모리 관리를 하면 된다.

위의 스택 구현체 클래스가 메모리에 취약했던 이유는 스택 클래스가 메모리를 직접 관리하기 때문이다. 위의 스택 구현체 클래스는 elements배열로 저장소 풀을 만들어 원소들을 관리한다. pop()되어서 size밖에 있는 원소들을 사용하지 않는데 가비지 컬렉터가 이 사실을 알 길이 없다. 가비지 컬렉터가 보기에는 size 안의 요소나 밖의 요소나 객체 참조가 되어 있기 때문이다. 그러므로 size 밖의 사용하지 않는 요소 객체는 null처리해서 가바지 컬렉터에게 해당 객체를 더는 쓰지 않을 것임을 알려줘야 한다.
일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.


캐시

캐시 역시 메모리 누수를 일으키는 주범이다. 객체의 참조를 캐시에 넣고 나서, 그 객체를 다 쓴 뒤로도 그냥 놔두는 일을 자주 접할 수 있다. 해법은 여러가지가 있다. 운 좋게 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해서 캐시를 만들면 된다. 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.

public class CacheSample {

    public static void main(String[] args) {
        Object key1=new Object();
        Object value1=new Object();

        Map<Object, Object> cache=new WeakHashMap<>();
        cache.put(key1, value1);
    }
}

WeakHashMap은 키를 WeakReference로 캐시한다. 위의 key1 객체가 사라지면, 캐시하는 해당하는 키의 엔트리도 삭제된다. 단, WeakHashMap은 이러한 상황에서만 유용하다는 사실을 기억하자.
캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵다. 그래서 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해줘야 한다. (ScheduledThreadPoolExecuter 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다. LinkedHashMapremoveEldestEntry 메서드를 써서 후자의 방식으로 처리한다. 더 복잡한 캐시를 만들고 싶다면 java.lang.ref패키지를 직접 활용해야 한다.


리스너 혹은 콜백

클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다. 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해간다. 예를 들어 WeakHashMap에 키로 저장하면 된다.


핵심 정리

메모리 누수는 발견하기 쉽지 않기 때문에 시스템에 수년간 잠복해 있을 수 있다. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러(heap profiler)같은 디버깅 도구를 동원해야만 발견되기도 한다. 그래서 이런 종류의 문제는 예방법을 미리 익혀두는 것이 매우 중요하다.

profile
백엔드

0개의 댓글