개발/Effective JAVA

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

ch4njun 2021. 6. 22. 17:13
반응형

C, C++에서와는 다르게 JAVA에서는 GC(가비지 컬렉터) 덕분에 메모리 관리에서 어느정도 벗어나 프로그래밍에 더욱 집중할 수 있다. 하지만, 그렇다고 해서 JAVA는 메모리 관리를 하지 않아도 된다고 오해해서는 절대 안된다.

 

public class Stack {
	private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
    	this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push() {
    	ensureCapacity();
        elements[size++] = e;
    }
    public Object pop() {
    	if (size == 0) throw new EmptyStackException();
        return elements[--size];
    }
    // 공간이 부족할 시 2배로 공간을 확장한다.
    private void ensureCapacity() {
    	if (elements.length == size)
        	elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

위 코드에서 메모리 누수가 어디서 발생하는지 생각해보자.

 

별 문제가 없어보이지만 위 스택을 사용하는 프로그램을 계속 실행하다보면 점차 가비지 컬렡션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다. 심할 경우에는 디스크 페이징이나 OutOfMemeoryError를 일으켜 프로그램이 예기치 않게 종료될 수도 있다.

 

위 코드에서는 push, pop에서 메모리 누수가 발생하는데, Stack에서 특정 객체를 꺼낸 후에 메모리에서 해제하는 것이 아니라 그대로 보관하다가 push가 들어왔을 때 그 위치를 덮어 씌우는 방식으로 프로그래밍이 되어있다.

 

이러한 방법이 Stack에서 꺼낸 후 반환한 객체를 더 이상 사용하지 않음에도 불구하고 참조가 해제되지 않았다.

 

"어? 어차피 카비지 컬렉션이 알아서 메모리 해제해주는거 아니야?" 라고 생각할 수 있겠지만 그렇지 않다. 왜냐하면 해당 객체는 배열에서 여전히 참조하고 있기 때문이다.

(해당 인덱스를 다른 객체로 다시 덮어 썻을 경우에는 가비지 컬렉션의 대상이 될 거 같기는 한데...)

 

이러한 문제를 가볍게 여기면 안되는 것이 해당 객체가 메모리 해제되지 않는다는 것은 그 객체가 참조하고 있는 다른 객체들 또한 메모리 해제가 되지 않기 때문이다.

참조해제 방법


그러면 위 상황에서처럼 더 이상 쓰지 않는 객체를 가비지 컬렉션의 대상이 되도록 참조해제하는 방법으로는 어떤 것이 있을까?

첫 번째 방법

해당 참조에 대해서 null 값을 대입함으로써 해당 객체에 대한 참조를 해제할 수 있다.

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

위와 같이 반환 후 더 이상 사용되지 않을 객체에 대해서 null을 대입함으로써 메모리 누수가 없는 안전한 코드를 완성할 수 있다.

 

이렇게 함으로써 얻을 수 있는 부가적인 장점은 누군가가 악의적으로 혹은 실수로 해당 객체를 다시 사용하려 했을 때 NullPointException이 발생하며 사전에 에러를 발생시킬 수 있다는 점이다.

 

하지만 이렇게 일일히 null을 대입하는 것은 프로그램을 필요 이상으로 지저분하게 만들 수 있다.

 

이러한 방법을 사용하는 경우는 위 예제 코드처럼 자기 메모리를 직접 관리하는 경우이다. 예를 들어 Stack은 특정 메모리를 사전에 할당받고 활성영역과 비활성영역을 size라는 기준을 통해서 나눈다. 하지만 이 비활성 영역이 더이상 사용되지 않는다는 것은 개발한 프로그래머만이 아는 사실이다. 따라서 이러한 경우 null을 통해 가비지 컬렉션에게 해당 객체가 더이상 사용되지 않는다고 알려야한다.

두 번째 방법

다 쓴 참조를 해제하는 가장 좋은 방법은 해당 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다. 즉, 최대한 작은 범위를 가지는 지역변수를 사용해 해당 범위에 대한 코드 실행이 끝나면 자연스럽게 참조는 해제되고 해당 객체는 가비지 컬렉션의 대상이 된다.

(아이템 57에서 해당 내용을 다룬다! 아직 멀었지만ㅎㅎ)

정리


이외에도 캐시, 리스너, 콜백함수와 같은 것들이 메모리 누수를 발생시킬 수 있다. 따라서 사용되지 않는 객체가 있다면 프로그래밍 단계에서 반드시 해제를 해주는 등의 조치를 해야 한다.

 

메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다. 이러한 메모리 누수는 철저한 코드 리뷰와 힙 프로파일러 같은 디버깅 도구를 동원해야 발견할 수 있다.

 

따라서 이와같은 예방법을 사전에 알고있는 것이 굉장히 중요하다.

반응형