Java 클래스로더(Class Loader) 작동 원리와 메모리 누수 방지

지난 포스팅에서는 JVM의 전체적인 아키텍처와 런타임 데이터 영역의 역할들을 살펴보았습니다. 이번 글에서는 그중 첫 번째 관문이자, 많은 개발자가 간과하기 쉬운 클래스로더(Class Loader)에 대해 집중적으로 파헤쳐 봅니다.

Java의 가장 큰 특징 중 하나는 런타임에 클래스를 동적으로 로드한다는 점입니다. 애플리케이션이 실행될 때 모든 코드를 한 번에 메모리에 올리지 않고 필요한 순간에 로드합니다. 이 동적 로딩을 책임지는 클래스로더의 작동 원리를 제대로 이해하지 못하면, 운영 중 원인을 찾기 힘든 메모리 누수(Memory Leak)나 ClassNotFoundException과 같은 치명적인 장애에 직면하게 됩니다.

1. 클래스로더의 3가지 계층 구조

JVM이 클래스를 메모리에 올릴 때 단 하나의 클래스로더가 모든 작업을 수행하지 않습니다. 보안과 효율성을 위해 철저한 계층 구조를 이루고 분업화되어 있습니다.

  • 부트스트랩 클래스로더 (Bootstrap Class Loader): JVM 구동 시 가장 먼저 동작하는 최상위 클래스로더입니다. java.lang 패키지나 java.util과 같은 Java의 가장 핵심적인 표준 라이브러리를 로드합니다.
  • 확장 클래스로더 (Extension Class Loader): 부트스트랩 다음으로 동작하며, Java의 기본 API 외에 추가적으로 제공되는 확장 라이브러리 파일들을 로드합니다. (Java 9 이후부터는 Platform Class Loader로 명칭이 변경되었습니다.)
  • 시스템 클래스로더 (System / Application Class Loader): 개발자가 직접 작성한 애플리케이션 코드나 외부 라이브러리(Classpath에 지정된 파일들)를 로드하는 주체입니다. 우리가 작성한 대부분의 클래스는 이 시스템 클래스로더에 의해 메모리에 올라갑니다.

2. 부모 위임 모델 (Delegation Model)의 이해

클래스로더의 가장 핵심적인 작동 원리는 ‘부모 위임 모델(Parent Delegation Model)’입니다. 특정 클래스를 로드하라는 요청이 들어오면, 시스템 클래스로더는 본인이 직접 찾기 전에 상위 계층인 확장 클래스로더에게 임무를 위임합니다. 확장 클래스로더 역시 최상위인 부트스트랩 클래스로더에게 위임합니다.

  • 안전성과 보안 확보: 이 구조의 최우선 목적은 보안입니다. 만약 악의적인 코드가 담긴 임의의 java.lang.String 클래스를 만들어 배포했다고 가정해 보겠습니다. 부모 위임 모델 덕분에 JVM은 항상 최상위 부트스트랩 클래스로더에서 안전한 표준 String 클래스를 먼저 찾아 로드하게 되므로, 악성 코드가 기존 코드를 덮어씌우는 것을 원천 차단할 수 있습니다.
  • 가시성 원칙: 하위 클래스로더는 상위 클래스로더가 로드한 클래스를 볼 수 있지만, 그 반대는 불가능합니다. 이 원칙이 지켜지지 않으면 런타임 환경에서 클래스 충돌 에러가 발생하게 됩니다.

3. 클래스로더로 인한 메모리 누수 원인과 방지책

일반적인 애플리케이션 객체들은 힙(Heap) 영역에 생성되고 가비지 컬렉터(GC)에 의해 쉽게 소멸하지만, 클래스로더와 연관된 메타데이터는 메타스페이스(Metaspace)라는 별도의 영역에 저장됩니다. 이곳에서 발생하는 메모리 누수는 서버 다운으로 직결될 수 있습니다.

  • 정적(Static) 변수의 오용: 정적 필드가 무거운 객체를 참조하고 있으면, 해당 클래스가 언로드(Unload)되지 않는 한 메모리에서 해제되지 않습니다. 클래스는 자신을 로드한 클래스로더에 대한 참조를 가지며, 클래스로더 역시 자신이 로드한 모든 클래스의 참조를 가집니다. 즉, 잘못된 정적 참조 하나가 전체 클래스로더의 GC를 방해하는 거대한 메모리 누수를 일으킵니다.
  • 웹 컨테이너(Tomcat 등) 재배포 이슈: 톰캣과 같은 WAS에서 애플리케이션을 핫 디플로이(재배포)할 때, 이전 애플리케이션의 클래스로더가 완전히 GC 되지 않고 남아있는 현상이 잦습니다. 이를 방지하기 위해서는 애플리케이션 종료 및 재시작 시 ThreadLocal 변수나 데이터베이스 드라이버 연결 등을 명시적으로 해제(Clean-up)하는 코드를 반드시 작성해야 합니다.

결론: 보이지 않는 곳의 최적화

클래스로더는 개발자가 평소에 직접 제어할 일이 많지 않아 블랙박스처럼 여겨지기 쉽습니다. 하지만 그 계층 구조와 위임 방식을 이해하고, 클래스 언로딩이 실패하는 원인을 파악하는 것은 고급 개발자로 나아가는 필수 관문입니다. 특히 대규모 트래픽을 처리하는 엔터프라이즈 환경에서는 메타스페이스 영역의 메모리 누수 방지가 최우선 과제 중 하나입니다.

다음 포스팅에서는 JVM이 바이트코드를 실행하는 속도를 비약적으로 끌어올려 주는 마법 같은 기술, ‘JIT(Just-In-Time) 컴파일러의 동작 방식과 코드 최적화 기법’에 대해 상세히 다루어 보겠습니다.

댓글 남기기