[글또]고급 디버깅 기법
들어가며
디버깅이 중요하다는걸 알지만 실질적으로 심도있게 깊게 해본적이 없어서 우연히 유튜브 (포프TV)를 보다가 이번 기회에 깊게 한번 보는 시간을 가져보기 위해 해당 내용을 쓰게 되었습니다.
기본적으로 프로그래머에게 굉장히 중요한 것은 디버깅 능력이라고 저는 봐요. 자기가 코드를 짜요. 사람이 실수 안하는 사람이 없어. 다 버그 있어요. 그 버그를 어떻게 잡냐의 문제거든요. 코딩의 절반은 제가 볼 때 디버깅 실력이라고 봐요. 절반 이상일 수도 있어요. 회사를 어디가도 보통 디버깅하는데 낭비하는 시간, 소비하는 시간이 왠만한 코딩짜는 시간보다 거의 맞먹는다고 봐도 되거든요? 그러면 어떤 사람은 꼭 남의 코드를 줬을 때도 디버깅을 못해서 헤매는 사람이 있는 반면에 디버깅을 남들보다 100배는 잘하는 사람들이 있어요. 그 디버깅을 잘한다는 게 뭐냐면 남의 코드를 잘 읽고요. 그 코드 속에 있는 로직을 따질 줄 알고요. 그 로직을 단계별로 나눌 수 있는 거예요. -출처 포프TV
디버깅
디버깅은 모든 소프트웨어에서 소스 코드의 오류 또는 버그를 찾아서 수정하는 과정
기본적인 기능으로 브레이크 포인트, 스텝 오버, 스텝 인투, 스텝 아웃이 있다.
브레이크 포인트 : 코드에 구현된 로직을 자세히 살펴보고자 디버거가 실행을 중단시킬 코드라인에 표시한 마커이다. 디버거는 브레이크 포인트가 찍힌 라인을 만나면 잠시 실행을 멈춘다.
스텝 오버 : 동일한 메서드에서 다음 코드 라인으로 계속 실행한다.
스텝 인투 : 현재 라인에서 호출된 메서드 중 하나의 내부에서 실행을 계속한다.
스텝 아웃 : 조사하던 메서드를 끝까지 실행하고 호출한 메서드로 돌아간다.
조건부 브레이크포인트
조건부 브레이크포인트는 특정한 조건을 만족할 경우에만 코드 라인에서 앱 실행을 중단시키는 방법이다. 디버거가 어떤 조건을 만족할 경우에만 실행을 중단할 수 있도록 장치한 브레이크 포인트로서 코드의 일부분이 주어진 값과 어떤 연관이 있는지 관심이 있는 경우 유용하다. 조건부 브레이크포인트를 활용하면 앱의 작동 방식을 더 쉽게 이해하고 시간을 절약할 수 있다.
package com.example.security.debugger;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ConditionalBreakpointTest {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Test
public void test() {
String[] input = {"323442039422342", "234231435431513454325"};
int total = 0;
try {
for (String s : input) {
var digits = getDigits(s);
var sum = (Integer) digits.stream().mapToInt(i -> i).sum();
total = sum;
}
} catch (Exception e) {
total = -1;
}
log.info("total : {}", total);
}
public List<Integer> getDigits(String s) {
List<Integer> ret = new ArrayList<>(s.length());
for (int i = 0; i < s.length(); i++) {
ret.set(i, Integer.parseInt(String.valueOf(s.charAt(i))));
}
return ret;
}
}
위는 조건부 디버깅을 확인하기 위한 테스트 케이스로 간단한 테스트이다. 인텔리 제이를 활용하여 sum 부분에 브레이크 포인트를 걸고 조건부 디버깅을 쉽게 걸 수 있다.

브레이크 포인트 위에 우클릭을 통해서 조건부 브레이크포인트의 조건을 사용할 수 있으며 여기에 조건을 걸어서 디버깅 할 수 있다.

이렇게 total 이 0 인 조건을 걸어서 디버깅을 디버깅을 할 수 있다.

이렇게 0일 경우 디버거가 걸리고 실행이 된다.
조건부 브레이크포인트를 잘 활용하면 조사하려는 특정한 케이스를 찾아 헤매지 않고 디버깅을 할 수 있어서 시간을 절약하는데 도움이 된다. 하지만 스코프에 있는 변숫값을 디버거가 지속적으로 가로채서 브레이크포인트 조건을 평가해야 해서 실행 성능에 상당히 큰 영향을 미친다는 단점이 있다.
추가적으로 조건부 브레이크포인트를 걸 때, 더보기를 클릭하게 되면

