프라이빗 클라우드를 제대로 공부하려면 OpenStack을 직접 깔아봐야 합니다. 가장 빠른 길이 DevStack — 스크립트 하나로 올인원 OpenStack을 올려주는 개발/학습용 도구죠. 저는 이걸 Proxmox 홈랩에 올려봤습니다. 결론부터 솔직히 말하면 한 방에 안 됐고, 여러 번 깨졌습니다. 이번 편은 설치 준비부터 첫 번째 벽까지의 실전 기록입니다.
1. 먼저 정한 것 — OS는 Ubuntu
제 홈랩엔 Rocky 9 설치용 VM이 있었지만, DevStack엔 안 썼습니다. DevStack은 사실상 Ubuntu 전용이거든요(공식 지원·테스트가 우분투 중심). Rocky/CentOS는 지원이 불안정합니다. 그래서 깨끗한 Ubuntu 24.04로 갔습니다.
RuntimeError: NumPy was built with baseline optimizations:
(X86_V2) but your machine doesn't support: (X86_V2).
원인은 가상 CPU 타입이었습니다. Proxmox VM의 기본 CPU 타입(kvm64)은 호환성을 위해 최신 명령어셋(SSE4.2 등, x86-64-v2)을 감춥니다. 그런데 요즘 NumPy 휠은 x86-64-v2를 전제로 빌드돼서, 그 명령어가 없으면 임포트 단계에서 크래시합니다. nova의 콘솔 프록시가 NumPy를 쓰다 죽은 거죠.
해결은 지난 KVM 글에서 다룬 바로 그 포인트 — CPU 타입을 host로 바꿔 물리 CPU 기능을 그대로 노출하는 것이었습니다.
qm stop 200
qm set 200 --cpu host # 물리 i5의 명령어셋 그대로 노출
qm start 200
# VM 안에서 확인 — 이제 노출됨
grep -o -m1 sse4_2 /proc/cpuinfo # sse4_2
grep -o -m1 avx2 /proc/cpuinfo # avx2
이건 일반 DevStack 튜토리얼엔 없는 함정입니다. 물리 서버나 워크스테이션에선 안 생기고, “기본 CPU 타입 가상머신”에서만 터지거든요. 홈랩에서 VM으로 OpenStack·AI 라이브러리를 돌린다면 --cpu host는 기본으로 챙기세요.
다음 편
CPU를 고치고 다시 ./stack.sh를 돌렸습니다. 그런데 이번엔 네트워크(Neutron)에서, 그다음엔 Glance에서, 또 placement에서 연달아 막혔습니다. 다음 편에서 이 “서비스는 뜨는데 안 되는” 연속 삽질과 각각의 원인을 낱낱이 분석합니다. DevStack이 홈랩에서 왜 만만치 않은지, 제대로 보여드리겠습니다.
Proxmox를 설치하면 vmbr0라는 게 자동으로 생깁니다. VM·컨테이너가 다 여기에 붙는데, 정작 이게 뭔지 모르고 쓰는 분이 많습니다. Linux Bridge와 VLAN — Proxmox 네트워크의 두 핵심을 실제 제 구성과 함께 정리합니다.
1. Linux Bridge — 가상 스위치
vmbr0는 소프트웨어로 만든 네트워크 스위치입니다. 물리 랜포트(NIC)와 VM/컨테이너들을 이 가상 스위치에 꽂아 서로·외부와 통신하게 합니다.
실제 제 홈랩의 설정은 이렇게 단순합니다.
# /etc/network/interfaces
auto vmbr0
iface vmbr0 inet static
address 192.168.20.100/24
bridge-ports enp1s0 # 물리 NIC를 브리지에 연결
bridge-stp off
bridge-fd 0
즉 물리 NIC enp1s0가 vmbr0에 물려 있고, 모든 게스트가 이 브리지를 통해 192.168.20.x 실제 네트워크에 직접 참여합니다. 그래서 VM에 공유기가 주는 IP가 그대로 붙는 겁니다.
2. 게스트를 브리지에 연결
# VM/LXC 네트워크를 vmbr0에 virtio로 연결
qm set 100 --net0 virtio,bridge=vmbr0
pct set 113 --net0 name=eth0,bridge=vmbr0,ip=dhcp
이게 전부입니다. 게스트는 마치 물리 스위치에 랜선을 꽂은 것처럼 동작합니다.
3. VLAN — 하나의 랜을 여러 개로 나누기
네트워크를 용도별로 분리하고 싶을 때(예: 서버망 / IoT망 / 게스트망) VLAN을 씁니다. 물리 케이블은 하나인데 논리적으로 여러 망으로 쪼개는 기술입니다.
Proxmox에서는 브리지를 VLAN-aware로 만들고, 게스트마다 VLAN 태그를 지정합니다.
# VLAN 인식 브리지
auto vmbr0
iface vmbr0 inet static
address 192.168.20.100/24
bridge-ports enp1s0
bridge-vlan-aware yes
bridge-vids 2-4094
# 게스트를 특정 VLAN(예: 30)에 배치
qm set 100 --net0 virtio,bridge=vmbr0,tag=30
이러면 tag=30인 게스트들끼리만 같은 망에 묶이고, 다른 VLAN과는 L3 라우팅(방화벽)을 거쳐야만 통신합니다. 보안 분리에 효과적이죠.
4. 솔직한 현실 — 내 홈랩은 아직 플랫
고백하자면 제 홈랩은 아직 VLAN 없이 단일 플랫 네트워크(192.168.20.x)입니다. 서비스가 늘면서 “IoT·실험 VM은 분리하는 게 맞는데” 싶은 순간이 오는데, 저도 그 숙제를 남겨둔 상태입니다. 홈랩이 커지면 VLAN 분리는 결국 마주치는 과제입니다.
5. 실전 팁
bridge-stp off: 단일 스위치 환경에선 STP 꺼도 됩니다(루프 위험 없을 때).
본딩(bond): NIC가 여러 개면 본딩으로 이중화·대역폭 확장 가능.
관리망 분리: 규모가 커지면 Proxmox 관리 접속용 망을 따로 두는 걸 권장.
6. 정리
Proxmox 네트워크의 기본은 Linux Bridge(가상 스위치)이고, 분리가 필요하면 VLAN(논리 분할)을 얹습니다. 대부분은 vmbr0 하나로 시작해 충분하고, 서비스가 많아지면 VLAN으로 넘어가면 됩니다. 브리지가 어떻게 물리 NIC와 게스트를 잇는지만 이해하면, Proxmox 네트워크는 더 이상 미스터리가 아닙니다.
Kubernetes 프로브 트러블슈팅은 단순히 /health 경로가 200을 반환하는지 확인하는 일이 아닙니다. 운영에서는 이 설정 하나가 재시작 루프, 트래픽 블랙홀, 배포 지연, 의존성 장애 전파를 만들기도 하고 막기도 하거든요. 13년 넘게 서비스를 운영하면서 느낀 건, 프로브는 애플리케이션의 상태를 보는 기능이라기보다 쿠버네티스가 장애에 어떻게 반응할지 정하는 정책에 가깝다는 점입니다.
대표적인 장면은 비슷합니다. 앱 로그만 보면 큰 문제가 없는데 kubectl get pods의 RESTARTS가 계속 올라갑니다. 또는 배포는 끝났는데 Service 뒤에서 파드가 빠져 502, 503이 발생합니다. 이때 무작정 앱을 재배포하거나 노드를 의심하면 시간이 정말 빨리 녹습니다. 먼저 봐야 할 것은 kubelet 이벤트입니다. Liveness probe failed 뒤에 Killing container가 붙는지, Readiness probe failed만 반복되는지에 따라 원인이 완전히 갈립니다.
위 흐름에서 핵심은 하나입니다. Liveness 실패는 재시작을 부르고, Readiness 실패는 트래픽 제외를 부릅니다. 둘 다 헬스 체크처럼 보이지만 운영 결과는 다릅니다. 그래서 프로브를 설계할 때는 “정상인가?”보다 “실패했을 때 쿠버네티스가 무엇을 해야 하는가?”를 먼저 정해야 합니다.
Liveness Probe 실패와 Readiness Probe 오류의 차이
Liveness Probe는 컨테이너가 복구 불가능한 상태에 빠졌는지 판단합니다. 실패가 임계치에 도달하면 kubelet이 해당 컨테이너를 재시작합니다. Readiness Probe는 지금 요청을 받아도 되는지 판단합니다. 실패해도 컨테이너는 계속 실행되지만, 해당 파드는 Service 엔드포인트에서 제외됩니다. Startup Probe는 초기 기동 중인 앱을 보호합니다. Startup Probe가 설정되면 성공하기 전까지 Liveness와 Readiness가 실행되지 않으므로, 기동이 느린 앱에서 조기 재시작을 막는 데 유용합니다.
운영에서 많이 터지는 실수는 외부 의존성까지 Liveness에 넣는 겁니다. DB가 잠깐 느려졌을 뿐인데 모든 파드가 동시에 재시작되면, 커넥션 재생성·캐시 워밍업·JIT/클래스 로딩 같은 비용이 한꺼번에 몰립니다. 장애를 복구하는 게 아니라 장애에 연료를 붓는 모양이 되더라고요. DB, Redis, 메시지 브로커, 외부 API는 대부분 Readiness에서 다루는 편이 안전합니다.
구분
Liveness Probe
Readiness Probe
Startup Probe
운영 판단 기준
목적
컨테이너를 재시작해야 하는지 판단
트래픽을 받을 준비가 됐는지 판단
초기 기동이 끝났는지 판단
실패 시 원하는 조치가 재시작인지, 트래픽 차단인지부터 정합니다.
실패 시 동작
컨테이너 재시작
Service 엔드포인트에서 제외
성공 전까지 다른 프로브 보류, 실패 시 컨테이너 재시작
RESTARTS, Events, EndpointSlice를 같이 봐야 오판이 줄어듭니다.
적합한 검사
이벤트 루프 멈춤, 데드락, 기본 핸들러 무응답
DB 연결, 캐시 준비, 필수 설정 로딩, 마이그레이션 대기
JVM/Node.js/.NET 앱의 긴 부팅, 모델 로딩, 초기 캐시 구성
외부 의존성 실패가 곧 프로세스 사망을 의미하지는 않습니다.
과도하게 엄격할 때
재시작 루프, 콜드 스타트 비용 증가
정상 파드가 트래픽을 못 받아 가용 용량 감소
기동 실패 감지가 늦어짐
엄격함은 안정성이 아니라 운영 정책입니다. 비용을 함께 계산해야 합니다.
주로 확인할 증거
Killing container, BackOff, --previous 로그
Ready 0/1, Endpoint/EndpointSlice 누락
기동 로그, 초기화 완료 시점, Startup 실패 이벤트
로그보다 먼저 Events를 보면 방향을 빨리 잡습니다.
쿠버네티스 헬스 체크 기본 설정 예시
아래 예시는 세 프로브를 분리한 Deployment입니다. 실제 애플리케이션 이미지가 /livez, /readyz, /startupz 엔드포인트를 제공한다는 전제로 봐주세요. /livez는 프로세스와 HTTP 서버가 응답 가능한지만 확인하고, /readyz는 실제 요청 처리 준비 상태를 확인합니다. /startupz는 초기화가 끝나기 전 Liveness가 끼어들지 않도록 막습니다. 운영 매니페스트에서는 이 정도 분리가 나중에 장애 분석 시간을 꽤 줄여줍니다.
값을 고를 때는 감이 아니라 앱의 실제 시간을 기준으로 잡습니다. 기동 로그에서 “서버 포트 바인딩 완료”, “마이그레이션 완료”, “캐시 로딩 완료”, “요청 처리 가능” 시점을 분리해 보세요. initialDelaySeconds를 길게 늘리는 방식은 단순하지만, 앱이 빨리 떠도 무조건 기다립니다. 반면 startupProbe는 성공하는 순간 다음 단계로 넘어가므로, 느린 기동과 빠른 회복을 같이 다루기 좋습니다.
프로브 파라미터는 서로 곱해져 운영 결과를 만듭니다. 예를 들어 periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 2라면 실패를 확정하기까지 여러 번의 검사 주기가 필요합니다. 대략 30초 안팎으로 생각할 수 있지만, 정확한 감지 시간은 스케줄링과 응답 지연에 영향을 받습니다. 고정 수치처럼 외우기보다 “얼마나 빨리 빼고 얼마나 늦게 죽일 것인가”라는 정책으로 보는 편이 낫습니다.
매니페스트에서 확인할 필드는 httpGet.path, port, periodSeconds, timeoutSeconds, failureThreshold, successThreshold입니다. 참고로 successThreshold는 Liveness와 Startup에서는 1이어야 하고, Readiness에서만 1보다 크게 둘 수 있습니다. Readiness 복귀를 일부러 보수적으로 만들고 싶을 때만 조정하세요.
실전 디버깅: Kubernetes 프로브 트러블슈팅 명령어
장애가 났을 때 저는 애플리케이션 로그보다 먼저 쿠버네티스 이벤트를 봅니다. 이벤트는 kubelet이 왜 컨테이너를 죽였는지, 왜 Ready에서 제외했는지 비교적 솔직하게 남깁니다. 아래 순서대로 보면 “앱 문제인지, 프로브 설정 문제인지, Service 라우팅 문제인지”가 빠르게 갈립니다.
NS=default
APP=probe-demo
POD=$(kubectl get pod -n "$NS" -l app="$APP" -o jsonpath='{.items[0].metadata.name}')
kubectl get pods -n "$NS" -l app="$APP" -o wide
kubectl describe pod "$POD" -n "$NS"
kubectl logs "$POD" -n "$NS" --all-containers=true --tail=200
kubectl logs "$POD" -n "$NS" --previous --tail=200
kubectl get events -n "$NS" --sort-by=.lastTimestamp
kubectl get svc "$APP" -n "$NS"
kubectl get endpoints "$APP" -n "$NS"
kubectl get endpointslice -n "$NS" -l kubernetes.io/service-name="$APP"
해석은 이렇게 합니다. Liveness probe failed 다음에 Killing container가 이어지면 재시작 원인은 Liveness입니다. Readiness probe failed만 반복되고 RESTARTS가 늘지 않으면 파드는 살아 있지만 Service 트래픽에서 제외된 겁니다. connection refused는 포트 미오픈, 잘못된 포트명, 앱 기동 전 검사 시작, 또는 앱이 127.0.0.1에만 바인딩된 경우를 의심합니다. HTTP probe failed with statuscode: 404는 대개 경로 오타입니다. context deadline exceeded나 timeout 계열 메시지는 앱 내부 지연, CPU throttling, I/O 대기, GC 지연까지 같이 봐야 합니다.
kubectl run curl-debug \
-n default \
--image=curlimages/curl:8.5.0 \
--restart=Never \
--rm -it \
-- sh
# 디버그 파드 안에서 실행
curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/readyz
curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/livez
curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local:80/readyz
여기서 중요한 건 “Service DNS로 되는가”와 “Pod IP로 직접 되는가”를 나눠 보는 겁니다. Service DNS는 실패하는데 Pod IP 직접 호출은 성공하면 Service selector나 EndpointSlice 쪽을 봅니다. Pod IP 직접 호출도 실패하면 컨테이너 포트, 앱 바인딩 주소, 경로, 응답 시간을 봅니다. 프로브는 kubelet이 Pod 네트워크의 Pod IP로 직접 보내는 검사라서, Ingress에서 보이는 증상과 다를 수 있습니다.
재현 가능한 시나리오: Readiness Probe 오류 만들고 확인하기
테스트 클러스터에서 일부러 잘못된 Readiness 경로를 넣어보면 동작이 선명해집니다. 아래 Pod는 nginx를 띄우지만 존재하지 않는 /not-ready를 Readiness로 검사합니다. 컨테이너는 실행되지만 Ready가 되지 않고, Service 엔드포인트에도 들어가지 않는 상태를 만들 수 있습니다. 이거 한 번 직접 보면 실장애 때 감이 훨씬 빨리 옵니다.
kubectl apply -f bad-readiness.yaml
kubectl get pod bad-readiness -w
kubectl describe pod bad-readiness
kubectl get endpoints bad-readiness
kubectl get endpointslice -l kubernetes.io/service-name=bad-readiness
kubectl delete -f bad-readiness.yaml
정상적인 관찰 결과는 이렇습니다. kubectl get pod에서 READY가 0/1로 보이고, describe Events에는 Readiness 실패가 찍힙니다. 하지만 RESTARTS는 증가하지 않습니다. Service의 Endpoint 또는 EndpointSlice도 비어 있거나 해당 파드 IP가 빠져 있어야 합니다. 이 작은 실험을 해두면 “왜 컨테이너는 Running인데 트래픽이 안 가지?”라는 질문에 훨씬 빨리 답할 수 있습니다.
반대로 Liveness 경로를 잘못 지정하면 결과가 달라집니다. READY 이전에 컨테이너가 재시작될 수 있고, Events에는 Liveness 실패와 컨테이너 종료 메시지가 같이 나타납니다. 같은 404라도 Readiness에서는 트래픽 제외, Liveness에서는 재시작입니다. 이 차이를 머리에 넣어두면 디버깅 방향이 흔들리지 않습니다.
제가 겪은 Liveness Probe 실패 패턴
가장 위험했던 패턴은 종합 건강검진식 Liveness였습니다. /health 요청 하나에 DB 쿼리, Redis ping, 외부 API 호출, 디스크 체크를 전부 넣는 방식입니다. 겉보기에는 꼼꼼합니다. 그런데 DB가 순간적으로 느려지면 모든 파드가 Liveness 실패로 재시작됩니다. 재시작된 파드는 다시 DB 커넥션을 만들고, 캐시를 채우고, 준비되지 않은 상태에서 또 검사를 받습니다. 문제의 뿌리는 DB 지연인데 증상은 애플리케이션 재시작 폭풍으로 바뀝니다.
제가 쓰는 기준은 꽤 엄격합니다. Liveness는 “이 프로세스를 죽이면 나아지는가?”에 예라고 답할 수 있을 때만 실패시킵니다. 외부 의존성이 실패했지만 프로세스가 요청 큐를 비우고 회복할 수 있다면 Liveness가 아니라 Readiness입니다. 이 기준이 약간 보수적으로 보여도, 운영에서는 불필요한 재시작을 줄이는 쪽이 대체로 더 싸고 안전했습니다.
Liveness: 기본 HTTP 핸들러, 이벤트 루프, 내부 상태 플래그처럼 가볍고 로컬인 검사만 둡니다.
Readiness: DB 연결, 브로커 구독, 필수 설정 로딩, 캐시 워밍업처럼 트래픽 처리 조건을 둡니다.
Startup: 앱 부팅이 느리거나 초기화 단계가 여러 개인 서비스에서 Liveness 개입을 늦춥니다.
외부 API: 핵심 경로가 아니라면 Readiness에서도 과하게 엄격하게 묶지 않습니다. 서킷 브레이커와 graceful degradation이 더 나은 경우가 많습니다.
이 구성을 그대로 복사하기보다, 먼저 앱의 실제 기동 경로를 관찰하세요. 포트가 열리는 시점과 요청을 안전하게 처리할 수 있는 시점은 다를 수 있습니다. 특히 Java, .NET, 대형 Node.js 앱처럼 초기 로딩이 긴 서비스는 포트가 먼저 열리고 내부 초기화가 나중에 끝나는 경우가 있습니다. 이때 Liveness를 빨리 켜면 멀쩡히 뜨는 중인 앱을 kubelet이 반복해서 죽입니다.
검증/결과: 무엇을 보고 정상이라고 판단할까
프로브 설정을 바꾼 뒤 “배포 성공”만 보고 끝내면 위험합니다. 제가 확인하는 기준은 네 가지입니다. 파드가 Ready인지, 재시작이 멈췄는지, Service 엔드포인트에 포함됐는지, 그리고 실패 이벤트가 더 이상 증가하지 않는지입니다. 이 확인 루틴은 짧지만, 운영에서는 진짜 편하더라고요.
NS=default
APP=probe-demo
kubectl rollout status deployment/$APP -n $NS
kubectl get pods -n $NS -l app=$APP -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,PHASE:.status.phase,NODE:.spec.nodeName'
kubectl get endpoints $APP -n $NS -o wide
kubectl get endpointslice -n $NS -l kubernetes.io/service-name=$APP -o wide
kubectl describe deployment $APP -n $NS
kubectl get events -n $NS --field-selector involvedObject.kind=Pod --sort-by=.lastTimestamp
판단은 이렇게 가져갑니다. READY가 모든 컨테이너 수와 일치하면 요청을 받을 준비가 된 상태입니다. RESTARTS가 계속 늘면 Liveness 실패 또는 애플리케이션 크래시를 봅니다. EndpointSlice에 파드 IP가 없으면 Readiness 실패, Service selector 불일치, 포트명 불일치, 또는 라벨 문제를 봅니다. connection refused는 포트와 바인딩 주소, timeout은 앱 지연과 리소스 압박, 404는 경로 불일치부터 확인하세요.
운영 대시보드가 있다면 프로브 실패율만 따로 보는 것도 좋습니다. 다만 프로브 자체가 너무 자주 실행되면 노드와 애플리케이션에 부하가 됩니다. 특히 exec 프로브는 컨테이너 안에서 프로세스를 실행하므로, 파드 수가 많고 주기가 짧으면 비용이 눈에 띄게 늘 수 있습니다. HTTP나 TCP로 충분한 경우라면 굳이 셸 명령을 매번 실행하지 않는 편이 낫습니다.
Kubernetes 프로브 모범 사례와 선택 기준
제가 팀에 리뷰할 때 가장 많이 보는 항목은 세 가지입니다. 첫째, Liveness가 외부 의존성을 검사하지 않는가. 둘째, Readiness가 실제 트래픽 처리 가능성을 반영하는가. 셋째, 느린 기동을 initialDelaySeconds 땜질이 아니라 Startup Probe로 풀었는가. 이 세 가지만 잡아도 Kubernetes 프로브 트러블슈팅의 절반은 줄어듭니다.
DB가 잠깐 끊길 수 있다면: DB 검사는 Readiness Probe에 둡니다. Liveness에서 DB를 때리지 않습니다.
프로세스가 가끔 멈춘다면: 로컬 핸들러 기반의 가벼운 Liveness Probe로 재시작하게 합니다.
경로가 자주 바뀐다면: 비즈니스 API와 헬스 체크 엔드포인트를 분리하고, 라우팅 리팩터링에 영향받지 않는 고정 경로를 씁니다.
서비스 메시나 Ingress를 쓴다면: kubelet의 Pod 프로브, Service 라우팅, 외부 로드밸런서 헬스 체크를 서로 다른 계층으로 보고 분리해서 검증합니다.
파드 수가 많다면: 짧은 periodSeconds와 무거운 exec 프로브 조합을 피합니다. 헬스 체크도 트래픽입니다.
자주 묻는 질문
Q. Liveness와 Readiness를 같은 경로로 써도 되나요? 가능은 하지만 운영 기본값으로 추천하지 않습니다. 같은 경로가 DB나 외부 API를 검사하면 Readiness 실패로 끝날 문제가 Liveness 재시작으로 커질 수 있습니다. 정말 같은 경로를 써야 한다면 그 경로는 로컬 상태만 검사해야 합니다.
Q. TCP Probe와 HTTP Probe 중 뭘 써야 하나요? HTTP 서비스라면 HTTP Probe가 보통 더 낫습니다. 상태 코드와 경로로 의도를 표현할 수 있기 때문입니다. TCP Probe는 포트가 열렸는지만 확인하므로, 앱이 내부 초기화 중이어도 성공할 수 있습니다.
Q. Exec Probe는 언제 쓰나요? HTTP 엔드포인트를 만들기 어려운 배치성 프로세스나 레거시 앱에서 컨테이너 내부 상태 파일·명령 결과를 확인할 때 씁니다. 다만 명령 실행 비용, 이미지 안의 바이너리 존재 여부, 셸 의존성을 반드시 확인해야 합니다.
Q. gRPC 서비스는 어떻게 하나요? gRPC Health Checking Protocol을 구현했다면 Kubernetes v1.27부터 안정화된 gRPC probe를 검토할 수 있습니다. gRPC probe는 HTTP/TCP probe와 달리 named port를 지원하지 않으므로 숫자 포트를 명시해야 합니다. 서비스 이름과 포트 설정을 명확히 하고, HTTP 게이트웨이와 혼동하지 않도록 매니페스트 리뷰를 거치는 편이 좋습니다.
프로브 설계의 기준은 명확합니다. 재시작하면 좋아지는 문제만 Liveness에 넣고, 트래픽만 빼면 되는 문제는 Readiness에 넣고, 느린 기동은 Startup Probe로 보호하세요. 이 원칙을 어기면 프로브는 장애 감지 장치가 아니라 장애 증폭 장치가 됩니다.
일반 웹 서비스라면 저는 이렇게 시작합니다. Liveness는 /livez에서 프로세스와 기본 핸들러만 확인합니다. Readiness는 /readyz에서 DB 연결, 필수 설정, 내부 큐 상태처럼 요청 처리 조건을 확인합니다. 기동이 느린 서비스는 /startupz를 두고, Startup Probe가 성공하기 전까지 Liveness가 개입하지 않게 합니다. 배포 후에는 kubectl describe pod, kubectl logs --previous, kubectl get endpoints, kubectl get endpointslice를 함께 확인합니다. 이 네 가지를 습관처럼 보면 Kubernetes 프로브 트러블슈팅에서 가장 흔한 삽질을 꽤 많이 줄일 수 있습니다.
“컨테이너는 가벼운 VM”이라고들 하지만, 사실 둘은 원리가 완전히 다릅니다. 컨테이너는 가상 머신이 아니라 “격리된 프로세스”일 뿐입니다. 그 격리를 만드는 두 가지 리눅스 커널 기능 — namespaces와 cgroups — 를 이해하면 LXC·Docker가 왜 그렇게 가벼운지, 그리고 왜 Docker-in-LXC에 특별한 설정이 필요한지가 보입니다.
1. 컨테이너 vs VM — 근본 차이
VM
컨테이너
격리 방식
하드웨어 가상화
커널 기능(격리된 프로세스)
커널
게스트마다 별도
호스트와 공유
부팅
OS 부팅(수십 초)
프로세스 시작(즉시)
오버헤드
큼
거의 없음
컨테이너는 호스트 커널을 그대로 공유하면서, “이 프로세스는 자기만의 세상에 있는 것처럼” 보이게 속입니다. 그 속임수의 두 축이 namespaces와 cgroups입니다.
2. namespaces — “무엇을 볼 수 있는가”
namespace는 프로세스가 보는 시야를 격리합니다. 종류별로 각각 다른 자원을 가립니다.
namespace
격리 대상
PID
프로세스 목록(자기 PID 1)
NET
네트워크(자기 IP·인터페이스)
MNT
파일시스템 마운트
UTS
호스트명
IPC
프로세스 간 통신
USER
사용자·권한(UID 매핑)
그래서 컨테이너 안에서 ps를 치면 자기 프로세스만 보이고, 자기만의 IP를 갖습니다. 실제로는 호스트의 한 프로세스일 뿐인데도요. USER namespace가 바로 “unprivileged 컨테이너”의 핵심 — 컨테이너의 root를 호스트의 비특권 사용자로 매핑해 보안을 높입니다.
Proxmox에서 LXC에 cores·memory를 지정하면, 내부적으로 cgroups가 그 한도를 강제합니다. vCPU 오버커밋이 가능한 것도 cgroups가 실제 사용량을 조율하기 때문입니다.
4. 그래서 Docker-in-LXC가 까다롭다
컨테이너가 이 커널 기능들에 의존하다 보니, 컨테이너 안에서 또 컨테이너(LXC 안 Docker)를 돌리려면 커널 기능 접근을 열어줘야 합니다.
nesting=1: 중첩된 namespace 생성 허용
keyctl=1: Docker가 쓰는 커널 키링 접근 허용
원리를 알고 나면 이 플래그들이 왜 필요한지 자연스럽게 이해됩니다 — 컨테이너의 격리 메커니즘을 한 겹 더 쌓는 것이니까요.
5. 정리
컨테이너 = namespaces(시야 격리) + cgroups(자원 제한)로 만든 “격리된 프로세스”입니다. VM처럼 커널을 통째로 복제하지 않으니 가볍고 빠른 거죠. 이 원리 하나를 잡으면 LXC·Docker의 동작, unprivileged의 의미, nesting 플래그의 이유가 전부 하나로 꿰어집니다.
지난 ZFS 글에서 “스냅샷은 백업이 아니다”라고 했습니다. 그럼 Proxmox에서 진짜 백업은 어떻게 할까요? 답은 vzdump입니다. VM·컨테이너를 통째로 백업하는 Proxmox 내장 도구인데, 솔직히 저도 아직 예약 백업을 안 걸어둔 상태라 이번 기회에 제대로 정리합니다.
1. vzdump이 하는 일
vzdump은 VM/LXC 전체를 하나의 아카이브로 백업합니다. 스냅샷(같은 디스크 안의 되돌리기)과 달리, 별도 스토리지(NAS 등)에 복제본을 만드는 진짜 백업입니다.
디스크가 통째로 죽어도 복구 가능
다른 노드·다른 서버로 이전 가능
보관 개수를 정해 자동 로테이션
2. 백업 모드 3가지
모드
다운타임
특징
snapshot
거의 없음
실행 중 백업(권장)
suspend
잠깐 정지
중간 안전성
stop
완전 정지
가장 일관적
대부분 snapshot 모드면 충분합니다. 실행 중인 서비스를 멈추지 않고 백업하니까요(단, DB처럼 일관성이 중요하면 stop 고려).
3. 수동 백업 — 한 줄
# LXC 113을 NAS 스토리지에 snapshot 모드로 백업
vzdump 113 --mode snapshot --storage nas-backup --compress zstd
# 여러 개 한 번에
vzdump 112 113 124 --mode snapshot --storage nas-backup --compress zstd
--compress zstd는 빠르고 압축률도 좋아 요즘 표준입니다. 백업 파일은 지정한 스토리지(예: NAS)에 .vma.zst(VM) / .tar.zst(LXC)로 쌓입니다.
중요: 백업은 복구 테스트까지 해봐야 진짜 백업입니다. 한 번쯤 테스트 ID로 복구해 정상 부팅을 확인해 두세요.
6. 정리 — 스냅샷 + vzdump = 완성
스냅샷은 위험한 작업 직전의 빠른 되돌리기, vzdump은 다른 스토리지에 두는 진짜 백업입니다. 둘은 역할이 다르니 둘 다 써야 합니다. 저처럼 “스냅샷은 하는데 예약 백업은 아직”인 분이라면, 오늘 Datacenter → Backup에서 매일 백업 하나만 걸어두세요. 미래의 내가 고마워할 겁니다.
게임 리뷰 분석을 기술 글답게 만들려면 “재미있다”, “최적화가 별로다”에서 멈추면 아쉽습니다. 독자가 실제로 알고 싶은 건 더 구체적이거든요. 내 PC에서 버틸지, 특정 구간의 끊김이 그래픽 카드 문제인지 저장 장치 문제인지, 튕겼을 때 원인을 좁힐 수 있는지, 모드를 깔았다가 업데이트 후 망가져도 되돌릴 수 있는지 말이죠.
저는 서버실에서 장애 보고서를 쓰고, 홈랩에서 이런저런 시스템을 굴리며 13년 가까이 로그와 지표를 붙들고 살았습니다. 그 습관으로 게임을 보면 리뷰의 초점이 조금 달라집니다. 게임은 감상 대상이기도 하지만, 동시에 CPU, GPU, 저장 장치, 드라이버, 런타임, 파일 구조가 같이 움직이는 작은 시스템입니다. 좋은 기술 리뷰는 그 시스템이 언제 안정적이고, 언제 무너지고, 어디까지 독자가 직접 회복할 수 있는지를 보여줘야 합니다.
그래서 이 글에서는 성능, 안정성, 모딩 지원을 따로 보되 마지막에는 하나의 판단으로 묶겠습니다. 숫자를 꾸며내지는 않겠습니다. 대신 실제 리뷰에서 바로 복사해 쓸 수 있는 명령어, 설정 파일, 로그 해석 기준, 실패 패턴을 최대한 구체적으로 남기겠습니다. 관련 벤치마크 글을 운영한다면 이 글을 기준 문서로 내부 링크해두면 독자 흐름도 훨씬 좋아집니다.
캡션: 게임 리뷰 분석을 성능, 안정성, 모딩 지원 관점으로 나눠 보는 전체 흐름입니다.
1. 게임 리뷰 분석은 세 줄 평이 아니라 관측 설계입니다
기술 블로거가 먼저 정해야 할 것은 도구가 아니라 질문입니다. “이 게임이 빠른가?”보다 “어떤 조건에서 체감이 무너지는가?”가 낫고, “잘 튕기는가?”보다 “같은 조건에서 재현되는 장애인가?”가 낫습니다. 이 차이가 글의 신뢰도를 가릅니다.
제가 쓰는 기본 축은 세 가지입니다. Performance는 평균 FPS보다 프레임타임의 일관성을 봅니다. Stability는 크래시 자체보다 재현 조건과 로그의 반복성을 봅니다. Modding Support는 모드 개수보다 설치, 비활성화, 롤백, 업데이트 후 복구 가능성을 봅니다.
분석 축
리뷰어가 확인할 것
좋은 질문
피해야 할 단정
게임 성능 리뷰
FPS, 프레임타임, 1% low 성격의 하위 구간, CPU/GPU 사용률, VRAM, 디스크 I/O
세 축을 분리하는 이유는 원인이 섞이기 쉽기 때문입니다. 예를 들어 자동 저장 순간 끊김은 GPU 옵션 문제가 아니라 디스크 쓰기, 압축, 세이브 직렬화 문제일 수 있습니다. 모드 적용 후 크래시는 게임 자체 안정성보다 DLL 주입, 스크립트 로더 버전, 원본 파일 덮어쓰기 문제일 수 있고요. 리뷰에서 이 구분을 해주면 독자는 “내가 겪는 문제와 같은 종류인지”를 훨씬 빨리 판단합니다.
2. 게임 성능 리뷰는 평균 FPS보다 프레임타임을 먼저 봅니다
FPS는 결과값이고 프레임타임은 흐름입니다. 평균 FPS가 높아도 프레임타임 그래프가 톱니처럼 솟으면 체감은 거칠어집니다. 반대로 평균은 아주 높지 않아도 프레임 간격이 일정하면 플레이는 안정적으로 느껴질 수 있습니다. 저는 리뷰에서 평균 FPS를 제목처럼 쓰지 않고, 프레임타임을 본문 판단의 중심에 둡니다.
Linux, Steam Deck, Proton 환경에서는 MangoHud가 리뷰용 관측 도구로 꽤 좋습니다. 다만 처음부터 센서를 전부 켜면 오버레이가 스크린샷을 잡아먹습니다. 리뷰 목적이라면 화면 캡처용 설정과 로그 수집용 설정을 나눠 두는 편이 낫습니다. 이거 진짜 편하더라고요.
# Steam 실행 옵션: 오버레이를 켭니다.
mangohud %command%
# Proton 로그까지 함께 남길 때
PROTON_LOG=1 mangohud %command%
# 비 Steam 실행 파일 테스트
mangohud ./GameExecutable
# MangoHud 설정 디렉터리
mkdir -p ~/.config/MangoHud
nano ~/.config/MangoHud/MangoHud.conf
아래 설정은 리뷰 초안 단계에서 쓰기 좋은 최소형입니다. FPS, 프레임타임, CPU/GPU, RAM/VRAM만 보고, 로그는 별도 폴더에 남깁니다. MangoHud 설정 파일의 output_folder에는 셸의 ~가 항상 기대대로 풀린다고 가정하지 말고, 실제 홈 경로를 적는 편이 안전합니다.
여기서 주의할 점이 있습니다. 로그 길이를 무작정 길게 잡으면 비교가 어려워집니다. 같은 저장 지점에서 60초, 같은 이동 루트에서 90초처럼 구간을 고정해야 합니다. 리뷰마다 측정 시간이 다르면 “이 게임은 평균이 높다/낮다”보다 “서로 다른 장면을 비교했다”가 되어버립니다.
상황
먼저 볼 지표
의심할 원인
리뷰 문장 방향
평균 FPS는 높은데 순간적으로 끊김
프레임타임 스파이크, VRAM 사용량, 저장 시점
셰이더 컴파일, 스트리밍, 자동 저장, VRAM 압박
평균 성능보다 일관성이 문제라고 설명
GPU 사용률이 계속 높고 옵션을 낮추면 개선
GPU 사용률, 해상도, 업스케일링, 그림자/반사 옵션
GPU 병목
그래픽 옵션 조정 효과가 있는 게임으로 평가
CPU 일부 코어만 높고 GPU가 놀고 있음
CPU 코어별 사용률, 프레임타임, 군중/AI 구간
메인 스레드 병목, 시뮬레이션 부하
해상도를 낮춰도 개선이 작을 수 있다고 안내
자동 저장 아이콘과 함께 끊김
iostat await, 디스크 쓰기, 프레임타임
저장 처리, 압축, 동기식 파일 쓰기
저장 장치와 세이브 처리 영향을 분리해서 설명
3. 리뷰용 측정 루틴: 같은 조건을 반복해야 글이 단단해집니다
제가 가장 많이 고친 리뷰 초안은 “여러 번 해봤는데 대충 비슷했다”는 식의 글이었습니다. 독자는 그 말을 믿고 싶어도 판단할 재료가 없습니다. 기술 리뷰라면 테스트 루틴을 간단히라도 남겨야 합니다. 고급 장비보다 중요한 건 반복 가능한 절차입니다.
해상도, 화면 모드, 그래픽 프리셋, 업스케일링 옵션을 고정합니다.
드라이버, OS, 커널, Proton 또는 런타임 버전을 기록합니다.
게임 내 같은 저장 지점, 같은 이동 경로, 같은 전투나 로딩 구간을 씁니다.
첫 실행과 두 번째 실행을 구분합니다. 셰이더 캐시 때문에 양상이 달라질 수 있습니다.
평균보다 튄 지점을 캡처하고, 그 순간의 시스템 지표를 같이 봅니다.
Linux 환경에서는 sysstat 도구의 sar, iostat, pidstat 조합만으로도 병목 방향을 꽤 좁힐 수 있습니다. 아래 명령은 게임 실행 직후 별도 터미널에서 돌리는 용도입니다.
# sysstat 설치 예시: 배포판에 따라 패키지 관리자는 다릅니다.
# Debian/Ubuntu 계열
sudo apt install sysstat
# Fedora 계열
sudo dnf install sysstat
# Arch 계열
sudo pacman -S sysstat
# 게임 프로세스 PID 확인
pgrep -af GameExecutable
# CPU 전체 사용률과 iowait 관찰
sar -u 1 60 | tee ~/game-review-sar.log
# 디스크 지연, 큐, 사용률 관찰
iostat -xz 1 60 | tee ~/game-review-iostat.log
# 최신 게임 프로세스 하나를 대상으로 CPU, 메모리, 디스크 I/O 추적
pidstat -rud -p $(pgrep -n GameExecutable) 1 60 | tee ~/game-review-pidstat.log
iostat -xz에서 await가 프레임타임 스파이크와 같은 타이밍에 튀면 저장 장치 대기를 의심할 수 있습니다. 다만 %util만 보고 단정하면 위험합니다. 빠른 SSD에서도 순간적인 동기 쓰기나 세이브 압축 때문에 체감 끊김이 생길 수 있고, 반대로 %util이 높아 보여도 실제 플레이 영향은 작을 수 있습니다. 저는 반드시 프레임타임 그래프와 이벤트 시점을 같이 맞춰 봅니다.
프로세스 이름이 자주 바뀌는 게임은 pgrep이 빗나갈 수 있습니다. 이럴 때는 Steam 실행 옵션에 wrapper 스크립트를 넣거나, 게임 실행 직후 ps -eo pid,comm,args –sort=-%cpu | head로 실제 프로세스를 확인하는 편이 안전합니다. 리뷰 글에는 “GameExecutable 기준”이라고 쓰지 말고 실제 프로세스명이나 확인 방법을 적어주세요.
캡션: MangoHud, sar, iostat, pidstat를 함께 사용하는 리뷰 측정 구성 예시입니다.
4. 게임 안정성 평가는 이벤트 뷰어와 안정성 모니터를 같이 봅니다
Windows 환경에서는 오버레이만으로 안정성을 판단하기 어렵습니다. 크래시가 났다면 이벤트 뷰어에서 Application Error를 보고, 안정성 모니터에서 같은 문제가 반복되는지 확인하는 게 빠릅니다. 리뷰에 모든 로그를 붙일 필요는 없지만, 오류 모듈명과 발생 조건은 기록해두는 편이 좋습니다.
# 최근 24시간 Application Error 이벤트 확인
Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Application Error'; StartTime=(Get-Date).AddDays(-1)} |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
# 게임 이름으로 이벤트 메시지 필터링
Get-WinEvent -LogName Application -MaxEvents 200 |
Where-Object {$_.Message -match 'GameExecutable|Faulting application'} |
Select-Object TimeCreated, Id, Message
Windows 로그를 읽을 때 흔한 실수는 “Faulting module”만 보고 원인을 확정하는 겁니다. 예를 들어 특정 DLL이 보인다고 해서 그 DLL이 항상 범인은 아닙니다. 게임, 드라이버, 오버레이, 안티치트, 모드 로더가 같은 프로세스 안에서 엮일 수 있습니다. 리뷰에서는 “해당 모듈이 마지막 오류 구간에 반복적으로 등장했다” 정도로 쓰고, 드라이버나 모드와의 인과관계는 재현 테스트로 확인해야 합니다.
간단한 분리 테스트는 이렇게 갑니다. 오버레이를 끄고 한 번, 모드를 끄고 한 번, 그래픽 프리셋을 낮추고 한 번, 같은 저장 지점에서 다시 실행합니다. 네 가지를 동시에 바꾸면 원인을 잃어버립니다. 서버 장애 분석에서 설정을 한꺼번에 바꾸면 사후 분석이 망가지는 것과 같습니다.
5. Proton 로그는 마지막 줄보다 반복 패턴을 봐야 합니다
Linux/Proton 환경에서 크래시를 다룰 때는 Proton 로그가 유용합니다. 다만 로그가 길고 경고가 많습니다. warn이 보인다고 전부 문제는 아닙니다. Wine/Proton 로그에는 정상 실행 중에도 경고가 쌓입니다. 제가 보는 기준은 마지막 줄이 아니라 “같은 구간에서, 같은 문자열 묶음이, 여러 번 반복되는가”입니다.
# Steam 실행 옵션
PROTON_LOG=1 %command%
# 홈 디렉터리에 생성된 Steam Proton 로그 확인
ls -lh ~/steam-*.log
# 오류 후보만 추려 보기
grep -Ei 'err:|warn:|crash|exception|fault|terminate|access violation' ~/steam-*.log | tail -n 120
# 같은 패턴이 반복되는지 대략 집계
grep -Eio 'err:[^ ]+|warn:[^ ]+|exception|access violation|fault' ~/steam-*.log | sort | uniq -c | sort -nr | head
Proton 로그를 리뷰에 쓸 때는 원문을 길게 붙이지 않는 편이 좋습니다. 독자는 로그 전문보다 판단을 원합니다. 예를 들어 “Proton 로그의 마지막 구간에서 같은 access violation 패턴이 반복됐고, 모드를 끈 뒤 같은 저장 지점에서는 재현되지 않았습니다”처럼 조건과 변화만 적어도 충분히 실용적입니다.
반대로 한 번만 난 오류, 다른 구간에서는 재현되지 않는 오류, 게임 업데이트 직후 사라진 오류는 조심해서 다뤄야 합니다. “제 환경에서는 한 차례 발생했지만 반복 재현은 되지 않았습니다”라고 쓰는 게 낫습니다. 이 문장은 약해 보이지만, 사실 리뷰 신뢰도를 올립니다. 단정하지 않는 것도 기술 글의 실력입니다.
6. 실제 케이스: 자동 저장 순간 끊김을 GPU 문제로 착각했습니다
제가 겪은 가장 헷갈리는 패턴은 자동 저장 순간의 끊김이었습니다. 처음에는 그림자 옵션과 반사 옵션을 낮췄습니다. 평균 FPS는 조금 달라졌지만 끊기는 순간은 그대로였습니다. 그때 프레임타임 그래프와 iostat 로그를 나란히 놓고 보니 자동 저장 아이콘이 뜨는 순간 await가 같이 솟았습니다. 그래픽 부하가 아니라 저장 처리와 겹친 이벤트성 병목에 가까웠던 겁니다.
이런 경우 리뷰 문장은 이렇게 바뀌어야 합니다. “최적화가 나쁘다”가 아니라 “특정 자동 저장 시점에서 프레임타임 스파이크가 반복됐고, 그래픽 옵션 조정만으로는 사라지지 않았습니다. 디스크 활동 증가와 같은 타이밍이라 저장 처리 영향 가능성이 있습니다.” 이 정도면 단정은 피하면서도 독자가 자기 환경을 점검할 단서를 얻습니다.
같은 저장 지점에서 반복되는 끊김이면 게임 이벤트와 연결해서 봅니다.
옵션을 낮춰도 같은 타이밍에 끊기면 GPU 병목만으로 설명하지 않습니다.
첫 실행에서만 심하고 두 번째 실행에서 줄어들면 셰이더 캐시나 스트리밍을 의심합니다.
모드 적용 후에만 생기면 모드 로더, 스크립트, 파일 덮어쓰기 여부를 분리합니다.
온라인 기능과 함께 생기면 네트워크, 인증, 클라우드 저장도 후보에 넣습니다.
기술 블로거의 장점은 바로 이런 분리입니다. 감상 리뷰는 “끊긴다”에서 끝날 수 있지만, 분석 리뷰는 “언제, 무엇과 함께, 무엇을 바꿨을 때 사라지는가”까지 갑니다.
7. 모딩 지원 분석은 설치 난이도보다 복구 난이도를 봅니다
모딩 지원 분석에서 제가 가장 중요하게 보는 것은 모드 설치 성공률이 아닙니다. 원래 상태로 돌아가는 길입니다. 모드가 잘 깔려도 원본 파일을 덮어쓰고, 제거 방법이 불명확하고, 업데이트 후 세이브가 깨진다면 기술 리뷰에서는 높은 점수를 주기 어렵습니다.
모딩 친화적인 게임은 보통 원본 파일, 사용자 설정, 세이브, 모드 파일을 분리합니다. 반대로 실행 파일 폴더에 DLL과 스크립트를 직접 넣고, 제거하려면 기억에 의존해 파일을 지워야 하는 구조라면 주의가 필요합니다. 특히 기술 블로거라면 “모드가 많다”보다 “모드가 망가졌을 때 회복 가능한가”를 써야 독자에게 도움이 됩니다.
# 테스트 전 백업 폴더를 한 번만 계산해서 재사용합니다.
BACKUP_DIR=~/game-review-backup/$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
# 설정과 세이브 백업 예시: 실제 게임 경로에 맞게 바꿔 쓰세요.
cp -a ~/.config/GameName "$BACKUP_DIR/config" 2>/dev/null || true
cp -a ~/.local/share/GameName "$BACKUP_DIR/save" 2>/dev/null || true
cp -a ~/.steam/steam/steamapps/common/GameName "$BACKUP_DIR/install-snapshot" 2>/dev/null || true
# 모드 적용 전후 변경 파일 확인
find ~/.config/GameName ~/.local/share/GameName -type f -printf '%TY-%Tm-%Td %TH:%TM %p\n' 2>/dev/null | sort | tail -n 50
위 예시는 일부러 단순하게 썼습니다. 다만 경로에 공백이 섞일 수 있어 백업 변수는 따옴표로 감쌌습니다. 모든 게임 폴더를 통째로 백업하면 디스크 공간을 많이 먹습니다. 대형 게임에서는 원본 설치 폴더 전체 백업보다 설정, 세이브, 모드 폴더, 변경 파일 목록을 남기는 쪽이 현실적입니다.
결정 지점
A를 선택할 때
B를 선택할 때
리뷰에 남길 표현
백업 범위
세이브·설정만 백업: 용량을 아끼고 빠르게 테스트할 때
설치 폴더까지 백업: 원본 파일 덮어쓰기 모드가 있을 때
복구 가능성은 모드 방식에 따라 달랐습니다
모드 배포
Workshop·공식 로더: 비활성화와 업데이트 추적이 쉬울 때
수동 복사: 버전 고정이나 특수 패치가 필요할 때
설치보다 제거와 충돌 관리가 관건입니다
테스트 순서
하나씩 적용: 원인 분리가 중요할 때
묶음 적용: 실제 플레이 환경을 빠르게 재현할 때
문제가 생기면 단일 모드 단위로 되돌려 확인했습니다
업데이트 대응
자동 업데이트 유지: 일반 독자 환경을 반영할 때
버전 고정: 모드 호환성이 핵심일 때
패치 후 모드 유지 비용이 있는 게임입니다
출처 불명의 실행 파일을 요구하는 모드는 특히 조심해야 합니다. 기술 리뷰에서 “잘 됩니다”만 쓰면 독자는 보안 위험을 놓칠 수 있습니다. 모드 관리자는 공식 배포처, 해시 제공 여부, 소스 공개 여부, 커뮤니티 검증 정도를 함께 봐야 합니다. 모든 독자가 샌드박스나 별도 테스트 PC를 갖고 있지는 않으니까요.
8. 흔한 실패 모드와 근본 원인 후보
리뷰를 오래 쓰다 보면 비슷한 실패 패턴이 반복됩니다. 중요한 건 증상 이름을 붙이는 게 아니라 원인 후보를 좁히는 겁니다. 아래 표는 제가 초안 검토할 때 자주 쓰는 체크리스트입니다.
증상
흔한 근본 원인
확인 방법
추천 판단
첫 전투나 첫 지역 진입 때만 큰 끊김
셰이더 컴파일, 에셋 스트리밍, 캐시 생성
같은 구간을 두 번째 실행했을 때 줄어드는지 확인
첫 실행 경험과 반복 플레이 경험을 구분해서 씁니다
해상도를 낮춰도 FPS가 거의 안 오름
CPU 병목, 메인 스레드 병목, 시뮬레이션 부하
GPU 사용률이 낮은지, CPU 일부 코어가 높은지 확인
GPU 업그레이드만으로 해결될 문제처럼 쓰지 않습니다
저장·로딩 순간 프레임타임 급등
동기식 파일 쓰기, 압축, 클라우드 저장, 느린 저장 장치
저장 아이콘, iostat await, 디스크 쓰기 타이밍 비교
저장 장치와 게임 저장 구조의 영향을 함께 설명합니다
모드 적용 후 특정 메뉴에서 크래시
UI 스크립트 충돌, 로더 버전 불일치, 의존성 누락
모드를 하나씩 끄고 같은 메뉴 재진입
게임 자체 안정성과 모드 안정성을 분리합니다
오버레이를 켰을 때만 이상 동작
후킹 충돌, 안티치트, 캡처 도구 충돌
Steam, Discord, GPU 오버레이를 각각 끄고 재현
측정 도구가 결과에 영향을 줬을 가능성을 적습니다
이 표를 그대로 본문에 다 넣을 필요는 없습니다. 하지만 리뷰 작성자는 이 정도 분기표를 머릿속에 갖고 있어야 합니다. 그래야 “끊김이 있습니다”라는 문장을 “첫 실행의 셰이더 캐시성 끊김에 가깝습니다” 또는 “저장 이벤트와 겹치는 반복 스파이크입니다”로 바꿀 수 있습니다.
9. 결과 정리: 독자가 바로 판단할 수 있는 형태로 씁니다
결과 섹션은 문장이 화려할수록 오히려 약해질 때가 많습니다. 저는 리뷰 말미에 항상 같은 형식의 작은 판정표를 둡니다. 독자가 자기 환경과 비교하기 쉽기 때문입니다.
테스트 환경: OS, CPU, GPU, RAM, 저장 장치 종류, 드라이버 또는 런타임 버전을 적었는가
그래픽 조건: 해상도, 화면 모드, 프리셋, 업스케일링, 프레임 제한 여부를 적었는가
측정 구간: 저장 지점, 이동 루트, 전투 구간, 반복 횟수를 설명했는가
성능 해석: 평균 FPS와 프레임타임 스파이크를 구분했는가
안정성 해석: 한 번 발생한 문제와 반복 재현 문제를 나눴는가
모딩 해석: 설치, 비활성화, 충돌, 롤백, 업데이트 후 복구를 확인했는가
한계 고지: 내 환경에서 확인한 범위와 확인하지 못한 범위를 분리했는가
제가 추천하는 결론 문장 구조는 단순합니다. “이럴 땐 추천, 저럴 땐 보류”가 들어가야 합니다. 예를 들어 프레임타임이 안정적이고 크래시 재현이 없으며 모드 롤백도 쉬운 게임은 기술적으로 추천할 수 있습니다. 평균 FPS는 높지만 특정 이벤트에서 반복 스파이크가 있고 원인을 피하기 어렵다면 고주사율 환경의 독자에게는 보수적으로 권해야 합니다. 모딩은 풍부하지만 원본 파일 덮어쓰기와 수동 복구가 필수라면 초보자에게는 조심하라고 말해야 합니다.
캡션: 성능 로그, 안정성 이벤트, 모딩 체크리스트를 한 화면에서 정리한 결과 대시보드 예시입니다.
10. 게임 리뷰 분석 추천 기준: 이럴 땐 A, 저럴 땐 B
리뷰의 마지막 판단은 애매하게 열어두지 않는 편이 좋습니다. 단, 자신이 확인한 범위 안에서만 단호해야 합니다. 제가 쓰는 기준은 아래와 같습니다.
독자 상황
추천 판단
이유
짧은 리뷰, 빠른 구매 판단이 목적
MangoHud나 기본 오버레이로 FPS와 프레임타임만 확인
복잡한 시스템 로그보다 체감 끊김 여부가 먼저입니다
기술 블로그용 심층 리뷰
프레임타임 로그와 sar, iostat, pidstat를 함께 기록
CPU, GPU, 저장 장치 후보를 분리할 수 있습니다
크래시 제보가 많은 게임
이벤트 로그나 Proton 로그를 켜고 같은 구간을 반복
한 번의 크래시보다 재현성이 판단의 핵심입니다
모드가 핵심인 RPG·시뮬레이션
설치보다 비활성화, 백업, 업데이트 후 복구를 먼저 확인
오래 플레이할수록 유지보수 비용이 커집니다
안티치트나 온라인 기능이 강한 게임
오버레이, 모드, 후킹 도구 사용을 보수적으로 접근
측정 도구나 모드가 실행 안정성에 영향을 줄 수 있습니다
제가 독자에게 직접 권한다면 이렇게 말하겠습니다. “구매 전 성능이 궁금한 독자”에게는 평균 FPS보다 프레임타임 스파이크 유무를 먼저 보라고 하겠습니다. “문제가 생겼을 때 해결하며 플레이할 독자”에게는 로그와 재현 조건이 있는 리뷰를 고르라고 하겠습니다. “모드 플레이가 목적인 독자”에게는 모드 수보다 제거와 롤백이 쉬운 게임을 고르라고 하겠습니다.
11. 자주 묻는 질문
Q. 게임 성능 리뷰에서 평균 FPS만 적으면 안 되나요?
짧은 인상평이라면 가능하지만, 분석 글이라면 부족합니다. 평균 FPS는 전체 요약이고 프레임타임은 체감의 흔들림입니다. 특히 “숫자는 높은데 끊긴다”는 불만은 평균 FPS만으로 설명하기 어렵습니다.
Q. 1% low나 0.1% low를 꼭 써야 하나요?
도구가 안정적으로 제공하고 측정 구간을 고정했다면 도움이 됩니다. 다만 짧은 구간에서 얻은 하위 백분위 수치는 튀는 이벤트 하나에 크게 흔들릴 수 있습니다. 저는 수치만 던지기보다 어떤 장면에서 하위 구간이 나빠졌는지 같이 씁니다.
Q. 크래시 로그를 못 읽겠으면 어떻게 하나요?
마지막 오류 한 줄을 해석하려고 애쓰기보다 반복 조건을 먼저 보세요. 같은 저장 지점, 같은 옵션, 같은 모드 조합에서 다시 발생한다면 리뷰에 쓸 가치가 있습니다. 반복되지 않으면 “확인됐지만 재현되지 않음”으로 남기는 편이 정직합니다.
Q. 모딩 지원은 모드 개수로 판단하면 되나요?
아닙니다. 모드 개수는 생태계의 크기를 보여주지만, 관리 품질을 보장하지는 않습니다. 설치 문서, 비활성화 방식, 원본 파일 보호, 버전 호환성, 세이브 복구 가능성이 더 중요합니다.
Q. 벤치마크 수치를 넣지 않으면 글이 약해 보이지 않나요?
수치가 있으면 좋지만, 조건 없는 수치는 오히려 위험합니다. 확인하지 않은 평균값을 꾸미는 것보다 테스트 환경, 측정 구간, 프레임타임 패턴, 재현 조건을 정확히 적는 쪽이 독자에게 더 쓸모 있습니다.
12. 마무리: 기술 블로거라면 감상보다 원인 분리를 보여주세요
게임 리뷰 분석은 감상문과 장애 분석의 중간쯤에 있습니다. 재미와 그래픽 이야기도 필요하지만, 기술 블로거의 가치는 원인 분리에 있습니다. 성능은 평균 FPS보다 프레임타임의 안정성을 보고, 안정성은 크래시 횟수보다 재현 조건을 보고, 모딩은 설치 성공보다 복구 가능성을 봐야 합니다.
짧은 리뷰라면 MangoHud나 기본 오버레이로 FPS와 프레임타임을 잡는 것만으로도 충분합니다. 깊이 있는 리뷰라면 sar, iostat, pidstat 같은 시스템 지표를 같이 남기세요. Windows라면 이벤트 뷰어와 안정성 모니터를 확인하고, Proton 환경이라면 PROTON_LOG를 켜서 반복 패턴을 봅니다. 모딩이 핵심인 게임이라면 백업, 비활성화, 업데이트 후 복구 과정을 반드시 확인하세요.
제가 내리는 기준은 분명합니다. 프레임타임이 안정적이고, 크래시가 반복 재현되지 않으며, 모드 제거와 롤백이 쉽다면 기술적으로 추천할 만합니다. 평균 성능은 높지만 특정 이벤트에서 반복적으로 끊기거나, 로그상 같은 문제가 재현되거나, 모드 복구가 수동 기억에 의존한다면 독자에게 조건부 추천으로 안내해야 합니다. 좋은 리뷰는 확신을 파는 글이 아니라, 독자가 자기 환경에서 판단할 수 있게 근거를 정리해주는 글입니다.
캡션: 게임 리뷰를 작성할 때 성능, 안정성, 모딩 항목별로 확인할 핵심 기준 요약입니다.
OpenStack 스토리지는 Glance(이미지)로 시작 → Cinder(블록)로 디스크 확장 → Swift(오브젝트)로 파일 보관, 이 세 축이 전부입니다. “블록은 디스크처럼, 오브젝트는 드라이브처럼”만 기억하면 헷갈릴 일이 없습니다. 홈랩 랩에선 Glance + Cinder(LVM)부터 붙여보는 걸 권합니다.
학습용 VM을 매번 ISO로 OS 설치하고, 계정 만들고, SSH 키 넣고, IP 잡고… 이걸 반복하다 지치신 적 있나요? 저는 Proxmox cloud-init 템플릿으로 이 과정을 없앴습니다. 클론 한 번이면 부팅과 동시에 설정까지 끝난 VM이 나옵니다. 실제로 제 홈랩 학습 VM들이 이 방식으로 찍혀 나옵니다.
1. cloud-init이 뭔가
cloud-init은 클라우드 이미지가 첫 부팅 때 자기 자신을 설정하게 해주는 표준 도구입니다. 호스트명·사용자·비밀번호·SSH 키·네트워크(IP)를 부팅 시 주입받아 자동 구성합니다. AWS·OpenStack이 인스턴스를 찍어내는 원리와 같은 것을, Proxmox에서도 그대로 씁니다.
2. 왜 템플릿 + cloud-init인가
속도: OS 설치 0분. 클론 → 부팅이면 끝.
일관성: 항상 같은 베이스에서 시작(학습·실습에 최적).
자동 설정: SSH 키·IP까지 부팅과 동시에.
실제로 제 홈랩엔 Ubuntu, Rocky 등 cloud-init 템플릿이 준비돼 있어서, 실습이 필요하면 몇 초 만에 새 VM을 뽑습니다.
3. 템플릿 만들기 (한 번만)
배포판이 제공하는 cloud 이미지(.img/.qcow2)를 받아서 VM에 붙이고 템플릿으로 변환합니다.
# 1) cloud 이미지 다운로드 (예: Ubuntu)
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# 2) 빈 VM 생성
qm create 9000 --name ubuntu-2404-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
# 3) 이미지를 디스크로 임포트
qm importdisk 9000 noble-server-cloudimg-amd64.img local-zfs
qm set 9000 --scsihw virtio-scsi-single --scsi0 local-zfs:vm-9000-disk-0
# 4) cloud-init 드라이브 + 부팅 설정
qm set 9000 --ide2 local-zfs:cloudinit
qm set 9000 --boot c --bootdisk scsi0 --serial0 socket --vga serial0
# 5) 템플릿으로 변환
qm template 9000
4. 템플릿에서 VM 즉시 찍어내기
이제 실습 VM이 필요할 때마다 클론 + cloud-init 값만 지정하면 됩니다.
# 템플릿(9000)에서 새 VM(201) 풀 클론
qm clone 9000 201 --name study-node1 --full
# cloud-init: 사용자·SSH키·고정 IP 주입
qm set 201 --ciuser myuser --sshkeys ~/.ssh/id_rsa.pub
qm set 201 --ipconfig0 ip=192.168.x.201/24,gw=192.168.x.1
qm start 201
부팅되면 지정한 사용자·SSH 키·IP가 이미 적용돼 있어 바로 SSH 접속됩니다. OS 설치도, 초기 설정도 없습니다.
cloud-init 템플릿은 홈랩 생산성을 통째로 바꿉니다. 한 번 템플릿을 만들어 두면, 이후엔 클론 한 줄로 설정 완료된 VM이 나옵니다. 학습·실습으로 VM을 자주 만드는 분이라면 무조건 세팅해 두세요. 그리고 이 원리는 그대로 프라이빗 클라우드(OpenStack)의 인스턴스 배포로 이어집니다.
LangChain 오류를 오래 들여다보면 한 가지 패턴이 보입니다. 실제 장애의 상당수는 “모델이 이상해서”가 아니라, 모델에 도달하기 전후의 계약이 깨져서 납니다. 입력 키가 하나 빠졌거나, 프롬프트 변수명이 바뀌었거나, retriever가 엉뚱한 문서를 가져왔거나, 출력 파서가 모델의 자연어 한 줄을 JSON으로 착각하는 식이죠.
인프라 엔지니어 관점에서 보면 LangChain은 단순한 체인 문법이 아니라 API 서버, 큐, 환경변수, 네트워크, 관측성까지 이어지는 실행 파이프라인에 가깝습니다. 이 글은 작은 RAG(Retrieval-Augmented Generation, 검색 증강 생성) 앱을 운영하며 정리한 실전 기준입니다. “어제 되던 코드가 오늘 왜 안 되지?” 싶은 순간에 바로 확인할 수 있도록 명령어와 판단 기준을 함께 넣었습니다.
LangChain 앱에서 입력, 프롬프트, 모델, 파서, 관측 도구가 어떻게 이어지는지 보여주는 개요 이미지입니다.
1. LangChain 오류는 컴포넌트보다 계약부터 봐야 합니다
LangChain은 Prompt Template(프롬프트 템플릿), Chat Model(채팅 모델), Retriever(검색기), Output Parser(출력 파서), Tool(도구)을 파이프처럼 이어줍니다. 여기서 중요한 건 각 단계가 서로 “어떤 입력을 받고 어떤 출력을 넘기는지”입니다. 저는 장애를 볼 때 컴포넌트 이름보다 계약 위반 여부를 먼저 봅니다.
입력 계약 위반: 프롬프트가 요구하는 변수와 실제 payload 키가 다릅니다.
타입 계약 위반: 다음 단계는 문자열을 기대하는데 앞 단계가 dict, list, Document 객체를 넘깁니다.
의존성 계약 위반: 예전 예제의 import 경로와 현재 설치된 패키지 구조가 맞지 않습니다.
인증·네트워크 계약 위반: API 키, 조직 설정, 프록시, 방화벽, timeout 설정이 실행 환경과 충돌합니다.
출력 계약 위반: 모델 응답이 JSON, Pydantic 스키마, enum 값 같은 엄격한 형식을 따르지 않습니다.
검색 품질 계약 위반: RAG에서 retriever가 질문과 관련 없는 문서를 가져오고, 모델은 그 문서 안에서만 그럴듯하게 답합니다.
이 관점이 유용한 이유는 단순합니다. “LLM이 틀렸다”는 말은 범위가 너무 넓거든요. 반면 “prompt 입력 계약이 깨졌다”, “parser 출력 계약이 깨졌다”라고 말하면 바로 재현 코드와 테스트 케이스를 만들 수 있습니다.
2. 재현 가능한 환경부터 고정하세요
LangChain 디버깅을 시작할 때 제일 먼저 할 일은 코드 수정이 아니라 환경 스냅샷입니다. 특히 LangChain은 코어 패키지, 통합 패키지, 커뮤니티 패키지가 분리되어 있어 import 경로와 설치 패키지를 같이 봐야 합니다. OpenAI 연동을 쓴다면 공식 문서 기준으로 langchain-openai를 별도로 설치하는 방식이 일반적입니다.
pip check는 의존성 충돌을 빠르게 확인할 때 좋습니다. 여기서 문제가 나오면 LangChain 코드를 보기 전에 패키지 상태부터 정리하는 편이 낫습니다. 운영 서버에서만 깨지는 오류라면 로컬과 서버에서 아래 명령 결과를 나란히 비교하세요.
python --version
python -m pip --version
python -m pip show langchain langchain-core langchain-openai langsmith
python - <<'PY'
import importlib.metadata as m
for name in ['langchain', 'langchain-core', 'langchain-openai', 'langsmith']:
try:
print(name, m.version(name))
except m.PackageNotFoundError:
print(name, 'NOT INSTALLED')
PY
튜토리얼을 따라 했는데 ModuleNotFoundError나 ImportError가 나면, 코드보다 먼저 “그 예제가 어느 시기의 패키지 구조를 기준으로 쓰였는지”를 보세요. 오래된 글에서 langchain.chat_models 계열 import를 복사했다면 현재 프로젝트의 공식 import 경로와 맞지 않을 수 있습니다. 이건 실력 문제가 아니라 패키지 분리의 흔적입니다.
3. INVALID_PROMPT_INPUT를 일부러 재현해보기
초보 단계에서 자주 만나는 LangChain 오류는 프롬프트 변수 누락입니다. LangChain의 INVALID_PROMPT_INPUT 오류는 프롬프트 템플릿이 요구하는 입력과 실제 전달한 값이 어긋날 때 발생합니다. 운영에서는 이게 더 교묘합니다. 프론트엔드에서는 question으로 보내는데 백엔드에서는 query로 넘기거나, 큐 메시지에 필드 하나가 빠지는 식이죠.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
('system', 'You are a helpful assistant.'),
('human', '{question}에 대해 {tone} 말투로 설명해줘'),
])
print('required variables:', prompt.input_variables)
# 일부러 tone을 빼서 오류를 재현합니다.
messages = prompt.invoke({'question': 'LangChain 오류'})
print(messages)
실제로는 모델 호출 전에 payload를 검증해서 “모델 API 비용을 쓰기도 전에 실패”하게 만드는 편이 좋습니다. 오류를 늦게 발견할수록 로그가 지저분해지고 비용도 새기 쉽거든요.
def validate_prompt_payload(prompt, payload: dict) -> None:
required = set(prompt.input_variables)
received = set(payload.keys())
missing = required - received
extra = received - required
if missing:
raise ValueError(f'Missing prompt variables: {sorted(missing)}')
if extra:
print(f'[debug] Extra payload keys ignored by prompt: {sorted(extra)}')
payload = {'question': 'LangChain 디버깅', 'tone': '친절한'}
validate_prompt_payload(prompt, payload)
messages = prompt.invoke(payload)
print(messages)
실무 기준으로는 extra를 무조건 오류로 보지 않습니다. API 요청 전체에는 사용자 ID, trace ID, locale 같은 부가 필드가 들어갈 수 있으니까요. 다만 missing은 즉시 실패시키는 편이 낫습니다. 누락된 값을 빈 문자열로 채우면 당장은 지나가도 모델 응답 품질이 조용히 망가집니다.
4. LCEL 체인은 예쁘게 쓰되, 디버깅은 끊어서 합니다
LCEL(LangChain Expression Language)은 prompt | model | parser처럼 읽기 좋은 체인을 만들 수 있어서 생산성이 좋습니다. 문제는 한 줄로 연결한 체인이 실패하면 어느 단계에서 깨졌는지 한눈에 보이지 않는다는 점입니다. 장애 상황에서는 체인을 잠시 해체하세요. 각 연결부에서 실제 값이 무엇인지 보는 게 훨씬 빠릅니다.
프롬프트, 모델, 파서 단계를 나누어 어디서 입력과 출력이 바뀌는지 확인하는 구성도입니다.
temperature=0은 디버깅할 때 변동성을 줄이려는 선택입니다. 창의적인 답변을 평가하는 단계가 아니라 오류 재현 단계라면, 같은 입력에 최대한 비슷한 응답이 나오는 편이 원인 분석에 유리합니다. timeout과 max_retries는 운영에서 더 중요합니다. timeout이 없으면 요청이 오래 붙잡혀 API 서버 worker를 묶을 수 있고, retry가 과하면 장애 상황에서 외부 API를 더 세게 두드릴 수 있습니다.
LLM 앱에서 인증 오류는 의외로 단순한 곳에서 납니다. .env에는 키가 있는데 프로세스에는 로드되지 않았거나, Docker 컨테이너에는 다른 값이 들어갔거나, 스테이징 서버가 운영용 LangSmith 프로젝트로 trace를 보내는 식입니다. 로컬 개발에서는 python-dotenv가 편하지만, 운영에서는 플랫폼의 secret 관리 기능을 우선하세요.
from dotenv import load_dotenv
import os
load_dotenv()
required_env = ['OPENAI_API_KEY']
for key in required_env:
if not os.getenv(key):
raise RuntimeError(f'Missing required environment variable: {key}')
print('LANGSMITH_TRACING=', os.getenv('LANGSMITH_TRACING', 'false'))
print('LANGSMITH_PROJECT=', os.getenv('LANGSMITH_PROJECT', 'not-set'))
운영 관점에서 LANGSMITH_PROJECT는 꼭 나누는 편이 좋습니다. 개발, 스테이징, 운영 trace가 섞이면 장애 분석 때 “어느 요청이 진짜 고객 요청인지”부터 다시 걸러야 합니다. 로그 비용과 보안 정책도 같이 봐야 합니다. 프롬프트나 검색 문서에 개인정보·계약 정보·내부 장애 내용이 들어갈 수 있다면 trace에 무엇을 남길지 팀 기준을 먼저 정해야 합니다.
6. LangSmith trace는 print의 대체가 아니라 요청 단위 블랙박스입니다
로컬에서 한 명이 테스트할 때는 print만으로도 충분합니다. 하지만 API 서버에서 동시에 여러 요청이 들어오면 stdout 로그는 금방 섞입니다. LangSmith 같은 관측성 도구를 붙이면 chain run, model call, parser 실행 흐름을 요청 단위 trace로 따라갈 수 있습니다. 이거 붙여두면 장애 회고 때 진짜 편하더라고요.
입력: 사용자가 보낸 원문과 서버가 체인에 넘긴 값이 같은지 확인합니다.
중간 출력: 프롬프트 완성본, 검색된 문서, 모델 raw response를 분리해서 봅니다.
실패 지점: 전체 latency 중 어디에서 오래 걸렸고 어느 단계에서 예외가 났는지 봅니다.
print 로그를 완전히 버리라는 뜻은 아닙니다. 개발 초반에는 print + 단계별 invoke가 가장 빠릅니다. 다만 팀 단위 운영으로 넘어가면 trace ID를 HTTP 응답 헤더나 애플리케이션 로그에 같이 남기는 쪽이 좋습니다. 고객 문의, 서버 로그, LangSmith trace를 같은 ID로 묶을 수 있으면 원인 추적 속도가 확 달라집니다.
상황
추천 도구
이유
쓰지 않아도 되는 경우
개인 실험, 노트북 코드
print, 단계별 invoke
설정이 적고 즉시 확인 가능
동시 요청이 없고 실패 입력이 단순할 때
FastAPI/Flask API 서버
구조화 로그 + trace ID
요청별 입력과 예외를 묶기 쉬움
일회성 데모라면 과할 수 있음
팀 운영 서비스
LangSmith tracing
체인 내부 단계와 모델 호출을 시각적으로 추적
민감 데이터 로깅 정책을 정하지 못했다면 먼저 보류
회귀 방지
pytest 재현 테스트
한 번 고친 오류가 다시 나는지 확인
모델 품질 평가처럼 정답이 유동적인 경우엔 별도 eval이 필요
7. JSON 파싱 오류는 프롬프트만으로 해결하기 어렵습니다
AI 프레임워크 오류 중 사람을 가장 오래 붙잡는 게 출력 파싱입니다. 모델은 자연어를 잘하지만, 애플리케이션은 종종 엄격한 JSON을 원합니다. 쉼표 하나, 따옴표 하나, enum 값 하나만 달라도 parser는 실패합니다. 제가 보는 선택지는 세 가지입니다.
문자열 후처리: 빠르지만 취약합니다. 데모나 내부 도구에만 제한적으로 씁니다.
Output Parser: 프롬프트에 형식 지시를 넣고 파서로 검증합니다. 실패 시 raw output 저장이 중요합니다.
구조화 출력: 모델 호출 단계에서 스키마를 강제합니다. 지원 모델과 provider를 확인해야 하지만 운영 안정성은 대체로 이쪽이 낫습니다.
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class Ticket(BaseModel):
title: str = Field(description='장애 제목')
severity: Literal['low', 'medium', 'high'] = Field(description='장애 심각도')
summary: str = Field(description='장애 요약')
model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=30)
structured_model = model.with_structured_output(Ticket)
result = structured_model.invoke('결제 API가 간헐적으로 실패하고 있습니다. 영향 범위가 넓어 보입니다.')
print(result)
print(type(result))
Literal을 쓴 이유는 단순합니다. severity가 critical, urgent, 높음처럼 제멋대로 늘어나면 downstream 시스템에서 다시 분기 오류가 납니다. 스키마를 좁게 잡으면 validation 실패는 늘 수 있지만, 잘못된 값이 조용히 DB에 저장되는 위험은 줄어듭니다. 운영에서는 둘 중 무엇이 더 비싼 장애인지 판단해야 합니다.
from pydantic import ValidationError
try:
ticket = structured_model.invoke('DB 연결이 실패해서 로그인 API가 실패합니다.')
except ValidationError as exc:
print('[schema validation failed]')
print(exc)
# 운영에서는 원문 입력, raw 응답, trace_id를 함께 저장하는 쪽이 좋습니다.
raise
반대로 간단한 블로그 요약, 내부 메모 생성처럼 사람이 읽고 끝나는 작업이라면 엄격한 Pydantic 스키마가 오히려 과할 수 있습니다. 이럴 땐 StrOutputParser로 충분합니다. 기준은 “사람이 읽을 텍스트인가, 시스템이 소비할 데이터인가”입니다. 시스템이 소비한다면 구조화 출력이나 검증 레이어를 빼지 않는 게 맞습니다.
8. RAG 오류는 모델보다 검색 결과부터 의심하세요
문서 검색 챗봇을 만들 때 가장 많이 속는 부분이 RAG 품질 문제입니다. 답변은 매끄러운데 사실관계가 묘하게 틀릴 때가 있거든요. 처음엔 모델을 바꿔야 하나 싶지만, trace를 보면 retriever가 질문과 관련 없는 문서를 가져오는 경우가 꽤 많습니다. 모델은 받은 근거 안에서 열심히 답했을 뿐입니다.
RAG에서 “틀린 답변”은 최소 세 종류로 나눠야 합니다. 첫째, 관련 문서를 못 찾은 검색 실패입니다. 둘째, 관련 문서는 찾았지만 청크가 잘려 핵심 문맥이 빠진 경우입니다. 셋째, 문서는 맞는데 프롬프트가 근거 밖 추론을 허용한 경우입니다. 이 셋은 대응이 다릅니다.
def debug_documents(docs, limit: int = 3) -> None:
for i, doc in enumerate(docs[:limit], start=1):
source = doc.metadata.get('source', 'unknown')
title = doc.metadata.get('title', 'no-title')
text = doc.page_content.replace('\n', ' ')[:500]
print(f'\n[doc {i}] source={source} title={title}')
print(text)
query = 'FastAPI에서 LangChain 체인이 timeout 날 때 어디를 봐야 하나요?'
docs = retriever.invoke(query)
debug_documents(docs)
위 출력에서 질문과 상관없는 문서가 계속 나온다면 모델 파라미터를 만지기 전에 retriever 설정을 봐야 합니다. top_k를 늘리면 관련 문서를 포함할 가능성은 올라가지만, 무관한 문서도 같이 들어와 토큰 사용량과 혼선을 늘릴 수 있습니다. chunk size를 키우면 문맥은 보존되지만 검색 단위가 둔해질 수 있고, 너무 작게 자르면 답변에 필요한 전후 맥락이 사라질 수 있습니다. 그래서 실패 질문을 모아 회귀 테스트처럼 돌리는 게 낫습니다.
증상
먼저 의심할 곳
확인 방법
추천 대응
답변이 유창하지만 근거가 틀림
Retriever
검색된 문서 제목·본문 일부를 출력
쿼리 재작성, metadata filter, top_k 조정
문서는 맞는데 답이 반쪽
Chunking
원문에서 앞뒤 문맥이 잘렸는지 비교
chunk size, overlap, 문서 분할 기준 재검토
근거 밖 추측이 섞임
Prompt
프롬프트가 모르면 모른다고 하게 만드는지 확인
근거 기반 답변 규칙과 citation 요구 추가
요청마다 품질 편차가 큼
모델 설정·검색 후보
같은 질문을 여러 번 실행해 raw context 비교
temperature 낮추기, 검색 결과 고정 테스트 작성
9. 재시도와 timeout은 비용 증폭 장치이기도 합니다
외부 모델 API를 쓰면 timeout, rate limit, 일시적인 네트워크 오류를 피할 수 없습니다. 그래서 max_retries를 켜는 건 합리적입니다. 다만 재시도는 공짜가 아닙니다. 실패 요청이 많을 때 retry가 겹치면 지연이 늘고, 일부 실패 모드에서는 비용도 커질 수 있습니다.
환경
timeout
retry
이유
로컬 디버깅
짧게
낮게
빨리 실패해야 원인 확인이 쉬움
사용자-facing API
제품 SLA에 맞춤
제한적으로
사용자 대기 시간과 성공률 사이 균형 필요
배치 작업
상대적으로 길게
조금 더 허용
실시간 응답보다 완료율이 중요할 수 있음
장애 재현 테스트
짧게
0 또는 낮게
재시도가 원래 오류를 가리지 않게 해야 함
from langchain_openai import ChatOpenAI
# 장애 재현용: 빨리 실패시키고 원인을 숨기지 않습니다.
debug_model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=10, max_retries=0)
# 일반 API용: 일시적 실패를 약간 흡수합니다.
api_model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=30, max_retries=2)
핵심은 모든 환경에 같은 모델 객체를 쓰지 않는 것입니다. 디버깅 모드에서는 오류를 선명하게 보고, 운영 모드에서는 사용자 경험을 보호해야 합니다. 같은 코드베이스라도 목적이 다르면 timeout과 retry 정책도 달라져야 합니다.
10. 실패 입력은 테스트로 남겨야 다시 안 밟습니다
LangChain 트러블슈팅을 하다 보면 그 자리에서 고치고 넘어가기 쉽습니다. 하지만 실패 입력을 테스트로 남기지 않으면 프롬프트를 조금 바꾼 날 같은 문제가 다시 돌아옵니다. 최소한 프롬프트 변수 검증, parser validation, retriever 결과 확인은 작은 테스트로 남겨두세요.
모델 호출까지 포함한 테스트는 더 신중해야 합니다. 외부 API 상태, rate limit, 비용, 모델 응답 변동성이 끼어들기 때문입니다. 계약 테스트는 로컬에서 빠르게 돌리고, 실제 모델 품질 평가는 별도 eval이나 스테이징 파이프라인으로 분리하는 편이 관리하기 쉽습니다.
요청별 trace, 체인 단계, 오류 발생 지점을 한눈에 확인하는 대시보드 예시입니다.
11. LangChain 트러블슈팅 순서
아래 순서는 장애를 볼 때 거의 습관처럼 쓰는 흐름입니다. 중요한 건 모델 호출을 맨 앞에 두지 않는 겁니다. 입력과 검색 결과가 깨진 상태에서 모델을 바꿔봐야 문제만 흐려집니다.
실패 입력을 고정합니다. 사용자 질문, request body, trace ID, 환경 이름을 한 묶음으로 저장합니다.
패키지와 import를 확인합니다.pip show, pip check, Python 버전을 비교합니다.
프롬프트 계약을 봅니다.prompt.input_variables와 payload 키를 비교합니다.
LCEL 체인을 끊습니다.prompt.invoke, model.invoke, parser.invoke를 따로 실행합니다.
RAG라면 검색 결과를 출력합니다. 문서 제목, source, 본문 앞부분을 직접 봅니다.
raw output을 저장합니다. parser가 실패했다면 모델 원문 응답 없이는 원인 분석이 어렵습니다.
timeout과 retry를 확인합니다. 재현 테스트에서는 retry를 낮춰 원래 오류를 가리지 않게 합니다.
고친 뒤 테스트를 남깁니다. 실패 payload를 최소 재현 케이스로 바꿔 회귀를 막습니다.
이 순서대로 보면 “LangChain이 이상하다”는 막연한 느낌이 “프롬프트 변수 누락”, “retriever 후보 품질 문제”, “JSON schema validation 실패”처럼 처리 가능한 단위로 바뀝니다. 디버깅은 결국 이름 붙이기 싸움입니다.
12. 자주 묻는 질문: LLM 앱 개발 문제 해결
Q. LangChain 오류가 나면 가장 먼저 뭘 봐야 하나요?
A. 에러 메시지 마지막 줄만 보지 말고 실패 단계를 먼저 나누세요. Prompt, Model, Parser, Retriever 중 어디서 깨졌는지 확인하면 해결 속도가 빨라집니다. 제일 빠른 방법은 LCEL 체인을 끊어서 각 단계의 입력·출력 타입을 출력하는 것입니다.
Q. 버전 문제는 어떻게 줄이나요?
A. 설치 직후 requirements.lock.txt를 남기고, 로컬·서버에서 python -m pip show langchain langchain-core langchain-openai langsmith 결과를 비교하세요. 오래된 블로그 예제의 import 경로를 그대로 쓰면 현재 패키지 구조와 맞지 않을 수 있습니다.
Q. JSON 파싱 오류는 프롬프트를 고치면 충분한가요?
A. 사람이 읽는 데모라면 프롬프트 보강으로도 버틸 수 있습니다. 하지만 시스템이 JSON을 소비한다면 Pydantic 스키마나 구조화 출력을 우선 검토하세요. 프롬프트만으로 형식을 강제하면 모델 응답 변동에 약합니다.
Q. LangSmith는 언제 붙이는 게 좋나요?
A. 개인 실험 단계에서는 필수는 아닙니다. API 서버로 여러 요청을 받거나 팀에서 장애를 같이 봐야 한다면 붙이는 편이 좋습니다. 단, trace에 민감 데이터가 남을 수 있으니 로깅 정책을 먼저 정해야 합니다.
Q. RAG 답변이 이상하면 모델을 바꾸는 게 먼저인가요?
A. 보통은 아닙니다. 검색된 문서가 질문과 관련 있는지 먼저 확인하세요. retriever가 엉뚱한 문서를 가져오면 좋은 모델도 그 문서 안에서 그럴듯한 오답을 만들 수 있습니다.
13. 상황별 추천: 이럴 땐 이렇게 가세요
LangChain 오류를 줄이는 핵심은 전체 체인을 한 번에 믿지 않는 것입니다. 단계별 계약을 확인하고, 실패 입력을 저장하고, 고친 뒤 테스트로 남기면 같은 문제를 반복해서 밟을 가능성이 확 줄어듭니다. 관련 구현 예제가 더 필요하다면 블로그의 RAG 구축 글, FastAPI 배포 글, LLM 관측성 글을 함께 연결해 내부 링크로 안내하면 좋습니다.
처음 개발 중이라면: print + 단계별 invoke로 충분합니다. 복잡한 도구보다 빠른 재현이 우선입니다.
프롬프트 오류가 잦다면: payload 검증 함수를 모델 호출 전에 두세요. 누락 키를 조기에 실패시키는 게 비용과 시간을 아낍니다.
API 서버로 운영한다면: timeout, retry, trace ID, LangSmith tracing을 같이 설계하세요. 관측성 없이 운영하면 장애 때 감으로 뒤지게 됩니다.
JSON 결과가 중요하다면: 문자열 파싱보다 구조화 출력 또는 Pydantic 검증을 우선하세요. 시스템이 소비하는 데이터는 느슨하게 두면 나중에 더 비싸게 터집니다.
RAG 품질이 문제라면: 모델 교체 전에 retriever 결과와 청크 전략을 확인하세요. 검색이 틀리면 생성도 흔들립니다.
장애 재현 중이라면: retry를 낮추고 temperature를 낮춰 오류를 선명하게 보세요. 운영 안정화 설정과 디버깅 설정은 달라도 됩니다.
기본값은 단순합니다. 개인 개발은 빠르게 재현하고, 팀 운영은 관측 가능하게 만들고, 시스템 연동은 스키마를 엄격하게 가져가세요. LangChain은 체인을 빠르게 만들게 해주지만, 안정적인 LLM 앱은 결국 각 단계의 입력과 출력을 얼마나 차분하게 검증하느냐에 달려 있습니다.
Proxmox에서 VM을 만들면 그 아래에서 실제로는 무엇이 도는지 아시나요? KVM, QEMU, libvirt — 가상화의 3대 축입니다. 이 구조를 이해하면 Proxmox 설정 하나하나가 왜 그런지 보이기 시작합니다. 가상화의 실체를 정리합니다.
1. 하이퍼바이저 타입 — Type 1 vs Type 2
Type 1 (베어메탈)
Type 2 (호스트형)
구조
하드웨어 위에 직접
일반 OS 위에 앱처럼
성능
높음
상대적으로 낮음
예시
KVM, ESXi, Proxmox
VirtualBox, VMware Workstation
Proxmox는 Type 1입니다. 정확히는 리눅스 커널 자체가 하이퍼바이저가 되는 KVM 방식이죠.
2. 3대 축 — KVM · QEMU · libvirt
KVM (Kernel-based VM): 리눅스 커널 모듈. CPU의 하드웨어 가상화 기능(Intel VT-x/AMD-V)을 써서 VM을 네이티브에 가깝게 돌립니다. “가상화의 엔진”.
QEMU: 에뮬레이터·디바이스 모델. 가상 디스크·네트워크카드·칩셋 등 “가상 하드웨어”를 제공합니다. KVM과 결합해 qemu-kvm으로 동작.
libvirt: 이들을 다루는 관리 API/데몬. Proxmox·virsh 같은 도구가 libvirt를 통해 VM을 제어합니다.
관리도구(Proxmox) → libvirt/QEMU → KVM(커널) → CPU 가상화(VT-x/AMD-V)
즉 KVM은 엔진, QEMU는 차체, libvirt는 운전대라고 보면 됩니다.
3. 전가상화 vs 반가상화(virtio)
가상 하드웨어를 “진짜 하드웨어처럼 완벽히 흉내”내면(전가상화) 호환성은 좋지만 느립니다. 그래서 나온 게 virtio(반가상화)입니다.
virtio: “이건 가상 환경이야”를 게스트가 알고, 전용 드라이버로 훨씬 빠르게 디스크·네트워크를 처리.
Proxmox에서 디스크를 VirtIO SCSI, 네트워크를 virtio로 두는 이유가 성능입니다.
# Proxmox에서 VM 디스크/네트워크를 virtio로 (성능 최적)
qm set 100 --scsihw virtio-scsi-single
qm set 100 --net0 virtio,bridge=vmbr0
단, 윈도우 게스트는 virtio 드라이버를 따로 설치해줘야 인식합니다(리눅스는 대부분 기본 내장).
4. CPU 타입과 중첩 가상화
Proxmox의 VM CPU 타입도 이 맥락입니다.
kvm64(기본): 호환성 위주, CPU 기능 일부 감춤
host: 물리 CPU 기능을 그대로 노출 → 성능↑, 중첩 가상화 가능
그래서 OpenStack 컴퓨트 노드처럼 “VM 안에서 또 VM”을 돌려야 하면 반드시 --cpu host를 줍니다. 이제 왜 그런지 이해되시죠?
5. 정리
Proxmox의 VM은 KVM(커널 엔진) + QEMU(가상 하드웨어) + libvirt/관리도구의 합작입니다. 성능의 핵심은 virtio, 중첩 가상화의 핵심은 CPU host. 이 구조를 알고 나면 Proxmox의 디스크·네트워크·CPU 옵션이 더 이상 주술이 아니라 논리로 보입니다.