본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 07. 19:43

AI 에이전트를 활용한 Java 커맨드 라인 디버깅

요약

에이전트 기반 코딩 시대에 IDE의 시각적 디버거 대신 활용할 수 있는 Java 커맨드 라인 디버거(JDB) 사용법을 소개합니다. JDB를 통해 에이전트가 직접 중단점 설정, 단계별 실행, 변수 조회를 수행하며 디버깅 피드백 루프를 가속화하는 방법을 다룹니다.

핵심 포인트

  • 에이전트 기반 코딩에서는 시각적 디버거보다 텍스트 기반 도구가 효율적임
  • JDB를 활용해 실행 중인 JVM에 연결하고 대화형 디버깅 가능
  • 에이전트에게 jdb 명령어 문서를 제공하여 디버깅 스킬 강화 가능
  • 로그 출력 방식보다 빠른 피드백 루프 구축 가능

복잡한 버그를 찾아내야 할 때, 여러분의 첫 번째 본능은 시각적 디버거 (visual debugger)를 찾는 것입니다. 중단점 (breakpoint)을 설정하고, 로직을 단계별로 실행하며, 스택 트레이스 (stack trace)를 조사합니다. 하지만 에이전트 기반 코딩 (agentic coding) 시대에 IDE와 시각적 디버거는 병목 현상을 일으킵니다.

에이전트 기반 코딩 시대에 IDE와 그 시각적 디버거는 더 이상 선택 사항이 아닙니다 🙅

가장 좋은 다음 대안은 무슨 일이 일어나고 있는지 이해하기 위해 빵부스러기 (breadcrumbs) 역할을 할 로그를 추가하는 것입니다.

