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