nathan_H

[Java] 바이트코드 조작 본문

Programming Laguage/Java

[Java] 바이트코드 조작

nathan_H 2020. 5. 8. 16:48

본 내용은 백기선님의 더 자바 강의 듣고 정리한 내용입니다.

바이트 코드 조작


  • 지난 포스팅에서 JVM 구조에서 가장 중요한 요소로 바이트 코드라고 언급 했는데, 자바 코드는 결국 바이트 코드로 변환이 된 후에 바이트 코드를 읽어 실행이 된다. 즉 최종적으로 바이트 코드 내용으로 실행이 되기 때문에 바이트 코드를 조작한다면, 내가 작성한 코드와 다른 결과가 나올 수 있다.
    • 바이트 코드 : JVM이 실행하는 명령어 집합

코드 커버리지


  • 코드 커버리지
    • 테스트 수행 결과를 정량적인 수치를 나타나는 방법으로 소스 코드 중 테스트를 통해 실행된 코드의 비율을 뜻함
  • 실제 코드 커버리지가 실행되는 과정은 바이트 코드을 통해 커버리지 측정이 이루어진다.
    • 바이트코드에서 테스트가 진행되어야 하는 전체 코드 라인을 카운팅 한 후 코드가 실행될 때, 이 중에서 실행된 코드라인을 표기하고 카운팅을 진행

자바에서의 코드 커버리지


  • Jacoco 활용

  • main

package bytecode;

public class Moim {

    int maxNumberOfAttendees;

    int numberOfEnrollment;

    public boolean isEnrollmentFull() {
        if (maxNumberOfAttendees == 0) {
            return false;
        }

        if (numberOfEnrollment < maxNumberOfAttendees) {
            return false;
        }

        return true;
    }
}
  • test
package bytecode;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MoimTest {

    @Test
    public void isFull() {
        Moim moim = new Moim();
        moim.maxNumberOfAttendees =  10;
        moim.numberOfEnrollment = 7;

        assertEquals(false, moim.isEnrollmentFull());
    }

}
  • 테스트 코드 실행시 아래와 같이 코드 커버리지가 측정된 내용이 상세하게 나오게 된다.

스크린샷 2020-05-08 오후 4 46 36

클래스에서 바이트 코드 조작


public class Moja {

    public String pullOut() {
        return "";
    }
}
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.FixedValue;

import java.io.File;
import java.io.IOException;

import static net.bytebuddy.matcher.ElementMatchers.named;

public class Masulsa {

