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 |