개발자로 후회없는 삶 살기

코드 조작 PART.바이트 코드 조작 본문

[백엔드]/[Java | 학습기록]

코드 조작 PART.바이트 코드 조작

몽이장쥰 2023. 9. 30. 16:01

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/the-java-code-manipulation 

 

더 자바, 코드를 조작하는 다양한 방법 - 인프런 | 강의

여러분이 사용하고 있는 많은 자바 라이브러리와 프레임워크가 "어떻게" 이런 기능을 제공할 지 궁금한적 있으신가요? 이번 강좌를 통해 자바가 제공하는 다양한 코드 또는 객체를 조작하는 방

www.inflearn.com

 

본론

바이트 코드를 조작하는 예제를 들어보겠습니다.

 

- 코드 커버리지

현재 신청이 꽉 찼는지 확인하는 메서드가 있습니다. max가 0이면 무한대로 신청을 받는 것이고 현재 신청 수가 max보다 적어도 full이 아닙니다.

 

이에 대한 테스트 코드를 작성합니다. 

 

코드 커버리지란? ✅

테스트 코드가 내 소스 코드를 얼마나 커버하느냐로 얼만큼을 테스트했느냐 얼마나 실행했느냐를 확인하는 것으로 그 중에 요즘 많이 사용하는 것이 Jacoco입니다.

 

jacoco로 실행해보면 리포트를 보여주며 어떤 클래스에서 커버리지가 부족한지 볼 수 있습니다.

 

이 노란색의 의미는 작성한 테스트 코드가 이 부분을 전부 거치지 않았다는 것으로 if문의 경우 False는 거쳤는데 True는 테스트하지 않았음을 의미하니 "더 테스트 코드를 작성해라"는 것을 의미합니다.

 

근데 이런 것을 jacoco가 어떻게 알고 이런 툴을 어떻게 만들 수 있을까요? ✅

바이트 코드를 읽어서 코드 커버리지를 챙겨야 하는 부분을 갯수를 다 세고 코드가 실행이 될 때 그 중 몇개를 지나갔는 지 카운팅을 하고 어디를 지나갔고 어디를 안지나갔는지를 비교해서 보여주는 겁니다. 이렇게 커버리지 툴을 쓰면 테스트 코드가 내 소스 코드의 얼만큼을 확인했는지 알 수 있는데 이게 다 바이트 코드 조작과 관련이 있고 바이트 코드를 조작해서 카운팅 정보와 지나간 정보를 알 수 있는 것입니다. 이런 툴이 어떻게 동작하는지 알아보겠습니다.

 

- 모자에서 토끼를 꺼내는 마술

바이트 코드를 조작하는 것은 정말 막강한 기술입니다. 이런 비어있는 모자에서 조차도 토끼를 꺼낼 수 있습니다. 빈 문자열을 출력했는데 토끼가 콘솔에 찍혀야 합니다. 바이트 코드를 조작할 수 있는 라이브러리중 바이트 버디라는 것을 사용해보겠습니다.


=> 바이트 버디 사용

바이트 버디로 모자를 재정의할 것입니다. pullOut 메서드를 intercept로 가로채서 Rabbit이라는 값을 넣습니다. 이게 지금 바이트 코드를 변경해서 저장을 하는 작업입니다. 이제 새로운 경로에 모자 클래스를 다시 바꿔서 저장하면 끝입니다. 바이트 버디와 print문을 같이 사용하면 안되는데 클래스 로딩이 print가 먼저 되기 때문에 바이트 버디 먼저 실행해야 합니다.

 

-> 실행

바이트 코드를 변경해서 저장하고 실행을 하면 토끼가 나옵니다. 원본 클래스는 변함이 없는데 class 파일의 바이트 코드를 보면 Rabbit으로 바뀌어 있습니다. 이제부터 모자를 쓰면 소스코드와 상관없이 바이트 코드가 바뀌어 있어서 모자 클래스를 다시 컴파일을 하기 전까지는 동일한 클래스 파일을 클래스 로더가 로딩하니 토끼가 계속 나옵니다. 이게 바이트 코드를 조작하는 것입니다.

 

바이트 버디를 실행하면 바이트 코드가 조작이 된 것처럼 jacoco를 실행하면 코드 cnt와 거쳐야 할 것들을 찾아서 정리하도록 코드가 조작이 됐고  그 class 파일을 실행해서 코드 커버리지를 구하는 것입니다.

 

🚨 근데 jacoco는 위 예제처럼 조작 코드를 실행하고 print를 한게 아닌 실행만 했는데 조작과 실행이 다 됐잖아요?

이게 어떻게 되는 것인지 바이트 버디의 코드 조작을 실행하지 않아도 토끼를 꺼내는 예제를 알아봅니다.

 