void processPayment() {
    System.out.println("Entering processPayment() heavy");
    // ...
...

이는 자연스러운 접근 방식이지만, Java에서는 피드백 루프 (feedback loop)가 느립니다. 변경 사항이 생길 때마다 다시 컴파일하고, 패키징하고, 실행해야 하며, 기껏해야 핫 리로드 (hot-reload)를 해야 합니다. 무슨 일이 일어나는지 이해하기 위해 많은 코드 수정을 거치지만, 이러한 출력문 (print statements)은 오직 여러분의 코드 내에서만 작동합니다.

제가 사용하는 방식처럼 제 에이전트도 사용할 수 있는 무언가가 필요합니다.

JDB: Java를 위한 내장 커맨드 라인 디버거

저는 (JDK 1부터 항상 존재해 온) 커맨드 라인 디버거인 jdb를 시도해 보았습니다.

디버거 옵션을 사용하여 실행 중인 JVM에 연결할 수 있습니다:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 MyClass

그런 다음 해당 포트를 사용하여 연결할 수 있습니다:

jdb -attach 5005

이것은 다음과 같은 명령어를 보내는 대화형 도구입니다:

  • 중단점 추가 (add breakpoints): stop at com.saburto.Bar:46 또는 메서드 단위 stop at com.saburto.Bar.getAllLedgers
  • 단계별 실행 (steps): step, step up, stepi, next, cont
  • 변수 정보 가져오기 (get variables info): print, dump, eval, locals, set
  • 스레드 (threads): where, threadgroups, up, down, kill, interrupt
  • 소스 코드 표시 (show source code): 소스 코드 경로를 추가하는 use, 코드를 보여주는 list

코딩 에이전트의 출력 결과에서 소스 코드는 깔끔하게 보입니다.

  42    @GetMapping
  43    public PageResponse<LedgerResponse> getAllLedgers(
  44            @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) {
...

에이전트에게 도구를 알려주세요

Frontier models는 jdb를 호출하는 방법을 스스로 파악할 수 있을 만큼 충분히 지능적이지만, 도움을 주고 싶다면 man jdb를 사용하여 주요 명령어를 문서화하는 스킬 (skill)을 생성해 보세요.

시나리오 (Scenarios)

  1. 요청 페이로드 (request payload) 덤프하기

5005 포트에서 내 앱에 jdb를 연결하고, LedgerController.getAllLedgers에 브레이크포인트 (breakpoint)를 설정한 뒤, curl localhost:8080/api/ledgers를 실행하여 ledgers Page 객체의 모든 필드를 덤프해 줘. 반환된 실제 데이터베이스 레코드를 보고 싶어.

  1. 변수의 상태 변화 (mutations) 추적하기

PaymentService.process에 브레이크포인트를 설정하고, next 명령어로 한 줄씩 실행하면서 매 라인마다 payment 변수를 덤프해 줘. 검증 (validation), 보강 (enrichment), 영속화 (persistence) 과정을 거치면서 상태 (status) 필드가 어떻게 변하는지 보고 싶어.

  1. 체크포인트에서 동시성 (concurrency) 조사하기

ReconciliationScheduler.run에 있는 내 스케줄링 작업의 시작 부분에서 중단하고, where all로 전체 스레드 덤프 (thread dump)를 실행해서 그 순간 다른 모든 스레드가 무엇을 하고 있는지 보여줘. 락 (lock)에 의해 차단된 스레드가 있어?

  1. 그 자리에서 표현식 (expression) 평가하기

InvoiceCalculator의 89번 라인에 브레이크포인트를 설정하고, 중단점에 도달하면 eval invoice.getLineItems().stream().mapToDouble(LineItem::getTotal).sum()을 실행해 줘. 로그 라인을 추가하고, 다시 빌드하고, Maven을 기다릴 필요 없이 라인 아이템 (line-item) 계산을 검증하고 싶어.

  1. 재배포 없이 예외 상태 (exception state) 디버깅하기

PaymentGateway.submit의 catch 블록 내부(203번 라인)에서 중단하고, 잘못된 카드 번호로 결제 요청을 보내줘. 브레이크포인트가 작동하면 예외의 메시지 (message), 원인 체인 (cause chain), 그리고 지역 변수 (local variables)를 덤프해 줘. 게이트웨이가 정확히 무엇을 거부했는지, 그리고 요청이 어떤 상태였는지 보고 싶어.

이러한 프롬프트들은 공통된 형태를 가집니다: 브레이크포인트를 선택하고, 코드 경로 (code path)를 실행한 다음, 에이전트가 데이터를 추출하도록 하는 것입니다. 에이전트가 타이밍, jdb 명령어, 그리고 출력 파싱 (output parsing)을 처리하며, 사용자는 터미널에서 정답을 얻게 됩니다.

간단한 예시

요청 내 변수 확인하기

Java 애플리케이션의 경우, 적절한 인자(arguments)와 함께 시스템을 시작해야 합니다: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar

mvn을 사용하는 spring-boot 애플리케이션의 경우:

mvn spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"

그 다음 포트(port)에 연결(attach)합니다: jdb -attach 5005. 그러면 준비가 끝납니다.

jdb는 설계상 대화형(interactive)이지만, 에이전트 워크플로우(agentic workflow)에서는 디버거가 연결되어 대기하는 동안 HTTP 요청이 발생하도록 세션이 수동 조작 없이(hands-off) 실행되어야 합니다.

[!WARNING]
이것은 제 에이전트가 디버거를 사용하기 위해 스크립트를 생성하는 방식에 대한 예시일 뿐입니다. 사용자의 사례에 따라 다를 수 있으므로, 에이전트가 적절한 스크립트를 생성하도록 하세요.

에이전트는 디버거 프롬프트에 명령어를 대화형으로 입력할 수 없기 때문에, 대신 표준 입력(standard input)을 통해 명령어를 파이프라인(pipeline)으로 전달합니다:

cd ~/projects/my-app && rm -f /tmp/jdb-output.txt && (
  echo "stop in com.saburto.ledger.controller.LedgerController.getAllLedgers"
  echo "cont"
...

이를 자세히 분석해 보겠습니다:

  1. ( echo ... echo ... ) 서브쉘(subshell)은 일련의 jdb 명령어를 디버거로 파이프(pipe)합니다. 각 명령어는 적절한 시점에 실행됩니다: 중단점(breakpoint) 설정, 계속 실행(continue), HTTP 요청이 도착하여 중단점에 걸릴 때까지 5초 대기, 그 후 할당(assignment) 문을 건너뛰고(step over) 장부(ledger) 데이터를 덤프(dump)합니다.
  2. 전체 과정은 백그라운드(&)에서 실행되며, 프로세스 ID(PID)가 캡처됩니다.
  3. 2초의 여유 시간을 둔 후, curlgetAllLedgers()를 실행하는 REST 엔드포인트(endpoint)를 트리거합니다.
  4. 그런 다음 스크립트는 jdb가 종료될 때까지 기다린 후 종료 코드(exit code)를 보고합니다.

에이전트는 그 후에 /tmp/jdb-output.txt를 읽습니다.

next 명령어는 덤프가 실행되기 전에 var ledgers = ... 할당문을 건너뜁니다. sleep 호출은 명령어가 중단점이 작동한 후에 도착하도록 속도를 조절합니다. 에이전트는 출력 파일을 읽고 데이터를 재구성합니다.

이제 각 LedgerEntity 인스턴스를 확인할 수 있습니다:

ledgers.content.elementData[0] = {
    id: instance of java.util.UUID(id=15654)
    name: "Basic Transactions Ledger"
...

[!TIP]
에이전트와 디버거 사이의 가교로 **tmux**를 사용하세요. 에이전트에게 tmux에서 새 패널이나 창을 열도록 지시하고, 코딩 에이전트는 원래의 창에 열어둔 상태에서 모든 디버깅 명령을 그곳으로 보내도록 하세요. 이는 나중에 나올 SQL 추출 과정을 포함하여, 에이전트와 jdb를 병렬로 계속 실행하기 위해 이 포스트 전체에서 사용하는 패턴입니다.

HTTP 요청 헤더 출력하기

다음은 그 예시입니다. 코드 한 줄 건드리지 않고, 에이전트에게 컨트롤러 메서드(controller method)로 들어오는 HTTP 요청 헤더를 보여달라고 요청했습니다. 몇 번의 시도 끝에 에이전트는 제가 정확히 필요로 했던 결과를 가져왔습니다:

LedgerController.getAllLedgers()에서의 요청 헤더 (line 46, bci=0)

 ┌───┬───────────────┬─────────────────────────────────────────────────────────────┐
...

도달 방법 (래퍼 체인, wrapper chain)

DispatcherServlet.doDispatch 프레임 15에 있는 요청 객체(request object)는 5개의 중첩된 .request 필드를 거칩니다:

SecurityContextHolderAwareRequestWrapper
  → HeaderWriterFilter$HeaderWriterRequest
    → (firewall wrapper)
...

코드 변경도, 재빌드도, 재시작도 필요 없습니다. 오직 디버거, 에이전트, 그리고 tmux만 있으면 됩니다.

고급 디버깅: SQL 추적하기

때로는 더 깊이 파고들어야 할 때가 있습니다. 저는 Spring Data JDBC가 전송하는 정확한 SQL을 PostgreSQL 와이어(wire)에서 직접 확인하고 싶었습니다. 저는 높은 수준(LedgerController.getAllLedgers)에서 시작하여 스택을 따라 내려가며, 적절한 지점을 찾을 때까지 중단점(breakpoint)을 시도했습니다:

┌──────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────┬────────┐
│ Layer │ Breakpoint tried │ Result │
├──────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┼────────┤
...

핵심 중단점 (The key breakpoint)

stop in com.zaxxer.hikari.pool.ProxyPreparedStatement.<init>

이 코드는 HikariCP가 prepared statement를 생성할 때마다 실행됩니다. 이 생성자(constructor)는 PostgreSQL 드라이버의 실제 PgPreparedStatement인 statement 매개변수를 받습니다.

드라이버 내부에서 SQL 추출하기

중단점에서, statement 변수는 CachedQuery 타입의 preparedQuery 필드를 가진 PgPreparedStatement입니다:

print statement.preparedQuery
→ CachedQuery{executeCount=10, query=SELECT "ledger_ledgers"."id" AS "id", ... LIMIT 20, isFunction=false}

CachedQuery.toString() 메서드는 편리하게 전체 SQL 문자열을 포함합니다.

tmux 창에 명령어 보내기

jdb가 tmux 창에서 대화형으로 실행되었기 때문에, 저는 다음 명령어를 사용했습니다:

tmux send-keys -t 3 'print statement.preparedQuery' Enter

체인 요약 (Summary of the chain)

curl → Tomcat → Spring MVC → LedgerController.getAllLedgers()
  → LedgerService → PagingAndSortingRepository.findAll(Pageable)
    → Spring Data JDBC → JdbcTemplate
...

최종 응답 (Final Response)

최종 응답은 다음과 같은 형태입니다:

GET /api/v1/ledgers를 위해 실행된 SQL 쿼리

PgPreparedStatementProxyPreparedStatement.<init> 중단점(breakpoint)에서 확인된 내용:

쿼리 1: COUNT (페이지네이션 메타데이터용)
SELECT COUNT(*) FROM "ledger_ledgers"

(이전 실행에서 포착됨: Spring Data JDBC는 PageImpl.totalElements를 채우기 위해 count를 먼저 실행합니다)

쿼리 2: SELECT 데이터 (LIMIT 포함)
SELECT "ledger_ledgers"."id" AS "id",
       "ledger_ledgers"."name" AS "name",
       "ledger_ledgers"."created_at" AS "created_at"

...

결론 (Conclusion)

에이전트 기반 디버깅 (Agentic debugging)은 강력합니다. 완전히 수동 작업 없이도 단 몇 초 만에 Java 시스템 상태에 대한 깊은 통찰을 얻을 수 있습니다. 한 줄씩 실행하기 위해 수동으로 F10을 누르던 시대는 지났습니다.

jdb는 단지 Java를 위한 것이며, 동일한 패턴이 C/C++를 위한 gdb, Python을 위한 pdb, Go를 위한 dlv, 또는 명령 인터페이스 (command interface)를 제공하는 모든 디버거에 적용됩니다. 터미널에서 사용할 수 있다면, 에이전트도 사용할 수 있습니다.

자, 여러분은 여전히 코드를 한 단계씩 실행하기 위해 F9, F10, F11을 누르는 방식을 선호하시나요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0