코드 커버리지는 어떻게 측정할까?
- 바이트코드를 사용한 대표적인 예제
- 코드 커버리지? 테스트 코드가 확인한 소스 코드를 %로 나타낸다.
- JaCoCo 사용
- (JaCoCo) https://www.eclemma.org/jacoco/trunk/doc/index.html
- (Test Coverage 논문) http://www.semdesigns.com/Company/Publications/TestCoverage.pdf
- 사용방법
- pom.xml에 플러그인 추가 후 maven 업데이트
<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>
- 메이븐 verify (validation -> compile -> test -> package -> verify)
$ mvn clean verify
-
target\site\jacoco\index.html 파일을 확인하면 패당 패키지에 클래스들의 coverage를 확인할 수 있다.
- 커버리지가 낮은 경우 빌드가 되지 않게 설정할 수 있다.
<execution> <id>jacoco-check</id> <goals> <goal>check</goal> </goals> <configuration> <rules> <rule> <element>PACKAGE</element> <limits> <limit> <counter>LINE</counter> <value>COVEREDRATIO</value> <minimum>0.50</minimum> </limit> </limits> </rule> </rules> </configuration> </execution>
- pom.xml에 플러그인 추가 후 maven 업데이트
- 어떻게 코드 커버리지를 파악할까?
- 가장 간단한 방법: 코드 커버리지에서 파악하여 라인 수를 카운팅과 코드 실행할 때 지나각 코드 라인 수를 카운팅하여 비교
** 참고: https://www.jacoco.org/jacoco/trunk/doc/
바이트 코드 조작
- 바이트 코드 조작 라이브러리
- ASM
- Javassist
- ByteBuddy
- 예제: ByteBuddy 사용
- 아무것도 없는 Moja에서 “Rabbit”을 꺼내는 마술
-
ByteBuddy 의존성 추가
<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.10.8</version> </dependency>
- 빈 Moja.java 파일
public class Moja { public String pullOut() { return ""; } }
public class Magician { public static void main(String[] args) { System.out.println(new Moja().pullOut()); } }
- 바이트버디 사용
public class Magician { public static void main(String[] args) { try { new ByteBuddy().redefine(Moja.class) .method(named("pullOut")).intercept(FixedValue.value("Rabbit")) .make().saveIn(new File("/Users/workspace/HalleHomework/target/classes/")); } catch(IOException e) { e.printStackTrace(); } System.out.println(new Moja().pullOut()); // Rabbit 출력 } }
- 만약 main 메소드에 try문이 적용된 Moja.class를 갖고 싶다면
- main 메소드를 2번 실행한다.
- 1번째 실행할 때에는 컴파일 하듯 Moja.java 파일안에 pullOut 메소드는 ““를 리턴하는 클래스 파일은 “Rabbit”을 리턴하는 클래스 파일로 다시 만든다.
- 아직은 ““를 리턴하는 클래스파일이기 때문에, ““이 출력
- main 메소드가 실행되기 전에 클래스로더에 의해 Moja.class가 메모리에 적재되고 해당 Moja.class는 조작되기 이전이다.
- 2번째 실행으로 “Rabbit” 클래스파일이 존재하기 때문에 “Rabbit”이 출력된다.
- 1번째 실행했을 때 바이트 코드를 조작하고, 2번째 실행했을 때 조작된 바이트코드를 실행하는 것
- Moja.java 파일안에 pullOut 메소드는 ““를 리턴한다.
- Moja.class 파일안에 pullOut 메소드는 “Rabbit”을 리턴한다.
- 수정하여 다시 컴파일 하기 전까지는 Moja.class 파일 안에 pullOut 메소드는 “Rabbit”을 리턴한다.
- 만약 main 메소드에 try문이 적용된 Moja.class를 갖고 싶다면
- Javaagent 활용
- main 메소드를 실행하기 전에 javaagent에 prev-main으로 지정한 메소드를 실행할 수 있다.
- class 파일을 변경하는 것이 아닌 클래스 로더에서 로딩할 때 변경된 바이트코드가 메모리에 적재되도록 한다.
- Transparent
- 예제
- agent 프로젝트 생성 후 ByteBuddy 의존성 추가
<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.10.8</version> </dependency>
- agent 프로젝트에 premain 구현
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); } }
- agent 프로젝트 jar 패키징
- plugin 설정
<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> <!-- pre-main으로 사용할 클래스 설정 --> <Premain-Class>com.study.MagicianAgent</Premain-Class> <!-- class 재정의 가능여부 설정 --> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
- 참고 자료: Apache Maven JAR Plugin(https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html)
- 참고 자료: java.lang.instrument(https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html)
- 패키지
mvn clean pakaging
- plugin 설정
- 클래스 조작 패키지에서 javaagent 적용
- VM Option에 javaagent jar 설정
-javaagent:/Users/workspace/magicianAgent/target/magicianAgent-1.0-SBAOSHOT.jar
- VM Option에 javaagent jar 설정
- agent 프로젝트 생성 후 ByteBuddy 의존성 추가
바이트코드 조작 툴 활용 예시
- 프로그램 분석
- 코드에서 버그 찾는 툴
- 코드 복잡도 계산
- 클래스 파일 생성
- 프록시
- Test에서 Mock은 프록시로 만든다.
- JPA에서 FetchType.LAZY를 하면 해당 객체를 프록시로 만든다.
- 특정 API 호출 접근 제한
- 스칼라 같은 언어의 컴파일러
- 프록시
- 스프링이 컴포넌트 스캔을 하는 방법 (asm)
- 컴포넌트 스캔으로 빈으로 등록할 후보 클래스 정보를 찾는데 사용
- ClassPathScanningCandidateComponentProvider -> SimpleMetadataReader
- ClassReader와 Visitor 사용해서 클래스에 있는 메타 정보를 읽어온다.
** 출처: 더 자바, 코드를 조작하는 다양한 방법, 백기선님