[Java] Reference 객체 1편

HenryHong·3일 전

java

목록 보기
13/15
post-thumbnail

1. 왜 굳이 “참조 객체”까지 써야 할까?

자바의 GC 덕분에 new만 적당히 써도 메모리 관리는 어느 정도 자동으로 돌긴 한다.
하지만 “언제, 어떤 객체를 먼저 수거할지”를 조금 더 미세하게 제어하고 싶을 때가 있다.

  • 캐시는 “메모리가 넉넉할 땐 오래 들고 있다가, 부족하면 과감히 버리고” 싶다.
  • 일회성 키로 맵을 쓰는데, 키 수명이 끝났으면 알아서 엔트리가 제거되면 좋겠다.
  • 객체가 완전히 소멸되기 직전에 “정리 작업(파일 핸들, 네이티브 리소스 해제 등)”을 하고 싶다.

이럴 때 쓰는 게 java.lang.ref 패키지의 참조 객체(Reference Object)들이다.

  • Strong Reference
  • SoftReference
  • WeakReference
  • PhantomReference

이 네 가지가 “GC 입장에서 얼마나 쉽게 버려도 되는 객체인지”를 등급처럼 나눠 준다.


2. 강한 참조 (Strong Reference)

강한 참조는 우리가 평소에 아무 생각 없이 쓰는 그 참조다.

Object obj = new Object(); // 강한 참조
  • 이 참조가 살아 있는 동안, GC는 해당 객체를 절대 수거하지 않는다.
  • 어디선가 이를 계속 들고 있으면, 메모리가 부족해도 끝까지 버티다 OutOfMemoryError가 날 수 있다.

실무 포인트:

  • “커다란 컬렉션/캐시를 static이나 싱글톤으로 들고만 있다가 안 비우면” 대표적인 OOM 원인이 된다.
  • 강한 참조를 끊고 싶으면, 컬렉션에서 제거하거나, 참조를 null로 두거나, 스코프 밖으로 흘려 보내야 한다.

3. 소프트 참조 (SoftReference) – 메모리 민감한 캐시

SoftReference<byte[]> ref = new SoftReference<>(new byte[10 * 1024 * 1024]); // 10MB
byte[] data = ref.get(); // 아직 살아 있으면 참조 가능
  • 메모리가 넉넉할 땐 객체를 계속 유지한다.
  • OOM이 나기 직전 수준으로 메모리가 부족해졌을 때, GC가 이 소프트 참조 대상부터 과감하게 수거한다.
  • 그래서 “있으면 좋고, 없으면 다시 만들면 되는 데이터”에 적합하다.

예: 이미지 캐시

class ImageCache {
    private final Map<String, SoftReference<Image>> cache = new HashMap<>();

    public Image get(String key) {
        SoftReference<Image> ref = cache.get(key);
        Image img = (ref != null) ? ref.get() : null;

        if (img == null) {
            img = loadImage(key);               // 디스크나 네트워크에서 다시 로드
            cache.put(key, new SoftReference<>(img));
        }

        return img;
    }
}
  • 메모리가 여유 있으면 캐시 히트율이 높고,
  • 메모리가 부족해지면 GC가 SoftReference들을 정리해 준다.

4. 약한 참조 (WeakReference) – 자동 청소 + WeakHashMap

WeakReference<Object> ref = new WeakReference<>(new Object());
Object obj = ref.get(); // GC가 수거했으면 여기서 이미 null
  • 소프트 참조보다 더 “가벼운” 참조다.
  • GC가 한 번 돌 때마다, 이 객체를 향한 강한 참조가 더 이상 없다면 무조건 수거한다.
  • 그래서 “객체 수명과 함께 자연스럽게 사라져야 하는 엔트리”를 관리할 때 쓰기 좋다.

대표적인 예가 WeakHashMap이다.

Map<Object, String> map = new WeakHashMap<>();

public void test() {
    Object key = new Object();
    map.put(key, "value");

    System.out.println("before gc: " + map.size()); // 1

    key = null;          // 강한 참조 제거
    System.gc();         // GC 힌트

    // 잠깐 대기하여 GC 기회 제공
    try { Thread.sleep(100); } catch (InterruptedException ignored) {}

    System.out.println("after gc: " + map.size());  // 대부분 0 (엔트리 제거)
}
  • WeakHashMap키를 약한 참조로 들고 있다.
  • 키를 가리키는 강한 참조를 코드에서 끊으면, GC가 해당 키 객체를 수거하면서 맵 엔트리도 같이 제거된다.
  • 이 패턴 덕분에 “라이프사이클이 끝난 키에 묶인 값들”이 자동으로 정리되어 메모리 누수를 줄일 수 있다.

실무 감각:

  • “라이브 객체에 붙은 메타데이터”나 “캐시지만 키 객체의 수명에 종속되어야 하는 값”을 관리할 때 적합하다.
  • 다만 GC 타이밍은 비결정적이므로, 이것만 믿고 “정시 청소”를 기대하면 안 된다.

5. 팬텀 참조 (PhantomReference) – 소멸 직전 후처리

ReferenceQueue<Resource> queue = new ReferenceQueue<>();
PhantomReference<Resource> phantomRef =
        new PhantomReference<>(resource, queue);
  • 팬텀 참조는 조금 성격이 다르다.
  • phantomRef.get()을 호출해도 항상 null이 나온다.
  • 이 참조는 “대상 객체가 실제로 GC에 수거되기 직전에 ReferenceQueue에 들어왔다”는 신호를 받기 위한 용도다.

패턴은 보통 이런 느낌이다.

  1. 어떤 객체를 생성할 때 함께 PhantomReference를 만들어 ReferenceQueue에 등록한다.
  2. 별도의 모니터링 스레드가 queue.remove()로 큐를 감시한다.
  3. 참조가 큐에 들어오면 “이 객체가 곧 메모리에서 완전히 사라진다”는 뜻이므로, 그 시점에 외부 자원 정리/로그 기록 등 후처리 작업을 수행한다.

이게 왜 필요하냐면:

  • finalize()는 이미 비권장(deprecated)이고, 동작 타이밍이 믿을 수 없다.
  • 팬텀 참조 + ReferenceQueue 조합은 “객체 수거 직전 타이밍에 후처리를 붙이고 싶을 때” 쓰는, 보다 예측 가능한 메커니즘이다.

실무에서의 현실적인 사용:

  • 흔히 쓰는 기능은 아니라, 네이티브 메모리/파일 핸들/다른 언어 런타임과 연동하는 저수준 라이브러리나 프레임워크 레벨에서 주로 쓰인다.
  • 일반 비즈니스 코드에서는 Soft/Weak 쪽을 쓸 일이 훨씬 많다.

6. 네 가지 참조 한 눈에 보기

참조 유형GC 수거 시점특징/용도 요약
Strong참조가 있는 한 절대 수거 안 함평소 우리가 쓰는 일반 참조, OOM의 주범이 되기도
Soft메모리가 부족해지면 우선적으로 수거메모리 민감한 캐시, 있으면 좋고 없어도 되는 데이터
WeakGC가 돌 때마다, 강한 참조 없으면 수거WeakHashMap, 자동 엔트리 정리, 메모리 누수 방지
Phantom수거 직전 ReferenceQueue로 통지객체 소멸 직전 후처리, finalize 대체 패턴

다음 시간엔 WeakHashMap·WeakReference 을 실무에서 사용할 때 어떤걸 고려해야 하는지에 대해 알아보겠다.

profile
주니어 백엔드 개발자

0개의 댓글