
Java Agent는 JVM에서 실행되는 애플리케이션의 바이트코드를 동적으로 조작할 수 있게 해주는 강력한 도구입니다.
이번 포스팅에서는 Java Agent를 활용하여 실제 프로덕션 환경에서 발생할 수 있는 문제들을 해결하는 방법에 대해 심도있게 다뤄보겠습니다.
운영 환경에서 특정 메서드의 실행 시간이 느려지는 문제를 탐지하기 위한 모니터링 에이전트를 구현해보겠습니다.
public class PerformanceMonitorAgent {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className.startsWith("com/example")) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
for (CtMethod method : cc.getDeclaredMethods()) {
if (!method.isEmpty()) {
method.addLocalVariable("startTime", CtClass.longType);
method.insertBefore("startTime = System.nanoTime();");
String endBlock =
"long endTime = System.nanoTime();" +
"if ((endTime - startTime) > 1000000) {" + // 1ms 이상 소요되는 경우
" System.out.println(\"[성능 경고] " + className + "." +
method.getName() + " 메서드 실행 시간: \" + " +
" (endTime - startTime) / 1000000.0 + \"ms\");" +
"}";
method.insertAfter(endBlock);
}
}
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
});
}
}
특정 객체의 생성과 소멸을 추적하여 메모리 누수를 탐지하는 에이전트입니다.
public class MemoryLeakDetectorAgent {
private static final Map<String, AtomicInteger> objectCount = new ConcurrentHashMap<>();
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className.startsWith("com/example")) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
// 생성자에 카운터 추가
for (CtConstructor constructor : cc.getDeclaredConstructors()) {
constructor.insertAfter(
"com.example.agent.MemoryLeakDetectorAgent.incrementCount(\"" +
className + "\");"
);
}
// finalize 메서드에 카운터 감소
try {
CtMethod finalize = cc.getDeclaredMethod("finalize");
finalize.insertBefore(
"com.example.agent.MemoryLeakDetectorAgent.decrementCount(\"" +
className + "\");"
);
} catch (NotFoundException e) {
CtMethod finalize = CtNewMethod.make(
"protected void finalize() throws Throwable {" +
" super.finalize();" +
" com.example.agent.MemoryLeakDetectorAgent.decrementCount(\"" +
className + "\");" +
"}", cc);
cc.addMethod(finalize);
}
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
});
// 주기적으로 객체 수 모니터링
Thread monitorThread = new Thread(() -> {
while (true) {
try {
Thread.sleep(60000); // 1분마다 체크
checkForLeaks();
} catch (InterruptedException e) {
break;
}
}
});
monitorThread.setDaemon(true);
monitorThread.start();
}
public static void incrementCount(String className) {
objectCount.computeIfAbsent(className, k -> new AtomicInteger())
.incrementAndGet();
}
public static void decrementCount(String className) {
AtomicInteger count = objectCount.get(className);
if (count != null) {
count.decrementAndGet();
}
}
private static void checkForLeaks() {
for (Map.Entry<String, AtomicInteger> entry : objectCount.entrySet()) {
int count = entry.getValue().get();
if (count > 10000) { // 임계값 설정
System.err.println("[메모리 누수 의심] " + entry.getKey() +
" 클래스의 인스턴스가 " + count + "개 존재합니다.");
}
}
}
}
JDBC 드라이버를 통해 실행되는 모든 SQL 쿼리를 모니터링하고 성능 측정을 수행하는 에이전트입니다.
public class DatabaseMonitorAgent {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className.equals("java/sql/Statement")) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
// executeQuery 메서드 수정
CtMethod executeQuery = cc.getDeclaredMethod("executeQuery");
executeQuery.addLocalVariable("startTime", CtClass.longType);
executeQuery.insertBefore(
"startTime = System.nanoTime();" +
"System.out.println(\"[SQL 실행] \" + $1);"
);
String endBlock =
"long endTime = System.nanoTime();" +
"double executionTime = (endTime - startTime) / 1000000.0;" +
"if (executionTime > 100) {" + // 100ms 이상 소요되는 쿼리 경고
" System.err.println(\"[느린 쿼리 경고] 실행 시간: \" + " +
" executionTime + \"ms\\n쿼리: \" + $1);" +
"}";
executeQuery.insertAfter(endBlock);
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
});
}
}
위에서 작성한 에이전트들을 사용하기 위해서는 다음과 같은 단계를 따르면 됩니다.
1. 에이전트 JAR 파일 생성
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.example.agent.PerformanceMonitorAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
2. 애플리케이션 실행 시 에이전트 적용
java -javaagent:performance-monitor-agent.jar -jar your-application.jar
Java Agent를 활용한 바이트코드 조작은 강력한 도구이지만, 신중하게 사용해야 합니다.
특히 운영 환경에서는 충분한 테스트를 거친 후 적용하는 것이 좋습니다.