Item 7. 다 쓴 객체 참조를 해제하라

다람·2025년 2월 24일
0

Effective Java

목록 보기
7/13
post-thumbnail

1. 자바의 가비지 컬렉션과 메모리 누수

자바는 가비지 컬렉션(Garbage Collection, GC)을 통해 자동으로 메모리를 관리한다. 그래서 메모리 관리에 신경 쓰지 않아도 된다고 착각하기 쉽다. 하지만 잘못된 객체 참조를 계속 유지하면 메모리 누수(메모리 낭비)가 발생한다.

  • 가비지 컬렉터 : 프로그래머가 동적으로 할당한 메모리 영역 중 더 이상 쓰이지 않는 영역을 자동으로 찾아내어 해제하는 기능이다.
  • 메모리 누수(Memory Leak) : 실제로는 더 이상 사용하지 않지만 참조가 남아있어 GC가 수거하지 못하는 객체가 쌓이는 것.

예: 메모리 누수가 심각해지면 디스크 페이징(메모리가 부족해 디스크를 사용)과 같은 성능 저하나, OutOfMemoryError가 발생할 수 있다.

디스크 페이징(Disk Paging)이란?

  • 시스템 메모리가 부족할 때, 하드디스크를 임시 메모리처럼 사용하는 과정을 말한다.
  • 메모리를 많이 차지하는 불필요한 객체가 계속 쌓이면 디스크 페이징이 잦아져서 성능이 크게 저하될 수 있다.

2. 다 쓴 참조(obsolete reference)란?

  • 다 쓴 참조: 앞으로 다시는 사용하지 않을 객체를 여전히 참조하고 있는 것 이다.
  • 가비지 컬렉터는 해당 참조가 남아 있으면 객체가 유효한 것으로 판단해 메모리를 회수하지 않는다.

스택(Stack)을 구현한 코드를 살펴보면 문제점을 알 수 있다.

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;
    }

    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()으로 배열의 끝 원소를 꺼냈지만, 배열의 해당 칸에는 여전히 그 객체 참조가 남아 있다.
  • 가비지 컬렉터는 참조가 있으니 살아있는 객체로 보고 회수하지 못한다메모리 누수가 발생

해결방법

  • 참조를 해제한다.
public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}
  • 스택에서 사용하지 않는 배열 칸null 처리리를 해서 참조를 해제게되면 가비지 컬렉터가 객체를 회수할 수 있게 된다.
  • 이런 직접 메모리 관리 하는 클래스에서는 다 쓴 참조를 해제해줘야한다.
  • 직접 메모리를 관리하는 클래스가 뭐지라고 생각할 수 있다. 직접 메모리 관리를 한다는 것은 내부적으로 배열이나 자료구조를 써서 객체 참조를 수동으로 관리한다는 뜻이다.
  • 자바의 표준 컬렉션들은 자동으로 필요 없는 참조를 제거하거나 가비지 컬렉터가 스코프를 알기 때문에 문제가 없다.
  • 직접 배열을 사용하거나(스택, 큐 등), 풀을 만들어 재사용하는 경우에 객체가 언제 필요 없어지는지를 개발자가 명확히 알려주지 않으면 가비지 컬렉터가 알 수 없어 메모리 누수가 발생할 가능성이 커진다는 말이다.

3. null 처리는 예외적 상황에만 하자

“모든 객체 참조를 사용 후 즉시 null로 설정하면 되지 않을까?”

  • 책을 읽으면서 처음 읽을 때 나도 한 생각이다. 하지만 이렇게 코드를 작성하게되면 코드가 지저분해진다. 매번 null을 대입하느라 가독성이 떨어지게되고, 가비지 컬렉터가 크게 좋아지는 것도 아니라고 한다.

  • 예를 들어서 아래의 코드가 메서드 곳곳마다 null을 대입한다고 하면, 보기만 해도 왜 null 처리를 하는건지 지저분해보이는 것 같다.

    public void someMethod() {
        Object obj = new Object();
        // === 객체 사용 ===
        obj = null; // 사용 끝났으니 null 처리
    
        // === 또 다른 객체 생성 ===
        Object another = new Object();
        // ... 객체 사용 ...
        another = null; // 사용 끝났으니 null 처리
    }
  • 스코프를 벗어나는 등의 자연스럽게 참조가 사라지는 구조를 선호한다.
    - 지역 변수는 해당 블록이 끝나면 스택에서 사라지게 된다. 이런 구조가바로 null을 대입하지 않더라도 블록 스코프를 벗어나 참조가 사라졌기 때문에 가비지 컬렉터가 자동으로 회수 할 수 있는 구조이다.(스코프에서 밀어내기)

  • 그러나 스택처럼 객체 참조를 직접 관리하는 구조라면 예외적으로 null 처리가 필요하다는 기 때문에 null 처리를 예외적인 상황에서만 하자라고 말하는 것이다.

4. 메모리 누수의 주범 3가지

4-1. 자기 메모리를 직접 관리하는 클래스

  • 예 : 직접 작성한 Stack처럼 배열로 원소를 관리하는 클래스
  • 비활성 영역의 원소들이 실제로는 더 이상 쓰이지 않음에도 참조가 남아 있으면 가비지 컬렉터가 회수하지 못한다.

해결방법

  • 비활성화된 영역에 있는 참조를 null 처리한다.

참고로 실제로 Java.util에 구현되어 있는 Stack은 아래와 같은 방식을 통해서 null 처리를 수행하고 있다.
Stack과 Vector

4-2. 캐시(Cache)

  • 캐시는 자주 사용하는 데이터를 임시로 저장해두고 사용하기 위한 공간이다.
  • 캐시에 객체 참조를 넣어두고, 다 쓴 후에도 제거하지 않으면 누수가 발생한다.
  • Key를 더 이상 사용 안 해도 캐시에 남아있으면 가비지 컬렉터는 객체를 회수하지 못한다.

해결방법

  • WeakHashMap : Key가 강한 참조가 사라지면 자동으로 엔트리를 제거한다.
  • LinkedHashMap의 removeEldestEntry 메서드 등을 사용하여 주기적으로 가장 오랫동안 사용하지 않은 데이터를 먼저 제거해서 캐시를 청소(예: LRU 캐시)한다.
  • 일정 시간이 지나면 자동으로 제거하는 로직(백그라운드 스레드, 스케줄러 등)이다.

4-3. 리스너(Listener)와 콜백(Callback)

  • 이벤트 리스너나 콜백을 등록하고 명시적으로 해제하지 않으면 해당 객체는 계속 참조가 남아 있어서 이벤트가 계속 필요한 것으로 인식되고 가비지 컬렉터가 회수하지 못한다.

해결방법

  • 더 이상 필요 없는 리스너를 removeListener() 같은 메서드로 명시적으로 해제한다.
  • 또는 리스너를 약한 참조(Weak Reference)로 관리해 가비지 컬렉터가 알아서 제거하게 한다.

5. 결론

  1. 가비지 컬렉터가 자동으로 메모리를 관리하지만, 사용하지 않는 객체에 대한 참조가 남아 있으면 여전히 누수가 발생한다.
  2. 자기 메모리를 직접 관리하는 클래스는 다 쓴 객체 참조를 null 처리하여 해제해야 한다.
  3. 캐시리스너 또는 콜백을 쓰는 경우 더 이상 필요 없어진 엔트리는 적절히 제거해줘야 한다.
  4. 스코프(유효 범위)에서 벗어나면 참조도 사라지므로, 굳이 null처리를 남발할 필요는 없고 예외적 상황에서만 사용하면 된다.
profile
개발하는 다람쥐

0개의 댓글