본문 바로가기
320x100
320x100

문제를 조사하는 방법에는

- 사전조사(Preliminary Investigation)

- 사후조사(Postmortem Investigation)

- 런타임&라이브 조사(Runtime/Live Investigation)

이 있다.

 

이번에는 JVM위에서 작동하는 애플리케이션을 만드는 개발자로서 알아야 할

디버깅, 샘플링, 프로파일링, 모니터링 / GC(개념)

디버깅에 대해서 조금 깊게 알아보려고 한다.


 

# 디버깅

1) Line breakpoint 활용

디버깅이란? 디버깅의 유래에 대해 AWS 문서에서 찾아봤다.

디버깅 유래

https://aws.amazon.com/ko/what-is/debugging/

 

디버깅이란 무엇인가요? - 디버깅 설명 - AWS

컴퓨터 프로그래밍은 추상적이고 개념적인 활동인 만큼, 버그와 오류가 발생하기 마련입니다. 컴퓨터는 전자 신호의 형태로 데이터를 조작합니다. 프로그래밍 언어는 사람이 컴퓨터와 더 효율

aws.amazon.com

디버깅? 디버그? 그거 그냥 IDE(IntelliJ, Eclipse, STS, VSCode, Cursor)에서 벌레 모양으로 생긴 Debug 모드로 실행시키면 되는거 아니야? 하실 수 있다.

조금 더 고급 디버깅에 대해서 알아보자. IDE는 인텔리제이 기준으로 작성됐다.

간단하게 들어온 Integer중에서 짝수는 2를 곱하고 홀수는 그 값을 그대로 더한 결과물을 반환하는 Calculator 클래스를 만들었다

public class Calculator {
    
    public int add(Integer... inputs) {
        int sum = 0;
        for (Integer input : inputs) {
            if (input == null) {
                continue;
            }
            if (input % 2 == 0) {
                sum *= 2;
            } else {
                sum += input;
            }
        }
        return sum;
    }

}

Main 함수는 다음과 같다

public class Main {

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        int result = calculator.add(3, 7, null, 2, 3, 10);
        System.out.println(result);
    }

}

 

잠시 눈을 감고 상상해보자, 이게 만약에 단순 인자값이 6개가 아니라 20000개쯤 되고, 중간에 특정 값에서만 오류가 발생하는 것이다.

다시 말하면, Web Application Server의 백엔드를 만들고 있고, 예상할 수 없는 유저의 입력값을 처리하는데 특정 입력값이 들어오면 중간 연산 결과에서 오류가 나는 상황이라고 가정해보자.

그럼 우리는 System.out.println이나 log를 찍어서 확인해볼 것이다.

public class Calculator {

    public int add(Integer... inputs) {
        int sum = 0;
        for (Integer input : inputs) {
            if (input == null) {
                continue;
            }
            if (input % 2 == 0) {
                sum *= 2;
            } else {
                sum += input;
            }
            System.out.println("input = " + input);
            System.out.println("sum = " + sum);
        }
        return sum;
    }

}

다음과 같이 말이다.

이렇게 하지 말고, Line breakpoint를 사용해보자.

단순사용

이 상태로 놓고 디버그버튼을 클릭해서 실행해보면

실행 / 디버그
멈춤

이렇게 보면 허수다..!

자, Line breakpoint에서 마우스 우클릭을 해서 조건을 걸어보자. sum이 20을 초과하면 멈추게 해봤다.

step into

브레이크가 걸린곳에서 step into로 다음줄로 넘기면 마지막 input 요소가 10이 순회중일때 그 이전에 sum이 23인 것을 볼 수 있었다.

조금 더 고급지게 해보자. 이번에는 이렇게 실행 중간에 멈추지 않고, 내부적으로 input, sum이 출력됐으면 좋겠다.

breakpoint에서 좌측 하단의 Mor를 클릭해 고급 옵션을 살펴보자.

고급 옵션

for문 내부로 들어가야 input 변수를 볼 수 있다. for문의 다음줄에서 다시 옵션을 건드려주자.

고급 옵션

Suspend에서 체크 해제를 해줬고, Condition을 비워주고, Log에 Breakpoint hit 메세지를 체크하고, Evaluate and log에 input, sum을 추가했다. 그리고 디버그 모드로 실행시켜봤다.

결과

한번도 중단되지 않고, 끝까지 실행됐다. 코드가 돌아가면서 if (input == null)의 조건 브랜치로 들어온 순간 콘솔에 Breakpoint reached at ... 이라는 로그가 찍혔고 다음줄에 input, sum값이 출력됐다.

 

 

