09. 클래스 로딩과 실행 서브시스템, 사례와 실전
1. 톰캣 : 정통 클래스 로더 아키텍처
- 자바 웹서버(톰캣, 제티, 웹로직,,) 은 모두 자체 정의한 클래스 로더를 사용한다.
- 서버는 개별 애플리케이션이 클래스 라이브러리를 독립적으로 사용할 수 있도록 보장해야한다. (각 애플리케이션이 각자의 라이브러리를 사용)
- 반대로 똑같은 서버에 배포된 둘 이상의 웹 애플리케이션이 사용하는 자바 클래스 라이브러리는 서로 공유될수도 있다. (공유되지않는다면, VM의 메서드 영역이 과도하게 확장될 위험)
- 보안상의 이유로 서버가 사용하는 클래스 라이브러리는 애플리케이션의 클래스 라이브러리와 독립적이어야한다.
- JSP는 핫스왑이 지원되어야하는데, 클래스로더를 바꿔치는방식으로 구현된다.
- 다양한 웹서버에서 서드파티 클래스 라이브러리 저장용으로 별도의 classpath를 제공하기도 한다.
- lib
- classes
Tomcat 5 까지의 ClassLoader 구조
디렉토리 경로 |
설명 |
라이브러리 적용 범위 |
클래스 로더 |
|
CATALINA_HOME/common/classes |
공통 클래스 디렉토리 |
톰캣 및 모든 웹앱에서 공유 |
Common ClassLoader |
|
CATALINA_HOME/common/lib |
공통 라이브러리 (JAR) 디렉토리 |
톰캣 및 모든 웹앱에서 공유 |
Common ClassLoader |
|
CATALINA_HOME/server/classes |
서버 전용 클래스 디렉토리 |
톰캣 서버 내부에서만 사용 |
Server ClassLoader |
|
CATALINA_HOME/server/lib |
서버 전용 라이브러리 디렉토리 |
톰캣 서버 내부에서만 사용 |
Server ClassLoader |
|
CATALINA_HOME/shared/classes |
공유 클래스 디렉토리 (웹앱 간 공유용) |
모든 웹앱에서 공유, 톰캣 서버에서는 사용하지 않음 |
Shared ClassLoader |
|
CATALINA_BASE/webapps/앱명/WEB-INF/classes |
애플리케이션 클래스 디렉토리 |
해당 웹앱에서만 사용 가능 |
Webapp ClassLoader |
|
CATALINA_BASE/webapps/앱명/WEB-INF/lib |
애플리케이션 라이브러리 디렉토리 (JAR) |
해당 웹앱에서만 사용 가능 |
Webapp ClassLoader |
hared ClassLoader |
CATALINA_BASE/webapps/앱명/WEB-INF/lib |
애플리케이션 라이브러리 디렉토리 (JAR) |
해당 웹앱에서만 사용 가능 |
Webapp ClassLoader |
|
graph TD
A[Bootstrap ClassLoader]
B[System JVM ClassLoader]
C[Common ClassLoader]
D1[Server ClassLoader = CatalinaClassLoader]
D2[Shared ClassLoader]
E1[Webapp ClassLoader 앱1]
E2[Webapp ClassLoader 앱2]
A --> B
B --> C
C --> D1
C --> D2
D2 --> E1
D2 --> E2
클래스 로더 |
주 역할 |
사용 주체 |
Common ClassLoader |
모든 웹앱 + 톰캣 공용 라이브러리 로딩 |
톰캣, 모든 웹앱 |
Server ClassLoader |
톰캣 서버 내부 코드 전용 |
톰캣 전용 |
Shared ClassLoader |
모든 웹앱에서 공유되는 클래스/라이브러리 |
웹 애플리케이션 |
Webapp ClassLoader |
개별 웹앱 전용, 클래스 격리 |
해당 웹 애플리케이션 전용 |
Tomcat 6 부터
디렉토리 경로 |
설명 |
라이브러리 적용 범위 |
클래스 로더 |
CATALINA_HOME/bin/ |
실행 스크립트 및 부트스트랩 클래스 (startup.sh, catalina.bat) |
톰캣 실행용 내부 코드 |
Bootstrap ClassLoader |
CATALINA_HOME/lib/ |
공통 라이브러리 JAR 위치 (예: servlet-api.jar, tomcat-util.jar) |
톰캣 전체 및 모든 웹앱에서 공유 |
Common ClassLoader |
CATALINA_HOME/conf/ |
설정 파일 저장소 (server.xml, web.xml, context.xml 등) |
- |
- |
CATALINA_BASE/webapps/앱명/WEB-INF/classes/ |
개별 웹앱의 클래스 파일 위치 |
해당 웹앱 전용 |
Webapp ClassLoader |
CATALINA_BASE/webapps/앱명/WEB-INF/lib/ |
개별 웹앱의 JAR 라이브러리 위치 |
해당 웹앱 전용 |
Webapp ClassLoader |
CATALINA_BASE/work/ |
JSP 등 컴파일 결과 저장 |
- |
- |
CATALINA_BASE/logs/ |
톰캣 로그 저장 디렉토리 |
- |
- |
CATALINA_BASE/temp/ |
임시 파일 저장소 (톰캣 내부에서 사용) |
- |
- |
질문
여러 웹 애플리케이션이 있다면, 스프링은 common이나 shared에 올라가고, 웹 애플리케이션은 webapps/WEB-INF/ 에 올라갈것. 스프링에서 사용자 프로그램클래스에 어떻게 접근하는가?
- 7장 : 부모 클래스로더가 사용자 코드를 다시 호출해야하는경우 -> 스레드별 콘텍스트 클래스 로더 도입으로 해결.
2. OSGi : 유연한 클래스 로더 아키텍처
3. 바이트 코드 생성 기술과 동적 프락시 구현
- 바이트 코드를 생성하는 예
- JSP 컴파일러
- 컴파일 타임에 코드를 변경해주는 AOP 프레임워크
- 동적 프락시 기술
java.lang.reflect.InvokeHandler
ProxyGenerator
- ‘동적’ 은 자바코드를 사용해 프락시 클래스의 ‘정적’ 프락시를 작성하는일.
- reflection 최적화
- 동적 프락시 예
interface IHello {
void sayHello();
}
class Hello implements IHello{
void sayHello(){
println("hello world")
}
}
class DynamicProxy implements InvocationHandler{
Object bind(Object obj){
return Proxy.newProxyInstance(obj.class.getClassLoader(),obj.class.getInterface(),this)
}
@Override
Object invoke(Object proxy,Method method ,Object[] args) throws Throwable{
println("welcome")
return method.invoke(originalObj,args)
}
}
main() {
IHello hello = (Ihello) new DynamicProxy().bind(new Hello())
hello.sayHello() //Welcome, hello world
}
- DynamicProxy가 IHello를 Implements 하는
$Proxy0.class
를 만들어낸다.
4. 백포트 도구 : 자바의 타임머신
- 상위버전 JDK로 작성된 코드를 하위버전 JDK 환경에 배포하는것.
- retroTranslator, retroLambda, Jable,,
- JDK 버전 업그레이드 유형별 대응방식
- 자바 클래스 라이브러리 API 개선 : 구버전 API로 라이브러리 구현
- 프런트엔드 컴파일러 수준의 개선 : 바이트코드를 조작하여 지원.
- 편의 문법 : 컴파일러 수준에서 기존 코드로 변환해줄 수 있는 구분개선
- ex) varargs -> 실제로는 array로 javac 가 변환
- 백포트 사례 : enum은 컴파일 후 class, interface 타입 상수로 표현되므로, 구버전에서 구현가능.
- 바이트코드 차원에서의 지원이 필요한 변경 : invokedynamic은 효율을 위한수단, 효율은 떨어지더라도 기존 바이트코드 메서드로 구현가능.
- ex) JDK7부터 추가된 invokedynamic
- JDK구조 전반 개선 : 불가능하거나 효율떨어지거나
- 가상머신 내부 개선 (GC) : 불가능하거나 효율떨어지거나
3. 실전 : 원격 실행 기능 직접 구현하기
예시코드
- 원격서버에서 특정 클래스에서 실행하는 system.out, system.err 를 다른 서버에서 받아보는 예제.
- 대상 클래스의 byte[] 를 받아서
- ClassModifier 로 클래스로 로딩한뒤
- 신규 클래스에서 system.out 의 상수값을 HackSystem으로 바꾸어 system.out, system.err 를 받아볼 수 있게 하고,
- HotSwapClassLoader 로 바이트를 클래스로 올려
- reflection으로 올린 클래스의 메서드를 실행.
classDiagram
class DynamicProxyTest {
+main(String[] args)
}
class JavaclassExecuter {
+execute(byte[] classBytes): String
}
class HotSwapClassLoader {
+addClass(String name, byte[] bytes)
+findClass(String name): Class<?>
}
class HackSystem {
+setOut(PrintStream)
+setErr(PrintStream)
+setIn(InputStream)
+getBufferString(): String
+clearBuffer()
}
class ClassModifier {
-classBytes: byte[]
+modifyUTF8(String oldStr, String newStr): byte[]
}
class ByteUtils {
+intToBytes(int value, int len): byte[]
+bytesToInt(byte[] b, int start, int len): int
+mergeBytes(byte[]... bytes): byte[]
}
%% Relationships (Dependencies)
DynamicProxyTest --> JavaclassExecuter : uses
JavaclassExecuter --> HackSystem : uses
JavaclassExecuter --> HotSwapClassLoader : uses
ClassModifier --> ByteUtils : uses
%% Optional: group utility classes
%% Utility classes
class HackSystem
class ByteUtils