Java는 Write Once, Run Anywhere(WORA)라는 철학 아래, JVM(Java Virtual Machine)을 통해 플랫폼 독립성을 실현하고 객체 지향 프로그래밍 패러다임을 기반으로 보안성과 안정성을 극대화한 범용 프로그래밍 언어

클래스 로더란 자바 소스 코드를 컴파일하여 만들어진 .class 파일(바이트 코드)을 읽어서 JVM 내부의 메모리 영역에 배치하는 역할을 수행하는 런타임 실행 엔진의 핵심 구성 요소이다.
클래스 로더는 한꺼번에 모든 클래스를 메모리에 올리지 않고, 필요한 시점에 동적으로 로드한다. 이를 위해 세 가지 원칙을 따른다.
상위 클래스 로더에게 로딩을 부탁하고, 없을 때만 본인이 직접 찾는다. (Bootstrap -> Platform -> Application 순서)
만약 위임 모델 원칙이 없을 때 아래와 같은 위험에 노출될 수 있다.
해커가 악의적인 코드를 심은 가짜 java.lang.String 클래스를 만들었다고 가정해보자. 위임 모델이 없으면 애플리케이션은 진짜 자바 기본 클래스 대신 해커의 클래스를 실행할 위험이 있다. 최상의 클래스 로더에게 먼저 로딩을 위임함으로써, 핵심 자바 API가 사용자 정의 클래스에 의해 조작되는 것을 원천 차단할 수 있다.
구체적인 메커니즘
진짜 핵심 API를 건네준다.String을 하위 경로에 숨겨두었어도, 요청이 상위로 먼저 가기 때문에 하위 로더가 가짜를 실행할 기회조차 얻지 못하게 차단한다.가짜 java.lang.String이 실행될 때 대참사
String이 실행된다면 비즈니스에 치명적인 타격을 입는다.하위는 상위를 볼 수 없지만, 상위는 하위를 볼 수 없다.
하위 클래스 로더는 상위 클래스 로더가 로드한 기본 라이브러리(String, Object 등)를 자유롭게 쓸 수 있어야 한다. 하지만 반대로, 자바 핵심 라이브러리(상위)가 사용자가 만든 특정 비즈니스 로직(하위)을 알아야 할 필요도 없고, 알아서도 안된다.
알아서도 안되는 이유
클래스는 로드될 수만 있고, 명시적으로 언로드(삭제)하는 것은 매우 어렵다.
한 번 메모리에 올라간 클래스의 관계(의존성)는 매우 복잡하게 얽혀 있다. 특정 클래스를 임의로 메모리에서 빼버리면(Unload), 그 클래스를 참조하던 다른 객체들이 줄줄이 에러(Null Reference)를 뱉으며 시스템이 붕괴될 수 있다.
필요 없을 때도 메모리에서 안 빼면 동적 로딩의 이점이 없지 않을까?
클래스 하나를 메모리에서 빼려면(Unload), 해당 클래스를 단 한번이라도 참조한 모든 객체와 다른 클래스들의 관계를 전수 조사해야한다. 이 조사 비용이 메모리 몇 MB를 아끼는 것보다 훨씬 비싸고 시스템을 느리게 만든다.
여기서 오해하면 안되는 점은 메모리를 비우는 것이 아니라, 당장 필요 없는 데이터로 인해 실행 자체가 불가능해지는 상황을 방지하고 필요할 때만 비용(로딩 시간)을 지불하는 것에 방점이 찍혀 있다. 한번 로드된 클래스는 시스템 종료 전까지 메모리에 캐시된다고 보면 정확하다.
클래스 로더의 주 역할 중 하나인 동적 로딩은 개발자가 애플리케이션 실행 중 새로운 라이브러리나 클래스를 동적으로 실행 또는 참조하기 위한 역할이다. 쉽게 말해 필요한 시점에만 클래스를 가져오는 기법이다.
OS의 가상 메모리가 요구 페이징을 통해 당장 실행에 필요한 데이터만 물리 메모리에 올리듯, JVM의 클래스 로더 역시 지연 로딩(Lazy Loading) 방식을 사용한다.
예시로 코드에서 new User();라는 구문을 처음 만나는 순간, 클래스 로더가 하드디스크에서 User,class 파일을 찾아 JVM 메모리에 적재한다.
또한 Class.forName("com.mysql.jdbc.Driver")처럼 개발자가 코드상에서 특정 텍스트(이름)로 라이브러리를 런타임에 동적으로 끌어오는 것도 클래스 로더의 역할이다.
메모리 절약
만약 엔터프라이즈급 애플리케이션의 수만 개 클래스를 시작과 동시에 모두 메모리에 올린다면, 심각한 메모리가 낭비될 것이다.
구동 속도 최적화
모든 파일을 디스크에서 읽어 오는 시간으 기다릴 필요 없이, 핵심 클래스만 로드하여 애플리케이션을 빠르게 시작할 수 있다.
웹 앱끼리 서로 간섭하지 못하게 독립적인 공간을 만드는 기법이다.
현업에서는 하나의 서버 (WAS 예 : Tomcat) 안에 여러 개의 웹 서비스(A 쇼핑몰, B 커뮤니티)를 동시에 띄우는 경우가 많다.
문제 상황
A 쇼핑몰은 Spring 프레임워크 3.0 버전을 사용하고, B 커뮤니티는 Spring 5.0 버전을 사용한다고 가정할 때, 두 버전에는 이름이 똑같은 클래스가 무수히 많지만, 내부 코드는 다르다. 만약 클래스 로더가 하나뿐이라면, 이름이 같기 때문에 충돌이 발생하거나 엉뚱한 버전이 실행될 수 있다.
격리의 해결책
WAS는 각 웹 애플리케이션 마다 고유한 자식 클래스 로더를 따로따로 생성한다. A 앱용 클래스 로더와 B 앱용 클래스 로더를 분리하여, 서로의 메모리 공간을 침범하지 않고 이름이 같은 라이브러리라도 버전별로 안전하게 사용할 수 있게 격리 공간을 만들어주는 것이다.
메모리 절약
유연한 확장성
주의사항
ClassNotFoundException이나 NoClassDefFoundError는 대부분 이 클래스 로더의 가시성이나 경로 설정 문제에서 발생한다. 또한 클래스 로딩 자체가 성능 오버헤드가 될 수 있으므로 초기 구동 속도가 중요한 서비스에서는 고려 대상이 된다.
ClassNotFoundException
동적 로딩을 시도했는데 클래스를 못찾을 때 발생한다. 오타가 났거나, 필요한 외부라이브러리(.jar) 파일을 라이브러리 폴더에 넣지 않았을 때 발생)
NoClassDefFoundError
컴파일 할 때(빌드할 때)는 분명히 클래스가 있어서 정상적으로 넘어갔는데, 막상 실행(Runtime)하랴고 보니 파일이 지워졌거나 권한 문제로 읽지 못하는 경우이다. 앞서 이야기한 WAS의 격리 환경에서, A 앱의 클래스 로더가 B 앱에만 존재하는 클래스를 몰래 가져다 쓰려고 할 때 가시성 규약에 막혀 이 에러들이 발생한다.
클래스 로딩의 성능 오버헤드의 의미와 발생 원인
클래스를 메모리에 올리는 작업은 단순히 파일 복사 수준이 아니라 매우 무겁고 복잡한 검증 과정을 거친다.
디스크 I/O 발생
하드디스크나 SSD에서 물리적으로 .class파일을 찾아 읽어오는 작업 자체가 CPU 작업에 비해 엄청나게 느리다.
바이트코드 검증
로드된 파일이 자바 문법과 보안 규칙을 잘 지켰는지, 해킹 코드는 없는지 한줄 한줄 검사한다.
메모리 할당 및 초기화
클래스 내의 static 변수들을 위한 메모리 공간을 확보하고, 초기값을 세팅하는 연산을 수행한다.
이러한 복잡한 연산 때문에, 서버를 갓 구동하여 첫 사용자가 접속했을 때 클래스들이 무더기로 로딩되면서 일시적으로 응답 속도가 뚝 떨어지는 현상이 발생한다.

