본문 바로가기

프로젝트/APM prototype 개발

개발 진행 : 메서드 변조(MethodVisitor, ClassVisitor), 로깅

목차

    PrepareStatement Search

    transformer를 통해 prepareStatement를 조회해보자.

    public class MyClassTransformer implements ClassFileTransformer {
    
        //이 인터페이스의 구현체(transform)은 JVM이 존재하는 클래스를 로드할 때마다 호출되며, 이 시점에서 바이트코드를 조사하고 변경할 수 있다.
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    
            if (className.contains("PreparedStatement") || className.contains("preparedStatement") ) {
                System.out.println("Detected PreparedStatement implementation: " + className);
                // Apply bytecode manipulation logic here
            }
    
            return classfileBuffer;
        }

     

     

     

    다음과 같은 경로 중 ProxyPreparedStatement를 바이트코드 조작하는 것이 낫다는 결론을 지었다.

    이 클래스는 PreparedStatement의 프록시 구현으로, 실제 SQL 쿼리 실행 메서드(execute, executeQuery, executeUpdate 등)를 오버라이딩한다. HikariCP 커넥션 풀 라이브러리에 의해 사용되며, SQL 쿼리 실행 시점을 가로채는 데 이상적인 지점을 제공한다.

     

    이 중 execute, executeQuery, executeUpdate 메서드 에 바이트코드를 삽입하여 SQL 쿼리 실행 전후에 트랜잭션 데이터를 캡처하고 로깅 또는 외부 시스템으로 전송하는 로직을 추가하는 것이 이상적이다.

     

     

     

    코드 적용(Agent)

    package org.agent.util.asm.testcode;
    
    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    
    /**
     * 테스트용 ClassVisitor 구현체
     * psmt 변조
     */
    public class PreparedStatementModifyVisitor extends ClassVisitor {
        private String currentClassName;
    
        public PreparedStatementModifyVisitor(ClassVisitor cv) {
            // 부모 클래스의 생성자에 ASM API 버전과 ClassVisitor 인스턴스(ClassWriter)를 전달
            super(Opcodes.ASM9, cv);
        }
    
        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            currentClassName = name.replace('/', ',');
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    
            // execute, executeQuery, executeUpdate 메서드 확인
            if ("execute".equals(name) || "executeQuery".equals(name) || "executeUpdate".equals(name)) {
                System.out.println("check method : " + name);
                
                return new AddLogVisitor(mv, name, currentClassName);
            }
    
            return mv; // 기본 MethodVisitor 반환
        }
    
        //메서드 최상단에 로그 생성
        private static class AddLogVisitor extends MethodVisitor {
            private final String methodName;
            private final String className;
    
            public AddLogVisitor(MethodVisitor mv, String methodName, String className) {
                super(Opcodes.ASM9, mv);
                this.methodName = methodName;
                this.className = className;
            }
    
            @Override
            public void visitCode() {
                super.visitCode();
    
                /*
                System.out.println("Method start: ")
                - Opcodes.GETSTATIC : 정적 필드 접근 / "out"은 접근하려는 필드 / "Ljava/io/PrintStream;"은 필드의 타입
                - mv.visitLdcInsn("Method start: "); - 상수 풀에서 문자열 상수를 로드하는 바이트코드 생성
                - Opcodes.INVOKEVIRTUAL : 가상 메서드 호출 / "java/io/PrintStream" : 메서드가 속한 클래스의 내부 이름 / println : 메서드 / 서술자
                 */
    
                mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Method start : " + methodName + " \t Class name : " + className);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
        }
    
    }

     

    바이트코드가 삽입된 .class 파일(파싱)

     

    check method : executeQuery
    check method : executeUpdate
    2024-02-06T16:58:26.822+09:00  INFO 19760 --- [nio-8080-exec-5] org.agent.util.asm.AsmCodeFactory        : done print
    Method start : executeUpdate 	 Class name : com,zaxxer,hikari,pool,HikariProxyPreparedStatement
    Method start : executeUpdate 	 Class name : com,zaxxer,hikari,pool,ProxyPreparedStatement
    2024-02-06T16:58:31.428+09:00  WARN 19760 --- [nio-8080-exec-1] c.d.j.restapi.service.SqlServiceImpl     : SQL insert, nowTime = 2024-02-06T16:58:31.428271200
    Hibernate: insert into sql_dummy (etc,send_time) values (?,?)
    Method start : executeUpdate 	 Class name : com,zaxxer,hikari,pool,HikariProxyPreparedStatement
    Method start : executeUpdate 	 Class name : com,zaxxer,hikari,pool,ProxyPreparedStatement
    2024-02-06T16:58:36.434+09:00  WARN 19760 --- [nio-8080-exec-4] c.d.j.restapi.service.SqlServiceImpl     : SQL insert, nowTime = 2024-02-06T16:58:36.434740600
    Hibernate: insert into sql_dummy (etc,send_time) values (?,?)
    Method start : executeUpdate 	 Class name : com,zaxxer,hikari,pool,HikariProxyPreparedStatement
    Method start : executeUpdate 	 Class name : com,zaxxer,hikari,pool,ProxyPreparedStatement
    2024-02-06T16:58:41.123+09:00  INFO 19760 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
    2024-02-06T16:58:41.126+09:00  INFO 19760 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 -

     

    로그 확인시 executeUpdate 메서드가 ProxyPreparedStatement HikariProxyPreparedStatement에서 각각 호출되고 있음을 알 수 있다.

     

    이것의 이유는 다음과 같다.

     

    - 프록시 체이닝

    HikariProxyPreparedStatement는 ProxyPreparedStatement를 상속받아 구현된다고 한다.

    그렇기에 executeUpdate가 호출되면, 먼저 HikariProxyPreparedStatement의 오버라이드된 메서드가 호출되고, 이 메서드 내부에서 super.executeUpdate() 형식으로 부모 클래스인 ProxyPreparedStatement의 executeUpdate 메서드를 명시적으로 호출된다.

     

    - 데코레이터 패턴

    HikariCP는 데코레이터 패턴을 사용하여 JDBC 커넥션과 관련된 클래스들을 래핑할 수 있다. 이 패턴은 각각의 프록시 클래스가 동작을 수행한 후 다음 프록시 또는 실제 대상 객체로 작업을 전달하는 방식으로 작동다.

     

    한계

    하지만 이는 SQL 쿼리문을 직접 볼 수는 없다.

    표준 라이브러리에서 실쿼리인 executeQuery()를 확인할 수 있는 방법이 없기 때문이다. 이를 위해 다른 방법을 고민해야 한다.

     

     

    이슈

     

    생성자 super 선언 : 

    super.visit(version, access, name, signature, superName, interfaces); .. 등

    super를 호출하지 않는다면, ASM이 내부적으로 수행해야 하는 작업들이 실행되지 않아 방문자 패턴(visitor pattern)이 제대로 동작하지 않을 수 있다.

     

    이는 visitor가 방문자 패턴이자, 체이닝 방식으로 동작하기 때문인데, 한 방문자(visitor)가 작업을 수행한 후 다음 방문자로 작업을 넘겨주는 방식으로 설계되어 있다.

    super를 호출하지 않으면, 체인 중 다음 방문자로의 작업 전달이 누락되어 문제가 발생할 수 있다.