본문 바로가기

더 나은 엔지니어가 되기 위해/지금은 안쓰는 자바

[더 자바] 바이트 코드 조작

인프런에서 백기선님의 더 자바, 코드를 조작하는 다양한 방법을 공부하며 개인적으로 정리한 글입니다.

코드 커버리지

코드 커버리지는 내가 작성한 테스트 코드가 내 코드를 얼마나 커버했는지의 정도를 말한다. 즉, 테스트 코드가 내 코드를 구석구석 잘 테스트 했는지를 알 수 있다.

Jacoco 를 이용한 예시

Jacoco 를 통해 코드 커버리지를 측정하는 예시를 살펴보자.
먼저 다음과 같은 간단한 클래스 하나를 만든다.

public class Moim {
  int maxNumOfAttendees;
  int numOfEnrollment;

  public boolean isEnrollmentFull() {
    if (maxNumOfAttendees == 0)
      return false;
    if (numOfEnrollment < maxNumOfAttendees)
      return false;
    return true;
  }
}

이 클래스의 isEnrollmentFull() 이 잘 작동하는지를 확인하기 위해 다음과 같은 테스트 코드를 작성한다.

public class MoimTest {

  @Test
  public void isFull() {
    Moim moim = new Moim();
    moim.maxNumOfAttendees = 100;
    moim.numOfEnrollment = 10;
    Assert.assertFalse(moim.isEnrollmentFull());
  }
}

이제 Jacoco 를 추가하기 위해 다음 build 에 다음을 추가한다.

<build>
  <plugins>
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>0.8.4</version>
      <executions>
        <execution>
          <goals>
            <goal>prepare-agent</goal>
          </goals>
        </execution>
        <execution>
          <id>report</id>
          <phase>prepare-package</phase>
          <goals>
            <goal>report</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

maven verify 를 하면 target/site/jacoco/index.html 이 생성되는 것을 확인할 수 있다. 이 index.html 을 열면 다음과 같이 해당 패키지의 커버리지(%) 가 보인다.

org.example 에 연결된 링크를 타고 들어가면, 클래스와 메쏘드 커버리지도 보인다.

jacoco 는 어떻게 코드 커버리지를 저렇게 세세하게 다 측정할 수 있었을까?
어떤 코드는 실행되고, 안되는지 어떻게 알았을까?
바로 바이트 코드에 접근하여, 바이트 코드 단위로 프로그래밍했기 때문이다.

바이트 코드 조작

바이트코드 조작은 말 그대로 바이트 코드에 어떤 조작을 가하는 것을 말한다.

ByteBuddy 를 이용한 예시

어떻게 바이트 코드에 조작을 가하는지 ByteBuddy 를 통해 알아보자.
먼저 다음과 같은 코드들을 작성한다.

public class Moja {

  public String pullOut() {
    return "";
  }
}
public class Masulsa {

  public static void main(String[] args) {
    System.out.println(new Moja().pullOut());
  }
}

실행하면 빈 문자열이 출력된다. 당연한 결과다.

이제 바이트 코드를 조작해보자.
다음과 같이 Bytebuddy 와 다음 코드를 추가하자.

<dependency>
  <groupId>net.bytebuddy</groupId>
  <artifactId>byte-buddy</artifactId>
  <version>1.10.8</version>
</dependency>
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/heumsi/Desktop/dev/java/maven/target/classes/"));
    } catch (IOException e) {
      e.printStackTrace();
    }

    // System.out.println(new Moja().pullOut());
  }
}

이제 다시 main 을 실행한 뒤, 바이트 조작한 부분은 주석을 치고, 다시 println 찍어보자.

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/heumsi/Desktop/dev/java/maven/target/classes/"));
    // } catch (IOException e) {
    //   e.printStackTrace();
    // }

    System.out.println(new Moja().pullOut());
  }
}
Rabbit!

실행하면 Rabbit! 이 출력된다.

빌드된 target/classes/org/example/Moja.class 을 확인해보면 다음과 같이 ByteBuddy 에 의해 코드가 조작되어 있는 pullOut 메쏘드를 볼 수 있다.

public class Moja {
  public Moja() {
  }

  public String pullOut() {
    return "Rabbit!";
  }
}

Java Agent

위의 예시에서, 바이트 코드 조작부와 조작 대상의 클래스를 사용하는 부분 new Moja().pullOut() 을 동시에 사용할 수 없었다. 이유는, 바이트 코드 조작부에 의하여 빌드된 targetMoja.class 는 조작되겠지만, 그 전에 클래스 로더에 의해 조작되기 전 Moja.class 가 메모리에 먼저 올라가기 때문이다. 즉, new Moja().pullOut() 는 메모리에 이미 올라온, 조작되기 전 Moja.class 를 사용하게 된다.

따라서 조작부를 클래스로더가 프로젝트 내 클래스들을 읽기 전에 먼저 실행되게할 필요가 있는데, 이러한 방법 중 하나로 Agent 를 사용할 수 있다. Agent 는 java 에서 지원하는 스펙 중 하나로, 실행 과정에서 클래스 로더에 거치기 전에 Agent 가 먼저 실행된다. 즉 이 Agent 에 바이트 코드 조작부를 넣으면 클래스 로더는 조작된 Moja.class 를 읽게 되는 것이다.

강의에는 실습이 나오지만, 여기서는 이 정도로만 내용 요약한다.

바이트 코드 조작 활용

바이트 코드를 조작해서 활용하는 예시는 다음과 같다.

  • 프로그램 분석
    • 코드에서 버그 찾는 툴
    • 코드 복잡도 계산
  • 클래스 파일 생성
    • 프록시
    • 특정 API 호출 접근 제한
    • 스칼라 같은 언어의 컴파일러
  • 그밖에도 자바 소스 코드 건리지 않고 코드 변경이 필요한 여러 경우
    • 프로파일러
    • 최적화
  • 로깅

바이트 코드를 조작하는 대표적인 툴들은 아래와 같다.

  • ASM
  • Javassist
  • ByteBuddy
  • CGlib