JIT 컴파일러는 Runtime(실행 시점)에 자주 사용되는 코드(Hotspot)를 기계어로 직접 컴파일하여 캐싱해버리는 것을 의미한다.
자바는 기본적으로 인터프리터 방식(한 줄씩 읽어서 실행)으로 동작한다. 하지만 이는 실행 속도가 느리다는 단점이 있기 때문에 이를 해결하기 위한 기능이다.
JIT 컴파일러는 반복문이 많거나 복잡한 계산이 반복되는 로직에서 성능을 극대화해야 하거나, 대규모 트래픽을 처리하는 서버 사이드 애플리케이션에 매우 효과적으로 사용된다.
프로그램 실행 중 어떤 메서드가 자주 호출되는지 체크한다.
자주 쓰인다고 판단되는 바이트 코드를 통째로 네이티브 기계어로 바꾼다.
변환된 기계어를 코드 캐시에 저장하여 다음부터는 인터프리팅 없이 즉시 실행한다.
초기 실행 시에는 인터프리팅과 컴파일을 동시에 수행하므로 CPU 사용령이 일시적으로 급증하고, 최적의 성능이 나오기까지 시간이 걸린다. 이를 예열(Warm-up) 과정이라고 부른다.
코드가 너무 방대하면 기계어로 변환된 코드가 담긴 메모리(Code Cache)가 부족해져 오히려 성능이 급락할 수 있다.