Logpoint로 타임아웃을 피해 디버깅하기
다른 언어: EnglishEspañolFrançaisDeutsch日本語Português中文
최신 개발 도구, 특히 IntelliJ IDEA에는 디버깅 기능이 워낙 많아서 무엇을 사용할 수 있는지 떠올리는 데에도 시간이 걸립니다. 사실상 어떤 특수한 사용 사례에도 그에 맞는 도구가 있습니다.
이 글에서는 반대쪽에서 디버깅에 접근해 기본을 살펴보려고 합니다. 디버깅 도구를 처음 배우고 있고, 학습에 투자한 시간 대비 가장 큰 효과를 원한다면 가장 먼저 살펴볼 기능은 logpoint입니다.
제가 logpoint를 좋아하는 이유는, 일반 println 문으로 디버깅하는 것만큼 단순하고
(어쩌면 더 단순하면서도),
디버깅할 수 있는 문제의 범위를 크게 넓혀 주기 때문입니다. 어떤 문제에서는 logpoint가 유일하게 실용적인 접근법입니다.
그리고 나머지 경우에도 logpoint는 많은 시간과 노력을 아껴 주는 편의성을 제공합니다.
또한 IntelliJ IDEA 2026.2에서는 logpoint에 몇 가지 아주 멋진 개선 사항이 추가되었으므로, 지금이 다시 정리해 보기 좋은 시점입니다.
문제 설명
여기 미니 클라이언트/서버가 있습니다. 통신에는 gRPC를 사용합니다. 서버에는 일부 tenant에 대해 잘못된 할인 값을 반환하는 버그가 있습니다.
그래서 일반적인 디버깅 흐름을 따라가겠습니다. 문제를 재현하고, 서버 내부 동작을 볼 수 있게 만들고, 문제가 있는 요청을 보낸 다음, 서버가 정확히 어떻게 잘못된 결과를 만드는지 관찰합니다.
버그 재현
다른 환경에서 실행되는 상황을 시뮬레이션하기 위해, 프로젝트에는
리스닝 포트와 디버그 포트가 노출된 Dockerfile이 포함되어 있습니다.
제공된 GrpcQuoteServer in Docker 실행 구성을 사용하거나 명령줄에서 직접 실행할 수 있습니다.
docker build -t grpc-timeout .
docker run --rm -p 50051:50051 -p 5005:5005 grpc-timeout
그런 다음 문제가 있는 요청을 위해 GrpcQuoteClientLoop 실행 구성을 사용합니다. 이 구성은 서버를
주기적으로 쿼리합니다. 덕분에 수동으로 요청을 보내는 일은 잊고 서버에서 무슨 일이 일어나는지에 더 집중할 수 있습니다.
서버와 클라이언트 루프가 모두 실행 중이면 콘솔에는 다음과 같이 표시됩니다.
tenant='JetBrains' region='EMEA' status=OK symbol=IDEA price=100.00 USD source=live detail=region=emea, discount_bps=0
… 기대한 결과는 다음과 같지만요.
tenant='JetBrains' region='EMEA' status=OK symbol=IDEA price=80.00 USD source=live detail=region=emea, discount_bps=2000
서버에 연결하기
서버는 로컬 IntelliJ IDEA 디버그 세션에서 시작된 것은 아니지만, 디버거 연결을 기다리고 있으므로
제공된 GrpcQuoteServer attach 실행 구성을 사용해 여전히 연결할 수 있습니다.
많은 사람이 잘못 이해하는 부분이 하나 있고, 여기서 언급할 가치가 있습니다. 디버거 입장에서는 프로세스가 로컬에서 실행되든, 별도 환경에서 실행되든, 원격 호스트에서 실행되든 차이가 없습니다. 어느 경우든 통신은 socket을 통해 이루어지므로, 이 연습은 위치와 관계없이 어떤 Java 프로세스를 디버깅할 때도 유효합니다.
Logpoints
Logpoint는 println 문으로 디버깅하는 것과 비슷하게 프로그램을 suspend하지 않고
필요한 세부 정보만 콘솔에 기록합니다. println 문과 달리,
애플리케이션을 다시 빌드하거나 배포하지 않고도 바꿀 수 있습니다.
이미 logpoint를 설정하는 방법을 알고 있을 수도 있지만, IntelliJ IDEA 2026.2부터는 새롭고 더 빠른 방법이 생겼습니다.
실행 가능한 두 줄 사이의 gutter를 클릭하고 기록하려는 표현식을 입력하면 됩니다.
시작점으로는 쿼리 처리 메서드의 시작 부분을 사용할 수 있습니다.
( QuoteEndpoint:12 ):
hot path에서 무거운 계산을 조심하세요. 같은 VM에서 실행되며 마법처럼 공짜가 아닙니다. 2026.2부터 IntelliJ IDEA는 instrumentation을 통해 디버거가 도입하는 overhead를 제거하지만, 무거운 logging 표현식은 여전히 실행 시간이 걸릴 수 있습니다.
이제 콘솔은 들어오는 모든 요청마다 다음을 출력합니다.
EMEA JetBrains
이제 요청 루프가 실행 중인 동안, 출력이 버그를 가리킬 때까지 logpoint를 점진적으로 변경하고 추가할 수 있습니다. logpoint를 더 추가하거나 기존 logpoint를 갱신한 뒤, 새 요청이 들어올 때 콘솔에 나타나는 메시지를 보면 됩니다.
호출 체인을 따라가며 초기 의심을 배제한 끝에,
discountBpsFor() 메서드에 도달합니다.
콘솔은 tenant 이름이 제대로 정규화되지 않았음을 보여줍니다.
tenant = JetBrains expected: jetbrains
또한 discount applied가 없다는 것은 올바른 할인이 적용되는 블록에 전혀 들어가지 않았다는 뜻입니다.
tenant 이름을 정규화하면 버그가 수정될 것입니다.
프로 팁: 특정 콘솔 출력이 어디서 나왔는지 확실하지 않을 때는 콘솔에서 해당 줄을 클릭하세요. IntelliJ IDEA가 관련 코드나 logpoint로 이동시켜 줍니다.

