Skip to content
Published on

원격 디버깅 실전 가이드: SSH 터널 · VS Code · IntelliJ · PyCharm으로 Java·Node·Python·Go 원격 Attach

Authors
  • Name
    Twitter

이 글은 디버깅 실전 시리즈 5편 중 5편이다.

  1. 언어별 디버깅 가이드
  2. 프레임워크별 디버깅 실전
  3. IDE별 디버깅 완전정리
  4. 언어×프레임워크 장애 사례집
  5. 원격 디버깅 실전 가이드 ← 현재 글

원격 디버깅이 필요한 순간

로컬에서 재현되지 않는 버그는 반드시 온다. 스테이징 전용 데이터, 운영 환경 고유의 네트워크 토폴로지, 특정 리전에서만 발생하는 타이밍 이슈 등 원인은 다양하다. 로그만으로 해결되지 않을 때 원격 디버깅(Remote Debugging)이 최후의 카드가 된다.

하지만 원격 디버깅은 양날의 검이다. 디버그 포트가 외부에 노출되면 RCE(Remote Code Execution) 취약점이 되고, 운영 프로세스를 suspend 상태로 멈추면 장애가 확대된다. 이 글에서는 안전하게 원격 디버거를 붙이고, 세션을 끝낸 뒤 깨끗하게 원복하는 전체 흐름을 정리한다.


1. 보안 원칙 — 원격 디버깅의 전제 조건

원격 디버깅은 보안 사고와 장애 확산 위험을 동시에 안고 있다. 아래 원칙을 팀 표준으로 정해 두어야 한다.

1-1. 디버그 포트 외부 공개 금지

디버그 프로토콜(JDWP, Chrome DevTools Protocol, DAP 등)은 인증 없이 코드 실행이 가능하다. 방화벽/Security Group에서 디버그 포트를 절대 퍼블릭으로 열지 않는다.

# 잘못된 예: 0.0.0.0으로 바인딩 → 모든 인터페이스에서 접근 가능
java -agentlib:jdwp=transport=dt_socket,server=y,address=0.0.0.0:5005,suspend=n -jar app.jar

# 올바른 예: localhost에만 바인딩
java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005,suspend=n -jar app.jar

1-2. SSH 터널 우선 원칙

로컬 IDE와 원격 서버 사이에는 반드시 SSH 터널 또는 VPN을 경유한다.

# 로컬 5005 → 원격 서버 127.0.0.1:5005 로 터널
ssh -N -L 5005:127.0.0.1:5005 deploy@staging-server

# 백그라운드 실행 + 자동 재연결
ssh -f -N -L 5005:127.0.0.1:5005 deploy@staging-server
# -f: 백그라운드, -N: 명령 실행 안 함

-L 옵션 구조: 로컬포트:원격_바인드_주소:원격포트

1-3. 운영 환경 디버깅 승인 프로세스

운영(Production) 환경에서 디버거를 붙이려면 다음 절차를 반드시 거친다.

  • 장애 대응 티켓(Incident Ticket) 생성
  • 팀 리드 또는 온콜 담당자 승인
  • 디버깅 세션 시작/종료 시각 Slack 채널 공유
  • 디버그 JVM 옵션/환경변수 제거 후 프로세스 원복 확인
  • 사후 회고(Postmortem)에 디버깅 세션 기록 포함

: 스테이징에서 충분히 재현을 시도한 뒤, 그래도 안 될 때만 운영 디버깅을 요청한다.


2. 언어별 원격 디버그 설정

Table 1: 언어별 원격 디버그 포트·프로토콜·시작 명령 비교

언어프로토콜기본 포트서버 시작 명령비고
JavaJDWP (Java Debug Wire Protocol)5005java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005,suspend=n -jar app.jarJDK 9+ address=*:5005 문법 변경
Node.jsChrome DevTools Protocol9229node --inspect=127.0.0.1:9229 app.js--inspect-brk로 첫 줄 suspend 가능
PythonDAP (Debug Adapter Protocol) via debugpy5678python -m debugpy --listen 127.0.0.1:5678 --wait-for-client app.pypip install debugpy 필요
GoDelve DAP2345dlv debug --headless --listen=127.0.0.1:2345 --api-version=2 --accept-multiclientgo install github.com/go-delve/delve/cmd/dlv@latest

2-1. Java JDWP Remote Attach

Java는 가장 성숙한 원격 디버깅 생태계를 가지고 있다.

