포스팅을 시작하며

Java Agent는 JVM에서 실행되는 애플리케이션의 바이트코드를 동적으로 조작할 수 있게 해주는 강력한 도구입니다.

이번 포스팅에서는 Java Agent를 활용하여 실제 프로덕션 환경에서 발생할 수 있는 문제들을 해결하는 방법에 대해 심도있게 다뤄보겠습니다.

1. 메서드 실행 시간 모니터링 에이전트 구현

운영 환경에서 특정 메서드의 실행 시간이 느려지는 문제를 탐지하기 위한 모니터링 에이전트를 구현해보겠습니다.

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;
            }
        });
    }
}

2. 메모리 누수 탐지 에이전트

특정 객체의 생성과 소멸을 추적하여 메모리 누수를 탐지하는 에이전트입니다.

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 + "개 존재합니다.");
            }
        }
    }
}

3. 데이터베이스 쿼리 모니터링 에이전트

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를 활용한 바이트코드 조작은 강력한 도구이지만, 신중하게 사용해야 합니다.
특히 운영 환경에서는 충분한 테스트를 거친 후 적용하는 것이 좋습니다.

출처

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글