logging에 println을 사용하더라도 IntelliJ IDEA의 디버거로 프로세스를 실행 중이라면
이 탐색 기능이 작동합니다.
수정 사항 테스트
여기까지 온 김에 수정 사항을 테스트해 봅시다. Logpoint는 logging을 위한 것이지 프로그램을 수정하기 위한 것은 아니지만, 특정 수정이 어떻게 동작할지 테스트하지 못하게 막는 것은 없습니다.
예상대로 동작합니다.
EMEA JetBrains
jetbrains
discount applied
println이 아닌 이유
아마 logpoint가 좀 더 나은 println 문처럼 보인다고 생각할 수 있습니다. 어떤 의미에서는 맞습니다. 핵심 기법은 같기 때문입니다. 프로그램 실행 방식에 영향을 주지 않는 간단한 방식으로 probe를 추가하는 것입니다.
Logpoint가 더 나은 선택일 수 있는 이유는 여러 가지입니다.
- 코드를 어지럽히지 않으므로 나중에 정리하느라 힘쓸 필요가 없습니다. 실수로 남겨진 문장을 production에 commit할 일도 없습니다.
- 무엇을 언제 기록할지 유연하게 정할 수 있습니다. 예를 들어 빈번한 이벤트를 sampling하고 싶다면, 이렇게 설정합니다.
- 의존성 내부에 logging을 삽입할 수 있습니다. 곧 그렇게 해 보겠습니다.
- 이 시나리오에서 가장 중요하게는, 비용이 큰 redeploy를 줄일 수 있습니다. logging을 추가하려고 로컬 Docker container를 다시 실행하는 일은 장난감 프로젝트에서는 괜찮을 수 있지만, 대규모 실제 프로젝트에서는 그렇지 않은 경우가 많습니다.
그 시점부터 logpoint는 println처럼 느껴지는 것을 멈추고, 전문적인 디버깅 도구처럼 느껴지기 시작합니다.
일반 breakpoint가 아닌 이유
디버거를 사용할 때 대부분의 개발자는 breakpoint를 먼저 찾습니다. 하지만 이 시나리오는 logpoint가 더 잘 맞는 대표적인 경우이며, 단순한 취향의 문제가 아닙니다.
일반 breakpoint를 사용하면 어떤 일이 일어나는지 보겠습니다.
서버에 연결한 뒤 GrpcQuoteServer.java:55 에 line breakpoint를 설정합니다.
루프에서 다음 요청이 오면 서버가 suspend됩니다.
하지만 프로그램 상태를 살펴보고 몇 단계 진행하면, 우리는 cancellation 경로에 들어가게 됩니다.
그곳에 도달한 뒤에는 코드가 다른 실행 경로를 탄 상태입니다. IntelliJ IDEA가 실행되지 않을 코드 부분을 회색으로 표시한 것을 볼 수 있습니다. 문제가 있는 상태를 다시 가져오려면 요청을 하나씩 계속 보내야 하고, timeout 창 안에 디버깅 작업을 맞춰 넣어야 합니다.
이런 일이 생기는 이유는 클라이언트가 원격 호출에 deadline을 설정하기 때문입니다. 일반적인 HTTP/REST 클라이언트 timeout은 클라이언트 쪽에서 실패를 표시하는 데 그치는 반면, gRPC는 클라이언트의 deadline을 서버에 전파할 수 있습니다. 그 결과 클라이언트는 응답 대기를 멈추는 것뿐만 아니라 서버 쪽 작업 자체를 실제로 취소할 수 있습니다.
반면 logpoint는 디버거 UI에서 얻을 수 있는 것과 같은 정보를 콘솔에서 관찰하게 해 줍니다. 중요한 점은 서버를 suspend하지 않는다는 것입니다. 그래서 timeout을 유발하지 않고 필요한 정보를 추출할 수 있습니다.
보너스: timeout 제거
이 시나리오를 다른 방식으로 디버깅하고 싶다면, 또 다른 접근법이 있습니다. 우리 gRPC 예제에서 문제의 핵심은 timeout이었고, logpoint를 사용하면 런타임에 이를 제거할 수 있습니다.
방금 본 것처럼, logpoint 표현식은 side effect를 통해 실행 중인 프로그램을 수정할 수 있습니다. 여기서는 이 기법을 사용해 들어오는 요청을 조정할 수 있습니다.
먼저 timeout을 설정하는 라이브러리 메서드를 찾습니다. 이를 할 수 있는 위치는 여러 곳입니다.
그중 하나가 io.grpc.internal.ServerImpl.createContext 입니다.
그 메서드 안에서 지역 변수 timeoutNanos 가 할당된 직후
그 값을 다시 쓸 수 있습니다.
이 logpoint가 있으면 gRPC timeout 값이 요청 headers에서 읽힐 때마다 즉시 5분짜리 deadline으로 대체됩니다. 즉, 서버를 다시 suspend할 수 있습니다.
reproducer 요청에 대해서만 timeout을 연장하고 서버는 평소처럼 동작하게 두고 싶다면, 예를 들어 공유 staging 인스턴스에서 실행 중인 경우, logpoint 안에 여러 줄의 로직을 사용할 수 있습니다.
logpoint에 복사할 코드는 다음과 같습니다.
Metadata.Key<String> DEBUG_HEADER =
Metadata.Key.of("Debug", Metadata.ASCII_STRING_MARSHALLER);
String debugHeader = headers.get(DEBUG_HEADER);
if ("Debug".equals(debugHeader)) {
timeoutNanos = java.util.concurrent.TimeUnit.MINUTES.toNanos(5L);
return "Timeout reset";
}
이 여러 줄 표현식은 요청 headers를 파싱하고,
Debug header가 있는 요청(테스트 클라이언트가 추가합니다)에 대해서만 timeout을 연장합니다.
다른 요청은 일반 deadline을 유지합니다.
if 분기는 "Timeout reset"을 반환하여 해당 분기를 방문했음을 확인해 줍니다.
Logpoint는 객체에 표시 (Mark Object) 같은 IntelliJ IDEA의 더 고급 기능과도 결합할 수 있습니다. 흥미롭게 들린다면 이 글을 살펴보세요.
물론 이 방법은 라이브러리에 익숙하거나, 그것을 탐색할 시간이 있어야 합니다. 둘 다 없고 런타임 동작을 빠르게 바꾸고 싶기만 하다면, 포함된 AI agent skill을 사용해 작업을 AI agent에 위임할 수 있습니다.
결론
이 글에서는 logpoint가 breakpoint나 println logging보다
더 단순하고 우아한 대안이 되는 사용 사례를 살펴봤습니다. 우리는:
- 원격 프로세스에 연결했고
- logpoint를 사용해 런타임에 진단 정보를 추가했고
- 가설이 바뀔 때마다 logging을 조정했고
- 애플리케이션 코드를 바꾸지 않고 수정 사항을 테스트했고
- 실행 중인 서버의 timeout을 변경했고
- ij-debugger AI agent skill을 사용했습니다
새로운 것을 배웠고, 다음번에 println 문이나 breakpoint가 방해될 때 사용할 수 있는
더 나은 선택지가 생겼기를 바랍니다.
시리즈의 다음 글에서는 새로운 logpoint를 이렇게 빠르게 만드는 기반 메커니즘인
debugger instrumentation이 어떻게 동작하는지 살펴보겠습니다.
즐거운 디버깅 되세요!