스프링 AOP

Java Instrumentation API

GLaDiDos 2024. 9. 3. 22:15

https://sundaland.tistory.com/75

 

[ ▶ Instrumentation API ]

Instrumentation API는 자바 플랫폼의 java.lang.instrument 패키지에서 제공되는 API로, JVM의 클래스 로딩 및 런타임 동작을 조작할 수 있도록하는 API이며, 이 API 자체는 인터페이스로 제공된다. 이 API는 주로 성능 모니터링, 프로파일링, 코드 커버리지 도구, 그리고 AOP (Aspect-Oriented Programming) 같은 기술을 구현할 때 사용한다. 구현체는 JVM 내부에서 이 API를 구현하고, 이를 통해 자바 에이전트와 같은 도구들이 해당 기능을 사용할 수 있도록 한다.

 

[ ▷ 주요기능 ]

1. ClassFileTransformer

  • ClassFileTransformer는 클래스가 로드되기 전에 바이트코드를 변환할 수 있는 인터페이스이다. 이를 통해 클래스 파일을 로드하거나 정의하는 과정에서 코드를 삽입하거나 수정할 수 있다.
  • 예를 들어, 메서드 호출전 후에 로그를 기록하는 기능을 추가하거나, 메서드 실행 시간을 측정하는 코드를 삽입할 수 있다.

2. 자바 에이전트

  • Instrumentation API를 사용하는 주요 방법 중 하나는 자바 에이전트를 사용하는 것이다. 에이전트는 JVM이 시작될 때 클래스 로더에 ClassFileTransformer를 등록하여, 모든 클래스가 로드되기 전에 바이트코드를 변경할 수 있다.
  • 에이전트는 JVM 시작 시에 -javaagent 옵션을 통해 지정되며, 런타임에 클래스를 조작하는 데 사용된다.

3. Retransformation

  • Instrumentation API는 이미 로드된 클래스를 다시 변환 (retransform)할 수 있는 기능도 제공한다. 이 기능은 클래스가 이미 JVM에 의해 로드된 후에도 바이트코드를 수정할 수 있게 한다.

4. Redefinition

  • RedefineClasses 메서드를 사용하여, JVM에서 실행 중인 클래스의 정의를 새롭게 바꿀 수 있다. 이 기능을 사용하면 기존 클래스의 메서드나 필드의 바이트코드를 새롭게 정의할 수 있다.

 

[ ▷ 구현체 ]

  • JVM 자체가 Instrumentation API의 실제 구현이다. 즉 JVM은 Instrumentation 인터페이스를 구현하고, 자바 에이전트 또는 다른 도구가 이 API를 통해 클래스의 로딩 및 변환 작업을 수행할 수 있게 한다.
  • 작성중
  • 자바 에이전트는 JVM에 의해 제공된 Instrumentation 구현체에 접근하여 클래스 파일의 바이트코드를 조작하거나 변경할 수 있다. 자바 에이전트가 JVM에 로드될 때, JVM은 Instrumentation 구현체를 자바 에이전트의 permain 메서드로 전달된다. 이를 통해 자바 에이전트는 JVM이 관리하는 클래스 로딩 과정에 개입할 수 있다.

Instrumentation API의 구현체는 JVM 자체에 포함되어 있으며, 이 구현체를 통해 자바 에이전트와 같은 도구가 런타임에 클래스 로딩 및 변환 작업을 수행할 수 있다. 자바 에이전트는 JVM에서 제공하는 Instrumentation API 구현체를 활용하여, 애플리케이션의 동작을 런타임에 조작하는 것이다.

 

[ ▷ 사용사례 ]

  • 프로파일링 및 성능 모니터링: 
  • 코드 커버리지 도구: 테스트 실행 중에 어떤 코드가 실행되었는지를 추적하기 위해 바이트코드 수준에서 커버리지 정보를 추가할 수 있다.
  • AOP 구현: 특정 메서드 호출 전후에 횡단 관심사를 적용하기 위해, 메서드 실행 전에 추가 코드를 삽입하는 방식으로 AOP를 구현할 수 있다.
  • 디버깅 도구: 실행 중인 애플리케이션에서 특정 코드 조작을 동적으로 변경하거나 추가하여 버그를 분석할 수 있다.

 