이런 창이 나오게 되는데 여러가지 표현식 값 또는 특정 조건에 대한 스택 트레이스 등의 세부 정보를 기록하는 기능도 지원한다.
예시로는 인텔리제이와 자바를 예시로 들었지만 다른 IDE(이클립스, 넷빈즈, 비주얼 스튜디오), 언어(nodejs, go)에서도 조건부 프레이크 포인트를 사용할 수 있다.
실행을 중단시키지 않고 브레이크포인트 사용하기
조건부 디버깅에 더보기를 클릭했을 때 창을 보게 되면 일시 중지가 있다.

여기서 일시 중지를 체크해제 하면 중단점에 프로그램이 멈추지 않고 프로그램에 디버깅을 할 수 있다.

위와 같이 설정을 하고 디버깅을 하게 되면

실행 시 멈추지 않으며 실행 종료 후에 스택트레이스와 값을 로그로 출력하는 것을 볼 수 있다.
조사 시나리오 동적으로 변경하기
이 기능은 디버깅 도중 스코프 안에 있는 변숫값을 변경하는 식으로 코드에 대한 조사를 간편하게 해주는 기법이다.
이 방법은 일시 정지를 걸어서 vm 을 멈추게 하고 실행해야 한다.

이렇게 실행을 하게 되면 중단점을 만나게 됐을 때 실행이 멈추게 된다.

디버거는 실행 중단점을 만나게 되면 스코프의 변수를 보여준다.

이 때 변수에서 값 설정 항목을 들어가게 되면 값을 임의로 변경이 가능하다.

조사 케이스 되감기
조사 케이스 되감기는 이미 지나간 코드를 디버거로 되돌려서 재실행하는 기법으로 실행 프레임 드로핑 또는 실행 프레임 퀴팅이라고 한다. 실행 프레임을 드롭한다는 것은, 실행 스택 트레이스에서 한 레이어 뒤로 간다는 뜻이다. 어떤 메서드에 스텝 인투했다가 다시 되돌아가고 싶을 때, 실행 프레임을 삭제하면 메서드가 호출됐던 위치로 돌아가는 것이다.
스텝 아웃은 현재 메서드를 계속 실행하고 그 이전의 메서드로 돌아가는 것이고 프레임 드롭은 메서드를 계속 실행하지 않고 이전 시점으로 돌아간다.

프레임 드롭은 여기서 프레임 재설정이란 키워드로 던지는 것이 가능하다.

여기서 코드를 실행하게 되면 33 줄에서 오류가 발생하게 된다. 리스트의 용량은 설정이 되었지만 리스트의 사이즈는 0이기 때문에 오류가 발생하게 된다. 이 경우 코드를 실행하게 되면 인덱스가 outbound 라서 오류가 나게 되어 스텝오버의 경우 오류가 발생하게 된다.

스텝 오버를 진행했을 때
하지만 프레임 던지기를 했을 경우에는 실행하지 않고 되감기를 하기 때문에 오류가 발생하지 않고 그 전 메소드로 돌아가는 것을 확인할 수 있다.

프레임 던지기를 했을 경우
프레임 던지기의 경우 동일한 케이스를 값을 변경해가며 여러번 실행했을 때 도움이 될 때가 많다. 그럴 때 과거로 되감기 하여 여러가지 케이스를 디버깅 해볼 수 있다.
외부(db, 파일) 등의 로직은 프레임 던지기를 사용하는 것을 권장하지 않는다.
원격 디버깅
원격 디버깅이란, 디버깅 기술을 로컬 시스템 대신 외부 환경에 있는 앱에 적용하는 것이다. 로컬과 다른 환경에서 오작동을 일으키는 경우 원격 디버깅을 통해 해결하기 위해 사용한다.
절대 운영환경에서 사용하지 해선 안 된다. 보안 사고가 일어나거나 서버에 문제가 생길 수 있다.
실행방법
원격 앱
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 app.jar
java -jar : 자바 커맨드로 jar 앱 파일을 실행한다.
-agentlib:jdwp : jdwp 에이전트를 통해 디버거와 통신 채널을 연다.
transport=dt_socket : 앱과 디버거는 서로 TCP/IP 통신을 한다.
server=y : 디버거를 리스닝하는 앱에 에이전트를 부착한다.
suspend=n : 디버거가 부착되길 기다리지 않고 앱을 바로 실행한다. y일 경우, 디버거가 부착될 때까지 앱을 시작하지 않는다.
address=*:5005 : 에이전트가 디버거와 통신하기 위해 여는 포트를 정의한다.
app.jar : jar 파일 경로
로컬 환경

메뉴바에서 더보기 버튼을 클릭하면 구성→ 편집으로 들어갈 수 있다.

여기서 + 버튼을 눌러서 새 구성하기 추가가 나오게 되며 여기서 원격 jvm 디버그를 찾아서 추가해야 한다.


여기서 서버 주소와 포트 번호를 맞추고 실행하게 되면 원격으로 다른 서버의 코드를 디버깅 할 수 있게 되며 로컬에서 브레이크포인트를 찍게 되면 원격 서버가 제어 되게 된다.