인프런에서 백기선님의 더 자바, 코드를 조작하는 다양한 방법을 공부하며 개인적으로 정리한 글입니다.
코드 커버리지
코드 커버리지는 내가 작성한 테스트 코드가 내 코드를 얼마나 커버했는지의 정도를 말한다. 즉, 테스트 코드가 내 코드를 구석구석 잘 테스트 했는지를 알 수 있다.
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()
을 동시에 사용할 수 없었다. 이유는, 바이트 코드 조작부에 의하여 빌드된 target
내 Moja.class
는 조작되겠지만, 그 전에 클래스 로더에 의해 조작되기 전 Moja.class
가 메모리에 먼저 올라가기 때문이다. 즉, new Moja().pullOut()
는 메모리에 이미 올라온, 조작되기 전 Moja.class
를 사용하게 된다.
따라서 조작부를 클래스로더가 프로젝트 내 클래스들을 읽기 전에 먼저 실행되게할 필요가 있는데, 이러한 방법 중 하나로 Agent 를 사용할 수 있다. Agent 는 java 에서 지원하는 스펙 중 하나로, 실행 과정에서 클래스 로더에 거치기 전에 Agent 가 먼저 실행된다. 즉 이 Agent 에 바이트 코드 조작부를 넣으면 클래스 로더는 조작된 Moja.class
를 읽게 되는 것이다.
강의에는 실습이 나오지만, 여기서는 이 정도로만 내용 요약한다.
바이트 코드 조작 활용
바이트 코드를 조작해서 활용하는 예시는 다음과 같다.
- 프로그램 분석
- 코드에서 버그 찾는 툴
- 코드 복잡도 계산
- 클래스 파일 생성
- 프록시
- 특정 API 호출 접근 제한
- 스칼라 같은 언어의 컴파일러
- 그밖에도 자바 소스 코드 건리지 않고 코드 변경이 필요한 여러 경우
- 프로파일러
- 최적화
- 로깅
바이트 코드를 조작하는 대표적인 툴들은 아래와 같다.
- ASM
- Javassist
- ByteBuddy
- CGlib
'더 나은 엔지니어가 되기 위해 > 지금은 안쓰는 자바' 카테고리의 다른 글
[스프링 프레임워크 핵심 기술] AOP (0) | 2020.02.23 |
---|---|
[스프링 프레임워크 핵심 기술] SpEL (0) | 2020.02.22 |
[더 자바] JVM 이해하기 (0) | 2020.02.21 |
[스프링 프레임워크 핵심 기술] 데이터 바인딩 추상화 (0) | 2020.02.21 |
[스프링 프레임워크 핵심 기술] Resource / Validation 추상화 (0) | 2020.02.21 |