C, C++처럼 메모리를 직접 관리해야 하는 언어를 사용하다가 자바처럼 가비지 컬렉터를 갖춘 언어로 넘어오면 프로그래머의 삶이 훨씬 평안해진다. 다 쓴 객체를 알아서 회수해가니 말이다. 그래서 자칫 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데 절대 사실이 아니다.
스택을 간단히 구현한 다음 코드를 보자.
Stack
package items.item7;
import java.util.Arrays;
import java.util.EmptyStackException;
public class Stack {
private Object[] elements;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public Stack() {
elements = new Object[DEFAULT_CAPACITY];
}
public void push(Object element) {
ensureCapacity();
elements[size++] = element;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (size == elements.length) {
elements = Arrays.copyOf(elements, size * 2);
}
}
}
- 특별한 문제는 없어 보인다. 별의별 테스트를 다 해도 거뜬히 통과할 것이다. 하지만 꼭꼭 숨어 있는 문제가 있다. 이는 바로 '메모리 누수'이다. 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다. 드물게 OutOfMemoryError가 발생할 수도 있다.
어디서 문제가 생겼을까? 스택이 커졌다가 줄어들었을 때, 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다. 프로그램에서 그 객체들을 더 이상 사용하지 않는다고 하더라도 말이다. 이 스택이 그 객체들의 다 쓴 참조를 여전히 가지고 있기 때문이다.
가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그객체들이 참조하는 모든 객체 ...)를 회수해가지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.
해법은 간단하다. 해당 참조를 다 썼을 때 null 처리(참조 해제)를 하면 된다. 각 원소의 참조가 더 이상 필요 없어지는 시점은 스택에서 꺼내질 때다.
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
다 쓴 참조를 null 처리하면 다른 이점도 따라온다. 만약, null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료할 것이다. (미리 null 처리하지 않았다면 아무 내색 없이 무언가 잘못된 일을 수행할 것이다)
그렇다고 모든 객체를 다 쓰자마자 일일이 null 처리하라는 말은 아니다. 오히려 필요 이상으로 지저분하게 만들 뿐이다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다. 변수의 범위를 최소가 되게 정의했다면 이 일은 자연스럽게 이뤄진다.
예를 들면, elements 라는 배열을 사용하는 부분을 특정 메서드에서만 사용하도록 변경하여 메서드 호출 후 메서드가 종료되면 더 이상 elements를 참조하고 있는 곳이 없도록 한다던가 하는 방식으로 말이다.
그게 아니라면, 이 경우에는 애시당초에 java.util.Stack, Deque를 사용하면 된다. 이미 내부적으로 잘 만들어져 있기 때문에 메모리 누수걱정을 할 필요가 없다. 그리고 사실 자바가 만들어놓은 Stack, Deque도 내부적으로 null처리를 한다.
메모리 누수의 세 번째 주범은 바로 리스너 혹은 콜백이라 부르는 것이다. 클라이언트가 콜백, 리스너를 등록만 하고 명확히 해지하지 않는다면 뭔가 조치해주지 않는 한 콜백은 계속 쌓여간다. 이럴땐 리스너나 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해간다. 예를 들어, WeakHashMap의 키로 저장하면 된다.
'Effective Java' 카테고리의 다른 글
아이템 8 - finalizer, cleaner 사용을 피하라 (5) | 2024.09.16 |
---|---|
아이템 6 - 불필요한 객체 생성을 피하라 (2) | 2024.09.16 |
아이템 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2024.09.16 |