# JDK 8
java -agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n \
  -jar myservice.jar

# JDK 9+ (주소 바인딩 문법 변경)
java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005,suspend=n \
  -jar myservice.jar

# Spring Boot Gradle 프로젝트에서 디버그 모드
./gradlew bootRun --debug-jvm
# → 기본 5005 포트에 suspend=y 로 대기

JDWP 옵션 정리:

옵션설명
transportdt_socketTCP 소켓 통신 (거의 항상 이 값)
servery디버그 서버 역할 (IDE가 클라이언트로 접속)
addresshost:port바인드 주소
suspendy / ny: 디버거 연결까지 JVM 대기, n: 즉시 실행
timeoutms디버거 연결 타임아웃 (0=무제한)

SSH 터널 연결:

# 로컬 PC에서
ssh -N -L 5005:127.0.0.1:5005 deploy@staging-01.internal

# 이후 IDE에서 localhost:5005 로 attach

2-2. Node.js --inspect Remote Attach

# 기본 inspect (연결 전에도 실행 계속)
node --inspect=127.0.0.1:9229 dist/server.js

# 첫 줄에서 멈춤 (초기화 코드 디버깅 시)
node --inspect-brk=127.0.0.1:9229 dist/server.js

# 이미 실행 중인 프로세스에 시그널로 inspect 활성화
kill -USR1 <PID>
# → stderr에 "Debugger listening on ws://127.0.0.1:9229/..." 출력

SSH 터널:

ssh -N -L 9229:127.0.0.1:9229 deploy@staging-node-server

주의: Node.js inspect는 WebSocket 기반이므로, 일부 프록시에서 WebSocket 업그레이드가 차단될 수 있다. SSH 터널이 가장 안전하다.


2-3. Python debugpy Remote Attach

# 1. 설치
pip install debugpy

# 2-A. CLI로 실행 (가장 단순)
python -m debugpy --listen 127.0.0.1:5678 --wait-for-client app.py

# 2-B. 코드에 삽입 (기존 실행 스크립트 유지할 때)

코드 삽입 방식:

import debugpy

# 포트 5678에서 대기
debugpy.listen(("127.0.0.1", 5678))
print("Waiting for debugger attach...")
debugpy.wait_for_client()  # 디버거 붙을 때까지 블로킹
debugpy.breakpoint()       # 여기서 멈춤

# 이후 비즈니스 로직
def process_order(order_id):
    # 디버거로 여기까지 step-into 가능
    ...
# 2-C. 이미 실행 중인 프로세스에 attach (제한적)
# → debugpy를 코드에 미리 내장해야 함. 런타임 inject는 지원 안 됨.

SSH 터널:

ssh -N -L 5678:127.0.0.1:5678 deploy@staging-api-server

2-4. Go Delve Remote Attach

# 설치
go install github.com/go-delve/delve/cmd/dlv@latest

# 방법 A: 소스에서 빌드 + 디버그 (개발용)
dlv debug ./cmd/server \
  --headless --listen=127.0.0.1:2345 \
  --api-version=2 --accept-multiclient

# 방법 B: 이미 빌드된 바이너리에 attach
dlv attach <PID> \
  --headless --listen=127.0.0.1:2345 \
  --api-version=2 --accept-multiclient

# 방법 C: 빌드된 바이너리 실행 (최적화 비활성화 필수)
go build -gcflags="all=-N -l" -o myserver ./cmd/server
dlv exec ./myserver \
  --headless --listen=127.0.0.1:2345 \
  --api-version=2 --accept-multiclient

중요: Go 컴파일러 최적화가 켜져 있으면 변수 값이 <optimized out>으로 보인다. 디버그용 빌드에는 반드시 -gcflags="all=-N -l"을 추가한다.

SSH 터널:

ssh -N -L 2345:127.0.0.1:2345 deploy@staging-go-server

3. IDE별 Attach 설정 예시

Table 2: IDE별 원격 Attach 설정 요약

IDE언어Attach 타입핵심 설정비고
VS CodeJavajava attachhost, portExtension Pack for Java 필요
VS CodeNode.jsnode attachport, localRoot/remoteRoot소스맵 경로 맞추기 필수
VS CodePythondebugpy attachconnect.host/port, pathMappingsms-python 확장 필요
VS CodeGogo dlv attachhost, port, remotePathGo 확장(golang.go) 필요
IntelliJJavaRemote JVM Debughost, port, JDK version커맨드라인 인자 자동 생성
PyCharmPythonPython Remote Debughost, port, path mappingsProfessional 에디션 필요