- javaagent 실습

이번에는 조작을 하고 실행하는게 아니라 print만 할 것이고 이렇게 꺼내기만 하는데 래빗이 나오게 할 것입니다.

 

-> 원리는 이러합니다

버디로 조작하는 코드를 다른 javaagent에 작성하는 것입니다. 바이트 코드를 조작하는 작업을 해줄 agent를 만들 것이고 이게 javaagent입니다.

 

-> 바이트 버디로 javaagent 구현하기

agent는 premain을 바이트 버디 스팩에 나와있는대로 재정의 해야합니다. 여기서 Instrumentation을 사용해서 조작을 하는데 이걸 구현할 때 바이트 버디를 사용할 것입니다.

 

이렇게 다 만든 agent를 jar로 패키징하고

jar 파일이 생성된 경로를 모자를 만드는 프로젝트의 JVM 옵션에 넣고 실행하면 토끼가 나옵니다.

 

이렇게 하면 class 파일이 바뀌었을까요? ✅

안 바뀌었습니다. 이전에 했던 방식은 class 파일 자체를 바꾸는 것인데, 이 방식은 파일 시스템에 있는 클래스 파일을 건드리는게 아닙니다. 

자바 에이전트가 하는 일이 클래스 로딩할 때 적용이 되는데 클래스 로딩한 다음에 메모리에 들어오는 것으로 로딩할 때 자바 에이전트를 거쳐서 변경된 바이트 코드 자체를 읽어오기 때문에 변경된 바이트 코드가 메모리에 들어가고 그대로 동작하는 것입니다. 


즉, 컴파일은 정상적으로 되어서 바이트 코드는 그대로 인데 그 바이트 코드를 클래스 로더가 읽어서 메모리에 올릴 때 에이전트가 동작해서 메모리 내부에서 바뀌는 것입니다. 이러한 것을 기존 코드를 건드리지 않는 비침투적이라고 합니다.

 

- 바이트 코드 조작 툴 활용 예

바이트 코드 조작 툴을 사용하는 예제를 알아봅니다. ASM, 바이트 버디, CGLIB 등 바이트 코드 툴은 여러가지에 사용될 수 있습니다.

 

1. 프로그램 분석

바이트 코드를 쭉 읽으면서 소스코드에 버그는 없는 지 시간 복잡도를 계산합니다.

 

2. 클래스 파일 생성

클래스 파일을 생성해서 대신 실행될 프록시를 만든다던가, 특정 API 호출을 방어하는 것도 가능합니다. 바이트 코드를 조작해서 추가적인 로직을 만족할 때만 해당 메서드를 호출할 수 있게끔 추가적인 로직을 넣을 수 있습니다.

 

3. 프로파일러

어플을 실행할 때 메이전트로 우리가 사용하는 어플이 메모리를 얼마나 쓰고 있는지 또는 쓰레드는 몇 개인지 어떤게 가장 바쁜지 그런 각종 성능 분석을 할 수 있습니다. 그런 툴들이 바이트 코드를 조작해서 자기들이 원하는 정보를 추출하는 것이 필요하며 프로파일러에서도 바이트 조작이 많이 일어납니다.

 

4. 스프링 컴포넌트 스캔

스프링은 컴포넌트 스캔을 할 때 ASM 바이트 코드 조작 툴을 사용합니다. 컴포넌트 스캔 어노테이션이 붙어있으면 그 위치부터 하위 패키지를 뒤져가면서 서비스, 레포 클래스들을 찾아서 빈으로 등록을 해야하는데 이런 특정 에노테이션 정보가 붙은 것을 찾는 과정을 할 때 사용하는 클래스가 ClassPathScanningCandidateComponentProvider 입니다.

 

이 클래스를 보면 asm을 사용하고 있고 메타 데이터를 읽을 때 이는 메타 데이터를 분석하는 ( 바이트 코드 1) 프로그램 분석 ) 기능을 사용하여 특정 어노테이션인지 확인하는데 asm을 활용하는 것입니다. Spring 자바 파일을 클래스 파일로 컴파일하고 클래스 로더로 읽어 드릴 때 ASM이 바이트 코드를 조작해 메타정보로 어노테이션을 감지하고 실행할 때 빈 등록하는 것입니다.

 

5. 프록시

프록시를 만들 때도 많이 사용합니다. CGLib도 바이트 코드 활용 툴입니다. 트랜잭션 시 생기는 프록시 클래스의 try-catch 문도 바이트 코드 조작입니다.

 

결론

정리해보자면 java 파일을 컴파일한 원본 클래스 파일 바이트 코드로 일어들일 때 Rabbit으로 바꾼다던가 코드 커버리지를 카운팅해는 등 바이트 코드를 조작하는 것입니다.

Comments