[ ▷ 예시 ]

자바 에이전트와 Instrumentation API를 사용하는 간단 예시는 아래와 같다.

이 예제는 클래스의 메서드 시작 부분에 간단한 로그를 추가하는 방식으로 바이트코드를 변환한다. 이를 위해, ASM 라이브러리를 사용하여 바이트코드를 조작한ㄴ 예시이다.

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;

public class SimpleAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new SimpleTransformer());
    }
}

class SimpleTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        System.out.println("Transforming " + className);

        if (!className.equals("MyTargetClass")) {
            // 타겟 클래스를 확인하여 특정 클래스에만 변환 적용
            return classfileBuffer;
        }

        try {
            ClassReader classReader = new ClassReader(classfileBuffer);
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
            ClassVisitor classVisitor = new MyClassVisitor(Opcodes.ASM9, classWriter);
            classReader.accept(classVisitor, 0);
            return classWriter.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return classfileBuffer;  // 오류가 발생하면 원본 바이트코드를 반환
        }
    }
}

class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, 
    	String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new MyMethodVisitor(Opcodes.ASM9, mv);
    }
}

class MyMethodVisitor extends MethodVisitor {
    public MyMethodVisitor(int api, MethodVisitor methodVisitor) {
        super(api, methodVisitor);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        // 메서드의 시작 부분에 System.out.println("Method entered") 추가
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Method entered");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
        	"(Ljava/lang/String;)V", false);
    }
}
  • ClassReader: 기존 클래스 파일의 바이트코드를 읽는다.
  • ClassWriter: 새로운 바이트코드를 생성한다.
  • ClassVisitor: 클래스의 구조 (메서드, 필드 등)를 방문하여 수정할 수 있도록 하는 클래스이다.
  • MethodVisitor: 메서트의 바이트코드를 수정할 수 있도록 지원하는 클래스이다.
  • MyClassVisitor: 특정 클래스의 메서드를 수정하기 위해 MethodVisitor를 생성한다.
  • MyMethodVisitor: 특정 클래스의 메서드를 수정하여, 메서드가 실행될 때마다 "Method enterd"라는 메시지를 출력한다.

 

[ ▷ 주의 사항 ]

이 코드는 ASM 라이브러리에 의존한다. ASM은 바이트코드 조작을 위한 프레임워크로, 이를 사용하기 위해서는 프로젝트에 ASM 라이브러리를 추가해야 한다. Maven이나 Gradle을 사용하는 경우, 의존성을 추가할 수 있다.

 

▼ Maven

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.2</version>
</dependency>

▼ Gradle

implementation 'org.ow2.asm:asm:9.2'

이렇게 하면 MyTargetClass의 모든 메서드 부분에 "Method enterd"라는 로그가 출력되도록 바이트코드를 수정하게된다.

 

위 예시에서, SimpleAgent 클래스는 자바 에이전트로 사용되며, JVM이 시작될 때 모든 클래스를 로드하기 전에 SimpleTransformer를통해 클래스 바이트코드를 변경할 수 있다.


Instrumentation API는 자바 어플리케이션의 런타임 동작을 동적으로 변경할 수 있는 강력한 도구이다. 이를 통해 개발자는 성능 모니터링, 프로파일링, AOP 등 다양한 목적을 위해 클래스 바이트코드를 변환하고, 애플리케이션의 동작을 세밀하게 제어할 수 있다.


 

 

Java Instrument API vs ASM: https://blank001.tistory.com/60

Java Agent: https://blank001.tistory.com/61

Instrumentation API vs AspectJ: https://blank001.tistory.com/62

Spring instrument library: https://blank001.tistory.com/63