Notice
Recent Posts
Recent Comments
Link
nathan_H
[Java] 바이트코드 조작 본문
본 내용은 백기선님의 더 자바 강의 듣고 정리한 내용입니다.
바이트 코드 조작
- 지난 포스팅에서 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());
}
}
- 테스트 코드 실행시 아래와 같이 코드 커버리지가 측정된 내용이 상세하게 나오게 된다.
클래스에서 바이트 코드 조작
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
라는 라이브러리를 활용해 바이트 코드를 조작 한 후 실행을 하면 토끼가 찍혀서 나온다.
토끼를 출력하기 위한 실행 순서
- 바이트코드 실행
- 바이트코드 제거 후 모자 클래스의
pullOut
메소드 실행
그럼 왜 두번 나눠서 실행을 해야할까?
- 바이트버디를 통해 클래스를 조작시 모자 클래스를 한번 읽어 들이는데, 이때 JVM 메모리에 클래스 로딩이 진행 된 후 모자 클래스를 호출하기 때문에 위 코드를 한번에 실행하게 되면 이미 메모리에 로딩되어 있는 원본 모자 클래스를 참조해 실행이 되기 때문에 변경된 클래스 파일을 참조하지 않게 되기 때문이다.
- 즉, 바이트코드가 조작된 클래스를 불러오고 싶다면 클래스로더에 클래스를 올리기 전 시점에 바이트코드를 조작해야 해야 한다.
독립적인 바이트코드 조작
- 위 예시는 클래스 로딩 순서에 의존적인데, java-agent를 만들어 활용하면 클래스 로딩 순서와 관계없이 독립적으로 바이트 코드를 조작해 실행할 수 있다.
Javaagent 실습
- 프로젝트 생성 byteBuddy 의존성 추가
<!-- pom.xml -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.8</version>
</dependency>
- 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
의 고정값을 리턴하는 내용이다.
- 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 옵션이다.
- 패키징
mvn clean pakaging
- javaagent 적용
- 마지막으로 VM option에 패키징으로 만든 jar파일 경로를 복사한 후 실행을 하면 클래스 로더에 독립적으로 바이트코드를 조작해 실행을 할 수 있다.
javaagent 정리
- 이 방법은 자바에이전트가 클래스파일을 직접 바꾸는 것이 아닌, 실행 후 JVM에 클래스로딩이 이루어지는 시점에 동작하기 때문에 외부에서 봤을때 변경 내용이 확인되지는 않는다.
- 이와같이 실제 코드가 변동되거나 직접적으로 관여하지 않는 방식을 Transparent하다고 표현한다고 한다.
그외의 바이트 코드 조작 활용 예시
프로그램 분석
- 코드에서 버그 찾는 툴
- 코드 복잡도 계산
클래스 파일 생성
- 프록시
- 특정 API호출 접근 제한
- 스칼라 같은 언어의 컴파일러
그밖에도 자바 소스 코드 건리지 않고 코드 변경이 필요한 여러 경우에 사용할 수 있다.
- 프로파일러 (newrelic)
- 최적화
- 로깅
- ...
스프링이 컴포넌트 스캔을 하는 방법 (asm)
- 컴포넌트 스캔으로 빈으로 등록할 후보 클래스 정보를 찾는데 사용
- ClassPathScanningCandidateComponentProvider -> SimpleMetadataReader
- ClassReader와 Visitor 사용해서 클래스에 있는 메타 정보를 읽어온다.
마무리
- 사실 이번 바이트코드 조작이 많이 쓰이지 않고 그렇게 중요하지 않다고 생각이 들 수 있다. 하지만 바이트코드 조작은 많은 곳에서 활용되고 있고 나중에 로우 레벨에 대한 개발이 필요하는 경우에는 자바라는 언어에서는 중요한 요소로 작용한다고 한다.
- 그리고 실제 바이트코드에 대해 알고 활용한다는 것은 자바를 좀 더 자바 다양하게 활용하고 제대로 활용할 수 있는 요소 중 하나라고 생각이 든다.
'Programming Laguage > Java' 카테고리의 다른 글
ThreadLocal 이란? (2) | 2020.08.29 |
---|---|
인터페이스의 개념과 추상 클래스와의 차이점 (0) | 2020.06.02 |
[Java] Java 애플리케이션에서 JVM 구조와 실행 과정 (0) | 2020.05.05 |
[Java] 상속 핵심 개념과 추상 클래스 (0) | 2020.05.01 |
[Java] 빠르게 정리하는 자바 클래스 (0) | 2020.04.28 |
Comments