3-1. VS Code launch.json — 언어별 Remote Attach

{
  "version": "0.2.0",
  "configurations": [
    // ── Java Remote Attach ──
    {
      "type": "java",
      "name": "Java Remote (5005)",
      "request": "attach",
      "hostName": "localhost",
      "port": 5005,
      "projectName": "myservice"
    },

    // ── Node.js Remote Attach ──
    {
      "type": "node",
      "name": "Node Remote (9229)",
      "request": "attach",
      "port": 9229,
      "address": "localhost",
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app",
      "sourceMaps": true,
      "skipFiles": ["<node_internals>/**"]
    },

    // ── Python Remote Attach (debugpy) ──
    {
      "type": "debugpy",
      "name": "Python Remote (5678)",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}",
          "remoteRoot": "/app"
        }
      ],
      "justMyCode": false
    },

    // ── Go Remote Attach (Delve) ──
    {
      "type": "go",
      "name": "Go Remote (2345)",
      "request": "attach",
      "mode": "remote",
      "port": 2345,
      "host": "localhost",
      "substitutePath": [
        {
          "from": "${workspaceFolder}",
          "to": "/app"
        }
      ]
    }
  ]
}

경로 매핑이 가장 중요하다: 로컬 소스 경로와 원격 서버의 소스/빌드 경로가 다르면 브레이크포인트가 "unverified" 상태가 된다. localRoot/remoteRoot 또는 pathMappings/substitutePath를 정확히 맞춰야 한다.


3-2. IntelliJ IDEA — Remote JVM Debug

  1. Run > Edit Configurations > + > Remote JVM Debug 선택
  2. 설정값 입력:
항목
Debugger modeAttach to remote JVM
TransportSocket
Hostlocalhost
Port5005
Command line arguments for remote JVM(자동 생성됨)
Use module classpath대상 모듈 선택
  1. SSH 터널이 열린 상태에서 Debug 버튼 클릭
# IntelliJ가 자동 생성해 주는 JVM 인자 (복사해서 서버에 적용)
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

IntelliJ 추가 팁:

  • Evaluate Expression (Alt+F8): 런타임에 임의 코드 실행 가능. 운영에서는 사이드이펙트 주의.
  • Drop Frame: 현재 메서드를 재실행. 상태 변경이 롤백되지는 않으므로 주의.
  • HotSwap: 클래스 바디 수정 후 재로드. 메서드 시그니처 변경은 불가.

3-3. PyCharm — Python Remote Debug (debugpy attach)

PyCharm Professional에서 원격 Python 디버깅을 설정하는 방법:

  1. Run > Edit Configurations > + > Python Debug Server 선택 (구 버전) 또는 Python Remote Debug 선택
  2. 설정값:
항목
Hostlocalhost
Port5678
Path mappings로컬 프로젝트 루트 ↔ 원격 /app
  1. 원격 서버에서 debugpy 코드 삽입 또는 CLI 실행
  2. PyCharm에서 Debug 버튼 → 서버가 연결 대기 상태 → 원격 앱 시작
# 원격 서버의 코드에 추가
import debugpy
debugpy.listen(("0.0.0.0", 5678))  # SSH 터널 사용 시 0.0.0.0도 가능
debugpy.wait_for_client()

PyCharm Community Edition에서는 Python Remote Debug가 지원되지 않는다. VS Code + debugpy 조합이 무료 대안이다.


4. Kubernetes 환경 원격 디버깅

컨테이너 오케스트레이션 환경에서는 SSH 터널 대신 kubectl port-forward가 표준이다.

4-1. kubectl port-forward로 디버그 포트 노출

# Pod 이름으로 직접
kubectl port-forward pod/myservice-abc123-xyz 5005:5005 -n staging

# Deployment를 통해 (임의 Pod 선택)
kubectl port-forward deployment/myservice 5005:5005 -n staging

# Service를 통해
kubectl port-forward svc/myservice 5005:5005 -n staging

이후 IDE에서 localhost:5005로 attach하면 된다.

디버그 모드 Pod 배포 예시 (Java):

# deployment-debug.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myservice-debug
  namespace: staging