=> 혹시라도 break point를 걸고 디버그 모드로 실행하고 특정 입력값까지 가기 위해서 반복적으로 step over/into/out을 기계적으로 누르고 있었다면.. 오늘부터 line breakpoint의 condition, suspend, log를 적극 활용해보자!. 이로써 log.debug/info, System.out.println같은 출력문을 추가하고 디버깅 후에 지울 필요가 없어졌다! ^^

 

2) Set Value

아까와 비슷하지만, 이번에는 특정 입력값을 알고 있을 때 즉각적으로 다음 로직을 체크해볼 수 있는 방법이다.

일단 멈춰

아무 옵션도 주지 않은 채 6번째 줄에 break point를 걸어서 디버그 모드로 실행한다.

set value

바꾸고 싶은(테스트 하고 싶은) 변수에서 우클릭을 하고 Set Value를 클릭하고 원하는 값으로 바꾼다

결과

input이 7732138로 바꼈다. inputs 배열에는 없는 값으로 테스트 해볼 수 있다. 실제 웹 어플리케이션의 경우는 "유저가 어떤 필드에 특정 값을 입력했더니 동일한 오류가 발생해요" 라는 버그수정 요청이 CS 팀 등에서 올 수 있는데, 그런 사항을 테스트 해볼 수 있다.

 

3) Stack Frame 조작

이번에는 스택 프레임을 보고 조작하는 방법에 대해서 알아보자.

클래스

main 메서드에서는 A.a();를 호출한다.. 우리는 내부가 어디를 타고 흐르는지 잘 모르지만 

throw를 일으키는 곳에 breakpoint를 걸었다.

그리고 디버그 모드로 실행시켜봤다.

스택 프레임

아래에서 위로 쌓인 스택 프레임을 보면 Main.main() -> A.a() -> B.b() -> D.d() -> C.c() 순서대로 실행된 것을 알 수 있다.

만약에 여기에서 우리가 의심이 가는or보고 싶은 곳만 필터링 하고 싶을때는 어떻게 할까?

D -> C를 오가는 곳은 제외하고 보고 싶을 때 아래처럼 하면 된다.

Reset Frame


D -> C를 호출했기 때문에 D에서 쌓여진 프레임들(D, C)이 지워지고 main A, B만 남았다.

파악할 공간을 줄였기 때문에 A, B의 내부만 조사해보면 될 것 같다.

나도 Rest Frame은 자주 사용하는 기능은 아니다. 그리고 만약에 D, C에 DB I/O(특히 트랜잭션), File I/O를 실행하는 로직이 있었다면 Reset Frame을 한다고 해서 DB 트랜잭션이 소실되거나 Rollback되거나 생겨난 파일이 삭제되지는 않는다. 그 점에 유의하자.

 

4) Remote Debugging(feat. JDWP agent)

원격에 있는 배포되어 있는 서버와 로컬의 IDE를 연결해서 line breakpoint를 걸고 debug mode로 실행하는 방법에 대해서 알아보자.

로컬에서 서버를 띄워서 테스트하면 안되는 이유는 여러가지다..

일단 회사에서 고사양의 맥북을 지급받거나.. 최소 개발하는데 문제가 없는 고사양의 윈도우 노트북을 지급받거나 할 것이다.

내 노트북만 해도 그냥 간단하게 들고다니는 M1 이동용 개인 저사양 맥북인데..

개인 맥북 사양

 

8코어에 16GB 메모리를 갖고 있다. 이것을 AWS에서 구매하려고 보면...

AWS 가격

Ubuntu Pro 기준으로 시간당 0.354 USD라고 한다.

한달 계산

하루는 24시간이고, 한달은 30일 기준으로 했을 때 숨만 쉬어도 한달에 약 37만원이 나간다.

그리고 서비스를 하는 회사에서 AWS, GCP, Azure에 한번쯤 들어가봤다면 작은 서비스는 8GB 메모리에 2~4 vCPU 정도로 운영하는 것을 봤을 것이다.

회사에서 왜 개발 서버를 따로 물리서버를 둬서 구축하려고 하는지, 그리고 왜 실제 테스트는 localhost가 아닌 환경에서 해야되는지 알 수 있다.

개발자가 테스트 코드를 만들어서 하는 건 로직검증을 위한 유닛 테스트나 슬라이스 테스트, 시나리오 테스트 정도여야 한다.

