본문 바로가기
프로그래밍

x86에 배포하고 나서야 보인 결함 둘: 추론 런타임과 Docker WORKDIR

by me_in_sk 2026. 8. 9.
반응형
이미지 편집 엔진

x86에 배포하고 나서야 보인 결함 둘: 추론 런타임과 Docker WORKDIR

빌드 성공을 배포 가능으로 세던 검증 커버리지, 그리고 CI 보강
핵심 요약
  • OCR·인페인팅을 돌리는 이미지 처리 서비스를 배포하기 직전, 하루에 배포를 완전히 막는 결함 두 개를 찾았다. 둘 다 유닛 테스트·타입체크·빌드를 전부 통과한 상태였다.
  • ① 추론 백엔드가 x86에서 실행 자체가 불가능했다. 개발기는 arm64 맥이라 해당 코드 경로에 닿은 적이 없었다. 기본 설정 그대로 배포하면 모든 업로드가 실패한다.
  • ② 프로덕션 웹 컨테이너가 매 부팅마다 즉시 종료됐다. 이미지 빌드는 성공했고, 아무도 그 컨테이너를 켜 본 적이 없었을 뿐이다.
  • 공통 원인은 하나다 — "코드가 import되고 테스트가 통과한다"와 "배포 환경에서 핵심 기능이 동작한다"를 같은 것으로 취급했다. 두 경로 모두 지금은 CI가 실제로 실행해서 확인한다.

1.증상: 러너에서 3회, 이미지에서 1회 — 전부 같은 지점

OCR 백엔드를 비교하려고 x86 CI 러너에서 벤치마크를 돌렸다. 목적은 "어느 백엔드가 빠른가"였는데, 나온 답은 다른 것이었다.

CI 러너 (x86_64) 로그stderr
NotImplementedError: (Unimplemented) ConvertPirAttribute2RuntimeAttribute
  not support [pir::ArrayAttribute<pir::DoubleAttribute>]
  at paddle/fluid/framework/new_executor/instruction/onednn/onednn_instruction.cc:116

파일명이 답을 준다. onednnx86 전용 최적화 경로다. 개발기인 arm64 맥에서는 이 코드에 아예 닿지 않는다. 그래서 로컬에서는 몇 달 동안 멀쩡했다.

중요한 건 이게 성능 문제가 아니라는 점이다. 느린 게 아니라 안 도는 것이다. 이 상태로 서버에 올렸다면 사용자가 이미지를 올릴 때마다 처리 실패가 떨어진다.

"CI가 통과했다"가 가려준 것

CI는 린트·타입체크·유닛 테스트를 성실히 돌리고 있었다. 다만 그 경로 중 어느 것도 실제 추론을 실행하지 않았다. 모델을 로드하고 이미지를 넣어 결과를 받아보는 일은, 개발자가 로컬에서 손으로 할 때만 일어났다. 그리고 그 로컬은 배포 대상과 CPU 아키텍처가 달랐다.

2.두 번째 결함: 컨테이너가 부팅 직후 종료된다

첫 번째를 고치고 나서, 이번엔 프로덕션 스택 전체를 x86에서 올려 실제 업로드 한 건을 통과시켜 보기로 했다. 컨테이너가 뜨지 않았다.

web 컨테이너 로그stderr
▲ 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 한 줄이었다.

web.Dockerfiledocker
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이 유효한 스택으로 병합되는가컨테이너를 켜지 않음
브라우저 E2EUI 흐름이 동작하는가Docker 밖, 백엔드는 stub
이미지 빌드 확인빌드 산출물이 생성되는가실행하지 않음
유닛 테스트함수 단위 로직모델 추론에 닿지 않음
무엇을 검증하고 있었나 코드빌드컨테이너 기동실제 요청 처리 유닛 테스트Compose 설정 검사브라우저 E2E이미지 빌드 확인 이 두 칸을 덮는 검증이 없었다 결함 두 개가 정확히 여기 있었다

각 칸은 저마다 성실하다. 그런데 셋을 합쳐도 "프로덕션 이미지를 배포 대상 아키텍처에서 켜고, 실제 요청 하나를 끝까지 통과시킨다"는 문장을 커버하지 못한다.

세 번째 줄이 특히 뼈아팠다. 이미지를 빌드해서 산출물을 grep으로 확인하고 "검증했다"고 적어 둔 게 나였다. 빌드가 됐다는 것과 컨테이너가 뜬다는 건 다른 얘기인데 그걸 같은 것으로 셌다.

4.대응: 배포 형상 그대로 한 번 실행하는 잡

추가한 검증은 화려하지 않다. 프로덕션 이미지를 배포 대상 아키텍처에서 빌드해서, 실제 요청을 한 건 돌린다. 그게 전부다.

CI (x86_64 러너) ──▶ 프로덕션 이미지 빌드 (개발용 아님) ──▶ 컨테이너 안에서 실제 추론 1건 ← 기본 백엔드가 실패하면 잡 실패 ──▶ Compose 스택 기동 ──▶ proxy → BFF → 처리 서비스 → 비동기 잡 → 결과 (업로드 1건 전 구간)

두 번째 잡이 앞서 말한 빈칸을 정확히 메운다. 기존 E2E는 백엔드를 stub했고, 이미지 검사는 서비스 클래스를 직접 호출했다. 프록시부터 결과까지 이어지는 경로는 한 번도 실행된 적이 없었다.

실패 조건을 어디에 두는가

이 잡은 기본 설정이 실패할 때 잡을 실패시킨다. 다른 백엔드는 비교용으로 같이 돌리되 실패해도 무시한다. 기준은 "모든 옵션이 동작하는가"가 아니라 "아무 설정도 안 한 배포가 동작하는가"이기 때문이다. 실제로 문제였던 것도 정확히 그 기본값이었다.

5.기본값이 "검증된 것"이 아니라 "익숙한 것"이었다

추론 백엔드 기본값이 왜 그 값이었는지 설정 파일에 이렇게 적혀 있었다.

환경변수 설명 (수정 전)env
# 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 최적화 경로·모델 런타임처럼 플랫폼별로 다른 코드가 실행되는 의존성이 있을 때 값이 나온다.

이미지 편집 엔진 · 배포 검증

반응형

댓글