spec:
  replicas: 1 # 디버그용은 반드시 1개
  selector:
    matchLabels:
      app: myservice-debug
  template:
    metadata:
      labels:
        app: myservice-debug
    spec:
      containers:
        - name: myservice
          image: myregistry/myservice:debug-20260307
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 5005
              name: jdwp
          env:
            - name: JAVA_TOOL_OPTIONS
              value: >-
                -agentlib:jdwp=transport=dt_socket,server=y,
                address=127.0.0.1:5005,suspend=n
          resources:
            requests:
              memory: '512Mi'
              cpu: '500m'
            limits:
              memory: '1Gi'
              cpu: '1000m'
kubectl apply -f deployment-debug.yaml
kubectl port-forward deployment/myservice-debug 5005:5005 -n staging

4-2. 디버거 사이드카 패턴

기존 컨테이너 이미지를 수정하지 않고, 사이드카 컨테이너에 Delve 등 디버거를 넣어 공유 프로세스 네임스페이스로 attach하는 패턴이다.

apiVersion: v1
kind: Pod
metadata:
  name: myservice-debug-sidecar
spec:
  shareProcessNamespace: true # PID 네임스페이스 공유
  containers:
    - name: myservice
      image: myregistry/myservice:latest
      ports:
        - containerPort: 8080
    - name: debugger
      image: golang:1.22
      command: ['sleep', 'infinity']
      securityContext:
        capabilities:
          add: ['SYS_PTRACE'] # ptrace 권한 필요
# 사이드카 컨테이너에 진입하여 Delve로 attach
kubectl exec -it myservice-debug-sidecar -c debugger -- bash
# 컨테이너 안에서:
go install github.com/go-delve/delve/cmd/dlv@latest
dlv attach <PID> --headless --listen=:2345 --api-version=2

4-3. Telepresence / Ephemeral Containers

Telepresence: 로컬 프로세스를 클러스터 네트워크에 "끼워 넣는" 도구. 원격 Pod 대신 로컬에서 실행하면서 클러스터 내부 DNS·서비스 접근이 가능하다.

# 설치
brew install datawire/blackbird/telepresence

# 클러스터 연결
telepresence connect

# 특정 워크로드 가로채기 (트래픽을 로컬로 리다이렉트)
telepresence intercept myservice --port 8080:8080 --env-file .env.telepresence

# 로컬에서 디버그 모드로 실행
JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005,suspend=n" \
  java -jar myservice.jar

Ephemeral Containers (Kubernetes 1.25+): 실행 중인 Pod에 임시 컨테이너를 주입한다.

kubectl debug -it pod/myservice-abc123 \
  --image=busybox \
  --target=myservice \
  --share-processes \
  -n staging

Ephemeral Container에는 디버거 바이너리가 포함된 이미지를 사용해야 한다. 프로덕션 이미지에 디버거를 넣지 않아도 되는 것이 장점이다.


5. 운영 환경 주의사항

5-1. suspend=y vs suspend=n

옵션동작사용 시점
suspend=y디버거가 붙을 때까지 프로세스 정지애플리케이션 시작 로직 디버깅
suspend=n디버거 없이도 즉시 실행운영·스테이징 상시 적용

운영 환경에서는 반드시 suspend=n을 사용한다. suspend=y는 프로세스가 디버거 연결을 기다리며 멈추므로 헬스체크 실패 → Pod 재시작 → 연쇄 장애로 이어질 수 있다.

5-2. 성능 영향 (디버거 연결 시 오버헤드)

디버그 에이전트가 로드된 상태에서도 디버거가 연결되지 않으면 오버헤드는 미미하다 (1-3% 이내). 그러나 다음 상황에서 성능 저하가 심해진다:

  • 브레이크포인트 다수 설정: 매 히트마다 모든 스레드가 suspend됨
  • 조건부 브레이크포인트: 조건 evaluation 비용이 매 실행마다 발생
  • Evaluate Expression: 무거운 expression 실행 시 전체 스레드 정지
  • Step-over/Step-into 반복: 단일 스레드 외에도 다른 스레드의 suspend/resume이 빈번
# 성능 영향 체감 수준
에이전트 로드만 (연결 없음)  : ~1-3% 오버헤드
브레이크포인트 1-2 (히트 빈도 낮음) : ~5% 오버헤드
브레이크포인트 다수 + 조건부 : ~10-30% 오버헤드 (가변)
Evaluate Expression 빈번 실행   : 순간적으로 응답 지연 수 초

5-3. 세션 종료 후 원복 절차

디버깅이 끝났으면 반드시 환경을 원래 상태로 돌린다.

# 1. IDE에서 디버거 연결 해제 (Disconnect)