하지만 그렇다고 해서 실제 서비스를 하고 있는 Production환경에서 부하 또는 성능 테스트를 하면 안되고 Production환경과 최대한 비슷한 하드웨어 아키텍처를 가진 서버 + 배포환경이 아닌 DB 환경을 맞춰놓고 테스트를 하는 것이다.

 

원격 디버깅을 하기에 앞서, 예시는 localhost에서 agent를 붙여서 테스트하는 방법으로 포스팅하려고 한다.

먼저 agent란 사용자 또는 다른 프로그램을 대신해서 작동하는 프로그램을 말한다.

요는 java -jar 명령어로 실행할때 agent를 붙이고, 특정 포트로 원격 디버그 요청이 들어오면 그 agent가 대신 실행하게 되는 것이다.

JDWP란 Java Debuggng Wire Protocol)이며 디버깅 프로세스(디버거)와 디버거(IntelliJ/Eclipse)가 서로 주고받는 데이터 포맷을 정의한 프로토콜이다.

 

- 문제 시나리오

API 요청을 하고 받은 응답은 200 OK인데, 응답으로 받은 데이터가 null인 경우

 

원격 디버깅을 하기 위해 먼저 확인해야 할 2가지 사항이 있다.

- 실제로 원격 디버깅을 하려면 사전에 통신이 가능한지 미리 확인해야 한다.(ex: 방화벽)

- 원격에 배포된 버전과 로컬 IDE에서 테스트하려는 버전이 같은지 확인하자.(git/svn)

  ex) git log --oneline 명령어를 사용하면 맨 앞에 해시태그로 버전 비교가 가능하다.

 

[주의] 배포환경과 똑같거나 최대한 비슷한 VM인스턴스 환경에서 실행하자!

java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 app.jar
  • java -jar: java 커맨드로 JAR 앱 파일을 실행
  • -agentlib:jdwp: jdwp 에이전트를 통해 디버거와 통신 채널을 연다
  • transport=dt_socket: 앱과 디버거는 서로 TCP/IP 통신을 한다
  • server=y: 디버거를 리스닝하는 앱에 에이전트를 부착한다
  • suspend=n: 디버거가 부착되길 기다리지 않고 앱을 바로 실행시킨다
  • address=5005: 에이전트가 디버거와 통신하기 위해 여는 포트를 정의한다
  • app.jar : JAR 파일 경로

결과

이 명령어로 실행하면 Spring 로고가 뜨기 전에 Listening for transport dt_socket at address: 5005라는 문구를 확인할 수 있다.

이제 IntelliJ에서 원격 앱에 디버거로 접속하자.

Add New Configuration -> Remote JVM Debug
디버그

이름과 포트번호를 기재 후, Debug 모드로 실행하자

23번째 라인에 breakpoint 추가
api 요청 재현
step over

step over를 통해 확인해보면 catch쪽으로 빠지는 것을 확인할 수 있다.

 

Entity

원인은 로컬DB에서 테스트할때는 quantity에서 데이터를 받아올때 null이 없었지만, 운영서버 DB에서는 null이 있고, 자바 클래스에서 primitive 타입으로 되어있기 때문에 예외가 발생한 것이었다.

 

 

여기까지 디버깅을 하는 여러 방법에 대해서 알아봤다.

사실, 디버깅보다는 샘플링, 프로파일링, 모니터링 등이 더 신기하고 재밌긴 하다!!

다음 글은 샘플링을 통해 원인 범위를 얼추 좁히고, 프로파일링을 통해 진짜 문제를 파악하는 글이 될 것 같다.

JMX, JMH, JFR, JMC, JConsole, VisualVM, JProfiler, Ecllipse Memory Analyzer

CLI를 통한 시스템 모니터링(vm_stat, iostat, top/htop/btop), Heap Dump(jmap), Thread dump(jstack), JVM(jmap)

JVM 설정확인(jps, jcmd, jinfo), VM 옵션(-PrintGC)

요런것들과 pull or push 방식의 모니터링까지 이어서 포스팅하려고 한다!

우리는 Java, Kotlin 개발자이기 이전에 JVM에서 동작하는 프로세스/프로그램을 만드는 개발자라고 생각한다.

JVM 위에서 도는 Spring Application

 

이번글은 디버깅에 대한거라 JVM에 대한 내용이 하나도 안나왔지만........ㅋㅋㅋㅋ(함정카드)

320x100

댓글