    public static void main(String[] args) {
        try {
            new ByteBuddy().redefine(Moja.class)
                    .method(named("pullOut")).intercept(FixedValue.value("Rabbit"))
                    .make().saveIn(new File("/Users/hongnadan/IdeaProjects/TheJavaStudy/build/classes/java/main/"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println(new Moja().pullOut());
    }
}
  • 모자라는 클래스에 pullOut 에는 아무런 값을 리턴하지 않지만 마술사 클래스에서 bytebuddy 라는 라이브러리를 활용해 바이트 코드를 조작 한 후 실행을 하면 토끼가 찍혀서 나온다.

토끼를 출력하기 위한 실행 순서

  1. 바이트코드 실행
  2. 바이트코드 제거 후 모자 클래스의 pullOut 메소드 실행

그럼 왜 두번 나눠서 실행을 해야할까?

  • 바이트버디를 통해 클래스를 조작시 모자 클래스를 한번 읽어 들이는데, 이때 JVM 메모리에 클래스 로딩이 진행 된 후 모자 클래스를 호출하기 때문에 위 코드를 한번에 실행하게 되면 이미 메모리에 로딩되어 있는 원본 모자 클래스를 참조해 실행이 되기 때문에 변경된 클래스 파일을 참조하지 않게 되기 때문이다.
  • 즉, 바이트코드가 조작된 클래스를 불러오고 싶다면 클래스로더에 클래스를 올리기 전 시점에 바이트코드를 조작해야 해야 한다.

독립적인 바이트코드 조작


  • 위 예시는 클래스 로딩 순서에 의존적인데, java-agent를 만들어 활용하면 클래스 로딩 순서와 관계없이 독립적으로 바이트 코드를 조작해 실행할 수 있다.

Javaagent 실습


  1. 프로젝트 생성 byteBuddy 의존성 추가
<!-- pom.xml -->
    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy</artifactId>
      <version>1.10.8</version>
    </dependency>
  1. javaagent premian 구현
public class MagicianAgent {
      public static void premain(String agentArgs, Instrumentation inst){
          new AgentBuilder.Default()
                  .type(ElementMatchers.any())
                  .transform(new AgentBuilder.Transformer(){

                      @Override
                      public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
                          return builder.method(named("pullOut")).intercept(FixedValue.value("Rabbit"));
                      }
                  }).installOn(inst);
      }
  }
  • premain함수는 byteBuddy에서 제공하는 AgentBuilder를 구현하여 pullOut 메서드를 intercept 하여 Rabbit의 고정값을 리턴하는 내용이다.
  1. agent프로젝트를 패키징
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
          <archive>
            <index>true</index>
            <manifest>
              <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
              <mode>development</mode>
              <url>${project.url}</url>
              <key>value</key>
              <Premain-Class>com.theJava.MagicianAgent</Premain-Class>
              <Can-Redefine-Classes>true</Can-Redefine-Classes>
              <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
  • 여기서 주목할 점은 maven-jar-plugin의 기본 옵션을 제외하고 manifestEntreis 내부에 추가된 Premain-Class, Can-Redefine-Classes, Can-Retransform-Classes 옵션이다.
  1. 패키징
mvn clean pakaging
  1. javaagent 적용

스크린샷 2020-05-08 오후 4 31 04스크린샷 2020-05-08 오후 4 32 55

  • 마지막으로 VM option에 패키징으로 만든 jar파일 경로를 복사한 후 실행을 하면 클래스 로더에 독립적으로 바이트코드를 조작해 실행을 할 수 있다.

javaagent 정리


  • 이 방법은 자바에이전트가 클래스파일을 직접 바꾸는 것이 아닌, 실행 후 JVM에 클래스로딩이 이루어지는 시점에 동작하기 때문에 외부에서 봤을때 변경 내용이 확인되지는 않는다.
  • 이와같이 실제 코드가 변동되거나 직접적으로 관여하지 않는 방식을 Transparent하다고 표현한다고 한다.

그외의 바이트 코드 조작 활용 예시


프로그램 분석

  • 코드에서 버그 찾는 툴
  • 코드 복잡도 계산

클래스 파일 생성

  • 프록시
  • 특정 API호출 접근 제한
  • 스칼라 같은 언어의 컴파일러

그밖에도 자바 소스 코드 건리지 않고 코드 변경이 필요한 여러 경우에 사용할 수 있다.

  • 프로파일러 (newrelic)
  • 최적화
  • 로깅
  • ...

스프링이 컴포넌트 스캔을 하는 방법 (asm)

  • 컴포넌트 스캔으로 빈으로 등록할 후보 클래스 정보를 찾는데 사용
  • ClassPathScanningCandidateComponentProvider -> SimpleMetadataReader
  • ClassReader와 Visitor 사용해서 클래스에 있는 메타 정보를 읽어온다.

마무리


  • 사실 이번 바이트코드 조작이 많이 쓰이지 않고 그렇게 중요하지 않다고 생각이 들 수 있다. 하지만 바이트코드 조작은 많은 곳에서 활용되고 있고 나중에 로우 레벨에 대한 개발이 필요하는 경우에는 자바라는 언어에서는 중요한 요소로 작용한다고 한다.
  • 그리고 실제 바이트코드에 대해 알고 활용한다는 것은 자바를 좀 더 자바 다양하게 활용하고 제대로 활용할 수 있는 요소 중 하나라고 생각이 든다.
Comments