# 2. SSH 터널 종료
# 터미널에서 실행 중이면 Ctrl+C
# 백그라운드면:
lsof -i :5005 | grep ssh
kill <PID>

# 3. 디버그 JVM 옵션 제거 후 프로세스 재시작
# Kubernetes의 경우:
kubectl rollout restart deployment/myservice -n staging
# 또는 디버그 전용 Deployment 삭제:
kubectl delete -f deployment-debug.yaml

# 4. 디버그용 코드 제거 (debugpy.listen 등)
git diff  # 실수로 커밋하지 않았는지 확인

# 5. port-forward 프로세스 종료 확인
lsof -i :5005
lsof -i :5678
lsof -i :9229
lsof -i :2345

5-4. 타임아웃과 연결 끊김 대응

상황원인대응
SSH 터널 자동 종료유휴 타임아웃ServerAliveInterval 60 설정
IDE "Connection refused"포트 바인딩 안 됨서버에서 ss -tlnp | grep 5005 확인
브레이크포인트 히트 안 됨경로 매핑 오류localRoot/remoteRoot 재확인
Pod 재시작 후 연결 끊김OOMKill / 헬스체크 실패Resource limits 상향, suspend=n 확인
Delve "could not attach"ptrace 권한 없음SYS_PTRACE capability 추가
# SSH 유휴 타임아웃 방지
ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 \
  -N -L 5005:127.0.0.1:5005 deploy@staging-server

# 연결 상태 확인
ss -tlnp | grep 5005
# 또는
netstat -tlnp | grep 5005

6. 원격 디버깅 세션 체크리스트

세션 전 (Before)

  • 로컬에서 재현 시도를 먼저 했는가?
  • 스테이징에서 재현 가능한가? (운영 디버깅은 최후 수단)
  • 디버깅 대상 서비스의 소스 코드가 배포 버전과 일치하는가?
  • 디버그 에이전트/debugpy/Delve가 서버에 설치되어 있는가?
  • 디버그 포트가 127.0.0.1에만 바인딩되는가?
  • SSH 터널 또는 kubectl port-forward 명령이 준비되었는가?
  • 운영 환경이라면 팀 리드/온콜 승인을 받았는가?
  • IDE launch.json / Run Configuration에 경로 매핑이 설정되었는가?

세션 중 (During)

  • suspend=n으로 설정했는가? (운영 필수)
  • 브레이크포인트는 최소한으로 설정했는가?
  • 조건부 브레이크포인트를 활용해 히트 빈도를 줄였는가?
  • Evaluate Expression에서 상태 변경(write) 코드를 실행하지 않았는가?
  • 디버깅 세션 시간을 제한했는가? (권장: 30분 이내)
  • 다른 팀원에게 디버깅 진행 중임을 공유했는가?

세션 후 (After)

  • IDE에서 디버거를 Disconnect 했는가?
  • SSH 터널 / port-forward 프로세스를 종료했는가?
  • 디버그 JVM 옵션/환경변수를 제거하고 프로세스를 원복했는가?
  • 소스 코드에 삽입한 debugpy.listen() 등을 제거했는가?
  • 디버그용 Deployment/Pod를 삭제했는가?
  • 발견한 내용을 티켓/Slack에 기록했는가?
  • 재발 방지를 위한 로그/메트릭 추가 계획을 세웠는가?

마무리

원격 디버깅은 "마지막 수단"이지만, 제대로 준비하면 가장 강력한 도구가 된다. 핵심을 요약하면:

  1. 보안 먼저: 디버그 포트는 localhost 바인딩 + SSH 터널이 원칙이다.
  2. 환경별 표준화: 언어별 디버그 시작 명령과 IDE attach 설정을 팀 wiki에 정리해 둔다.
  3. Kubernetes 친화적: kubectl port-forward, Telepresence, Ephemeral Container를 상황에 맞게 선택한다.
  4. 원복은 필수: 디버깅이 끝나면 에이전트 제거, 터널 종료, 코드 원복까지 확인한다.

로그와 메트릭만으로 해결되지 않는 문제가 왔을 때, 이 가이드의 체크리스트를 따라가면 안전하고 빠르게 원인을 찾을 수 있다.

원격 디버깅에서 찾은 문제의 근본 원인 분석은 언어×프레임워크 장애 사례집의 8가지 사례를 참고한다. IDE별 상세 설정은 IDE별 디버깅 완전정리를 참고한다.