Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

>

> 1. [언어별 디버깅 가이드](/blog/devops/2026-03-07-devops-debugging-by-language-python-javascript-go-java)

> 2. [프레임워크별 디버깅 실전](/blog/devops/2026-03-07-devops-debugging-by-framework-spring-django-fastapi-react-nextjs)

> 3. [IDE별 디버깅 완전정리](/blog/devops/2026-03-07-devops-debugging-by-ide-vscode-intellij-pycharm)

> 4. [언어×프레임워크 장애 사례집](/blog/devops/2026-03-07-devops-debugging-casebook-language-framework-combos)

> 5. **[원격 디버깅 실전 가이드](/blog/devops/2026-03-07-devops-remote-debugging-guide-ssh-tunnel-vscode-intellij-pycharm)** ← 현재 글

원격 디버깅이 필요한 순간

로컬에서 재현되지 않는 버그는 반드시 온다. 스테이징 전용 데이터, 운영 환경 고유의 네트워크 토폴로지, 특정 리전에서만 발생하는 타이밍 이슈 등 원인은 다양하다. 로그만으로 해결되지 않을 때 원격 디버깅(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: 언어별 원격 디버그 포트·프로토콜·시작 명령 비교

| 언어 | 프로토콜 | 기본 포트 | 서버 시작 명령 | 비고 |

| ------- | ---------------------------------------- | --------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |

| Java | JDWP (Java Debug Wire Protocol) | 5005 | `java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:5005,suspend=n -jar app.jar` | JDK 9+ `address=*:5005` 문법 변경 |

| Node.js | Chrome DevTools Protocol | 9229 | `node --inspect=127.0.0.1:9229 app.js` | `--inspect-brk`로 첫 줄 suspend 가능 |

| Python | DAP (Debug Adapter Protocol) via debugpy | 5678 | `python -m debugpy --listen 127.0.0.1:5678 --wait-for-client app.py` | pip install debugpy 필요 |

| Go | Delve DAP | 2345 | `dlv debug --headless --listen=127.0.0.1:2345 --api-version=2 --accept-multiclient` | go 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 옵션 정리**:

| 옵션 | 값 | 설명 |

| ----------- | ----------- | ------------------------------------------ |

| `transport` | `dt_socket` | TCP 소켓 통신 (거의 항상 이 값) |

| `server` | `y` | 디버그 서버 역할 (IDE가 클라이언트로 접속) |

| `address` | `host:port` | 바인드 주소 |

| `suspend` | `y` / `n` | y: 디버거 연결까지 JVM 대기, n: 즉시 실행 |

| `timeout` | ms | 디버거 연결 타임아웃 (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. 코드에 삽입 (기존 실행 스크립트 유지할 때)

코드 삽입 방식:

포트 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 Code | Java | `java` attach | host, port | Extension Pack for Java 필요 |

| VS Code | Node.js | `node` attach | port, localRoot/remoteRoot | 소스맵 경로 맞추기 필수 |

| VS Code | Python | `debugpy` attach | connect.host/port, pathMappings | ms-python 확장 필요 |

| VS Code | Go | `go` dlv attach | host, port, remotePath | Go 확장(golang.go) 필요 |

| IntelliJ | Java | Remote JVM Debug | host, port, JDK version | 커맨드라인 인자 자동 생성 |

| PyCharm | Python | Python Remote Debug | host, port, path mappings | Professional 에디션 필요 |

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 mode | Attach to remote JVM |

| Transport | Socket |

| Host | localhost |

| Port | 5005 |

| Command line arguments for remote JVM | (자동 생성됨) |

| Use module classpath | 대상 모듈 선택 |

3. 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. 설정값:

| 항목 | 값 |

| ------------- | ------------------------------- |

| Host | localhost |

| Port | 5678 |

| Path mappings | 로컬 프로젝트 루트 ↔ 원격 /app |

3. 원격 서버에서 debugpy 코드 삽입 또는 CLI 실행

4. PyCharm에서 **Debug** 버튼 → 서버가 연결 대기 상태 → 원격 앱 시작

원격 서버의 코드에 추가

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. **원복은 필수**: 디버깅이 끝나면 에이전트 제거, 터널 종료, 코드 원복까지 확인한다.

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

> 원격 디버깅에서 찾은 문제의 근본 원인 분석은 [언어×프레임워크 장애 사례집](/blog/devops/2026-03-07-devops-debugging-casebook-language-framework-combos)의 8가지 사례를 참고한다.

> IDE별 상세 설정은 [IDE별 디버깅 완전정리](/blog/devops/2026-03-07-devops-debugging-by-ide-vscode-intellij-pycharm)를 참고한다.

현재 단락 (1/302)

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

작성 글자: 0원문 글자: 12,646작성 단락: 0/302