x86에 배포하고 나서야 보인 결함 둘: 추론 런타임과 Docker WORKDIR
- OCR·인페인팅을 돌리는 이미지 처리 서비스를 배포하기 직전, 하루에 배포를 완전히 막는 결함 두 개를 찾았다. 둘 다 유닛 테스트·타입체크·빌드를 전부 통과한 상태였다.
- ① 추론 백엔드가 x86에서 실행 자체가 불가능했다. 개발기는 arm64 맥이라 해당 코드 경로에 닿은 적이 없었다. 기본 설정 그대로 배포하면 모든 업로드가 실패한다.
- ② 프로덕션 웹 컨테이너가 매 부팅마다 즉시 종료됐다. 이미지 빌드는 성공했고, 아무도 그 컨테이너를 켜 본 적이 없었을 뿐이다.
- 공통 원인은 하나다 — "코드가 import되고 테스트가 통과한다"와 "배포 환경에서 핵심 기능이 동작한다"를 같은 것으로 취급했다. 두 경로 모두 지금은 CI가 실제로 실행해서 확인한다.
1.증상: 러너에서 3회, 이미지에서 1회 — 전부 같은 지점
OCR 백엔드를 비교하려고 x86 CI 러너에서 벤치마크를 돌렸다. 목적은 "어느 백엔드가 빠른가"였는데, 나온 답은 다른 것이었다.
NotImplementedError: (Unimplemented) ConvertPirAttribute2RuntimeAttribute not support [pir::ArrayAttribute<pir::DoubleAttribute>] at paddle/fluid/framework/new_executor/instruction/onednn/onednn_instruction.cc:116
파일명이 답을 준다. onednn은 x86 전용 최적화 경로다.
개발기인 arm64 맥에서는 이 코드에 아예 닿지 않는다. 그래서 로컬에서는 몇 달 동안 멀쩡했다.
중요한 건 이게 성능 문제가 아니라는 점이다. 느린 게 아니라 안 도는 것이다. 이 상태로 서버에 올렸다면 사용자가 이미지를 올릴 때마다 처리 실패가 떨어진다.
CI는 린트·타입체크·유닛 테스트를 성실히 돌리고 있었다. 다만 그 경로 중 어느 것도 실제 추론을 실행하지 않았다. 모델을 로드하고 이미지를 넣어 결과를 받아보는 일은, 개발자가 로컬에서 손으로 할 때만 일어났다. 그리고 그 로컬은 배포 대상과 CPU 아키텍처가 달랐다.
2.두 번째 결함: 컨테이너가 부팅 직후 종료된다
첫 번째를 고치고 나서, 이번엔 프로덕션 스택 전체를 x86에서 올려 실제 업로드 한 건을 통과시켜 보기로 했다. 컨테이너가 뜨지 않았다.
▲ Next.js 16.2.9 - Local: http://localhost:3000 ✓ Ready in 79ms Error: Could not find a production build in the '.next' directory.
"Ready in 79ms" 다음 줄에서 죽는다. 원인은 Dockerfile 한 줄이었다.
WORKDIR /app ← 워크스페이스 루트 … CMD ["node", "…/next", "start"] ← .next를 cwd에서 찾는다 // 빌드 산출물의 실제 위치: /app/apps/web/.next // next start가 보는 곳: /app/.next → 없음
모노레포라 앱이 워크스페이스 패키지 안에 있는데, next start는
현재 작업 디렉터리 기준으로 빌드 산출물을 찾는다. 고치는 건 한 줄이다 —
CMD 앞에 WORKDIR /app/apps/web을 두면 된다.
문제는 고치는 난이도가 아니라, 이게 왜 지금까지 안 걸렸는가이다.
3.왜 아무도 몰랐나 — 검증 커버리지의 빈칸
| 기존 검증 | 실제로 확인한 것 | 빈칸 |
|---|---|---|
| Compose 설정 검사 | YAML이 유효한 스택으로 병합되는가 | 컨테이너를 켜지 않음 |
| 브라우저 E2E | UI 흐름이 동작하는가 | Docker 밖, 백엔드는 stub |
| 이미지 빌드 확인 | 빌드 산출물이 생성되는가 | 실행하지 않음 |
| 유닛 테스트 | 함수 단위 로직 | 모델 추론에 닿지 않음 |
각 칸은 저마다 성실하다. 그런데 셋을 합쳐도 "프로덕션 이미지를 배포 대상 아키텍처에서 켜고, 실제 요청 하나를 끝까지 통과시킨다"는 문장을 커버하지 못한다.
세 번째 줄이 특히 뼈아팠다. 이미지를 빌드해서 산출물을 grep으로 확인하고
"검증했다"고 적어 둔 게 나였다. 빌드가 됐다는 것과 컨테이너가 뜬다는 건 다른 얘기인데
그걸 같은 것으로 셌다.
4.대응: 배포 형상 그대로 한 번 실행하는 잡
추가한 검증은 화려하지 않다. 프로덕션 이미지를 배포 대상 아키텍처에서 빌드해서, 실제 요청을 한 건 돌린다. 그게 전부다.
두 번째 잡이 앞서 말한 빈칸을 정확히 메운다. 기존 E2E는 백엔드를 stub했고, 이미지 검사는 서비스 클래스를 직접 호출했다. 프록시부터 결과까지 이어지는 경로는 한 번도 실행된 적이 없었다.
이 잡은 기본 설정이 실패할 때 잡을 실패시킨다. 다른 백엔드는 비교용으로 같이 돌리되 실패해도 무시한다. 기준은 "모든 옵션이 동작하는가"가 아니라 "아무 설정도 안 한 배포가 동작하는가"이기 때문이다. 실제로 문제였던 것도 정확히 그 기본값이었다.
5.기본값이 "검증된 것"이 아니라 "익숙한 것"이었다
추론 백엔드 기본값이 왜 그 값이었는지 설정 파일에 이렇게 적혀 있었다.
# paddle Default. Current known-good runtime.
# onnx … 속도·정확도 동등성 미검증이라 opt-in
"known-good"의 실제 의미는 "내 맥에서 잘 돌았다"였다. 그리고 opt-in으로 밀어둔 쪽이 배포 아키텍처에서 유일하게 동작하는 백엔드였다. 보수적으로 잡아둔 기본값이 실제로는 더 위험했다는 뜻이다.
기본값을 바꾸면서 하나는 일부러 넣지 않았다 — 실패 시 자동 대체다. 기본 백엔드가 안 뜨면 다른 걸로 넘어가게 만들면 편하지만, 그건 이번 결함을 정확히 그 방식으로 숨겼을 동작이다. 몇 달 동안 "잘 돌고 있다"고 믿으면서 실제로는 폴백 위에서 굴러갔을 것이다.
6.트레이드오프
| 비용 | 완화 |
|---|---|
| 잡 1회에 이미지 빌드 + 모델 다운로드(수백 MB) + 실제 추론 — 수십 분 | PR 경로에 올리지 않고 수동 실행. 배포 전 게이트로만 쓴다 |
| CI 러너는 공유 vCPU라 절대 시간이 실서버와 다르다 | 시간이 아니라 동작 여부를 판정하는 잡으로 성격을 좁혔다 |
| 모델 가중치를 매번 받으면 느리다 | 네임드 볼륨으로 캐시. OCR과 인페인팅 가중치는 경로가 달라 둘 다 캐시해야 한다 |
솔직히 말하면 이 잡은 평소에 아무것도 알려주지 않는다. 초록불만 계속 뜬다. 값은 배포 직전 한 번, 또는 런타임·베이스 이미지·패키징을 건드렸을 때 나온다. 이번처럼.
7.정리
두 결함은 종류가 달라 보이지만 원인이 같다. 하나는 CPU 아키텍처 차이, 하나는 모노레포 디렉터리 구조인데, 둘 다 "성공한 빌드"를 "동작하는 배포"의 증거로 셌기 때문에 남았다.
실용적인 규칙 하나로 줄이면 이렇다 — 개발기와 배포 대상이 다른 지점(CPU 아키텍처, 베이스 이미지, 실행 사용자, 작업 디렉터리)이 있다면, 그 차이를 넘어서 핵심 기능을 한 번 실행해 보는 자동 검증이 하나는 있어야 한다. 유닛 테스트를 아무리 늘려도 이 문장을 대신해 주지 않는다.
단, 모든 팀에 이 잡이 필요하다는 뜻은 아니다. 개발기와 서버가 같은 아키텍처이고 런타임 의존성이 순수 인터프리터 코드뿐이라면 얻는 게 적다. 네이티브 확장·GPU/CPU 최적화 경로·모델 런타임처럼 플랫폼별로 다른 코드가 실행되는 의존성이 있을 때 값이 나온다.
'프로그래밍' 카테고리의 다른 글
| GitHub Actions 러너로 VPS 사양 재는 법: 36% 편차와 측정 함정 (0) | 2026.08.09 |
|---|---|
| 병목은 OCR이 아니었다: 요청의 53%를 쓰던 폰트 추론 2.2배 줄이기 (0) | 2026.08.09 |
댓글