애플 로그인 만드는 작업을 했는데, 애플 로그인은 다른 로그인과는 다른 특징이 있었습니다.
- http나 로컬호스트에서 사용 불가
결국 Dev나 Stag 서버에 배포해 테스트해보는 수밖에 없었는데 디버거 없이 개발하는 과정이 굉장히 불편하게 느껴졌습니다. 하나하나 로그 찍고 배포하고, 또 확인할 거 생겼을 때 다시 로그 찍고 배포해서 확인할 수도 없고...
어떻게 하면 원격에서도 디버거를 사용할 수 있을까요?
본격적으로 시작하기 전에 디버거에 대해 알아봅시다.
Debugger
백엔드 애플리케이션을 만들어 보신 분이라면 대부분 IDE에 내장된 디버거를 사용해보신 경험이 있으실 겁니다.
Eclipse, IntelliJ IDEA와 같은 IDE는 디버거를 가지고 있는데요. 개발자들은 이 디버거를 사용해 코드의 플로우나 변수의 값을 확인해 버그를 쉽게 잡을 수 있습니다.
Java platform debugger architecture
그렇다면 자바에서 디버그는 어떤 원리로 실행될까요? Java Platform debugger architecture에 대해 알아봅시다.
이 아키텍처는 디버깅을 당하는 대상인 Debuggee(서버)와 디버깅을 하는 Debugger로 구성됩니다.
여기서 Debuggee는 Debugger는 JDWP라는 프로토콜로 메시지를 주고 받는데요. 이 때 중요한 역할을 하는 것이 Debuggee의 JMVTI 인터페이스, Debugger의 JDI 인터페이스 입니다.
간단하게 설명하자면, JMVTI(JVM Tools Interface)는 JVM에서 실행되는 애플리케이션의 상태를 검사하고 실행을 제어해주는 네이티브 인터페이스입니다. JDI(Java Debug Interface)는 애플리케이션이 디버깅되는 동안 개발자가 디버거를 통해 애플리케이션과 상호 작용할 수 있도록 해주는 인터페이스입니다. JDI를 통해 중단점 설정, Stepping, 스레드 처리, 변수 검사 등의 작업을 처리할 수 있습니다.
JDWP(Java Debugging Wire Protocol)는 Debuggee와 Debugger가 소통할 때 사용하는 프로토콜입니다.
그런데 로컬에서 디버거를 사용할 수 없다면?
그런데 서론에 적혀 있는 것처럼 로컬에서 개발해야하는 핵심 로직을 실행할 수 없다면? 그래서 디버그를 사용할 수 없는 상황이라면 어떻게 해야할까요? 그럴 때 사용할 수 있는 것이 Remote JVM Debug 기능입니다.
Remote Debug
서버는 Debuggee, IDE는 Debugger.
이렇게 간단하게 나누고 보면, 원격으로 디버깅을 하나 로컬로 디버깅을 하나 원리는 같습니다.
서버와 IDE를 JWDP로 연결해주면 로컬에서 사용하는 IDE에서 디버깅이 가능해집니다.
로컬에서 디버거를 사용할 때는 별도의 설정 없이 IDE가 Debuggee를 제어할 수 있기에 연결이 자유자재였습니다만 원격 서버는 아무런 설정 없이 IDE가 제어하기란 불가능하니 디버거를 연결하고 싶으면 별도의 설정이 필요합니다.
Java 옵션 명령어 사용
Java 명령어로 Application을 실행할 때 agentlib 옵션을 설정하면 됩니다.
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5556 -jar application.jar
- agentlib 옵션
- jdwp=transport=dt_socket: JVM에 JDWP를 에이전트로 등록하여 디버거가 JVM에 소켓 방식으로 연결할 수 있도록 설정. 실행 중인 두 애플리케이션을 연결할 때, 소켓이 표준으로 사용.
- server=y: y로 설정할 경우, 서버 역할을 합니다.
- suspend=n: 디버거를 기다리지 않고, 즉시 프로그램을 실행합니다.
- address=*:5556: 5556 포트에 디버거가 연결될 수 있도록 Listening 합니다.
도커파일에서 명령어 적용해보기
아마 실제 배포 환경에서는 도커 이미지를 많이 사용하실텐데요. 간단합니다.
도커에서 Application을 실행하는 명령어를 동작시킬 때 위의 옵션을 그대로 적용하면 됩니다.
FROM gradle:8.4.0-jdk21-alpine AS builder
ARG name=my-server
WORKDIR /app
COPY gradle ./gradle
COPY build.gradle.kts ./
COPY settings.gradle.kts ./
COPY ${name} ./${name}
RUN gradle :${name}:bootJar
FROM openjdk:21
ARG name=my-server
ENV jar_name=${name}
WORKDIR /home/my-server
COPY --from=builder app/${name}/build/libs/${name}.jar ./${name}.jar
ENTRYPOINT exec java \
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5556 \
-jar ${jar_name}.jar
원격 연결하기
위 이미지로 배포에 성공했다면 이제 IDE의 Remote JVM Debug 기능을 사용해 원격의 Debuggee에 연결을 시도해봅시다.
- Edit Configurations로 들어갑시다.
- Add New Configuration을 클릭하고 Remote JVM Debug를 선택합니다.
- Attach to remote JVM을 선택합니다.
- 호스트 주소와 Listening 포트를 적어주고 Apply -> OK!
- Listening 포트는 위의 명령에서 작성한 5556입니다.
이제 위 Configuration으로 디버그를 실행하면 원격 서버에서 실행되는 로직이 로컬에서 디버그 가능해집니다!!!
(대신 원격 서버의 5556가 열려 있어야 합니다!!)
보너스! k8s에서 Remote JVM Debug 쉽게 사용하기
kubernetes 환경에서 Remote JVM Debug를 쉽게 사용할 수 있는 방법 2가지를 소개해드리도록 하겠습니다!
Pod가 여러 개 띄워져 있을 때 사용이 불가능합니다. API 요청 시 로드밸런싱에 의해 요청이 나눠지게 되는데, 그 요청이 포트포워딩된 파드나 노드 포트로 연결된 파드로 무조건 전달된다는 보장이 없기 때문입니다.
1. 포트포워딩하기
Application Pod를 로컬호스트로 포트포워딩하여 IDE의 Remote JVM Debug 설정 시 호스트 주소를 로컬호스트로 두는 방식입니다. 가장 간단한 방법입니다.
- 아래 명령어에 대한 설명
- {로컬에서 사용할 포트}:{서버에서 리스닝하고 있는 포트}
- 서버에서 리스닝하고 있는 포트는 도커 파일에서 설정해준 5556이 들어가야합니다.
- 로컬에서 사용할 포트는 IntelliJ에서 설정할 포트 값으로 해주시면 됩니다. (현재 로컬에서 사용하고 있지 않은 포트 사용)
$ kubectl port-forward -n my-namespace pods/my-server-deploy-69d75789cf-psfcw 5556:5556
Forwarding from 127.0.0.1:5556 -> 5556
Forwarding from [::1]:5556 -> 5556
- 아래와 같이 호스트를 localhost로 둬도 연결이 됩니다!
2. 노드포트 사용하기
포트포워딩은 정말 간단한 방법입니다만 모종의 이유로 갑자기 포트포워딩이 끊겨버려 디버깅에 불편함을 겪게될 수 있습니다.
서비스의 Type을 NodePort 두는 것이 방법이 될 수 있습니다.
아래 코드를 확인해봅시다. Remote JVM Debug에 사용할 포트를 고정하기 위해 debug 포트를 32000번으로 고정해줬습니다. (주의! deployment에서도 5556번 포트를 열어줘야함.)
apiVersion: v1
kind: Service
metadata:
name: my-server-svc
namespace: my-namespace
spec:
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: server
- port: 5556
targetPort: 5556
protocol: TCP
nodePort: 32000
name: debug
type: NodePort
selector:
app: my-server
- 파드가 어떤 노드에 속하는지 알아봅시다.
$ kubectl get pod -o wide -n lohasmeal-dev
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-server-deploy-795888d8c5-jcp27 1/1 Running 0 3d23h xxx.xx.x.xx my-nodepool-AAAAAAA <none> <none>
- 노드의 INTERNAL-IP를 호스트주소 삼아 원격 연결을 시도하면 됩니다.
- xx.x.x.xx:32000 형식으로 연결!
kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
my-nodepool-AAAAAAA Ready <none> 88d v1.23.9 xx.x.x.xx <none> Ubuntu 20.04.3 LTS 5.4.0-99-generic containerd://1.6.16
참고 자료