작년 말, DSP Fallback의 성과를 개선하기 위해 CTR 예측 모델을 도입하는 이야기가 나왔다.
Fallback은 메인 DSP가 내보낼 광고가 없다고 판단한 경우에 동작한다. 광고 슬롯 대비 노출 비율(Fillrate)을 끌어올리는 것이 목적이다.
나는 백엔드 엔지니어였다. AI 배경은 없었다.
모델 자체보다 주변 시스템을 엮는 작업이 더 많을 거라는 판단이 있었고, 그래서 내가 맡게 됐다.
이 글은 그때 내린 기술 결정과 그 근거, 그리고 AI 비전문가가 ML 인프라를 만들며 배운 것들을 정리한 기록이다.
모델 선택: Logistic Regression
모델은 Logistic Regression으로 정했다.
광고 CTR 개선이다보니 클릭 여부를 학습시키면 됐다. 이항 분류 문제로 풀 수 있었다.
사내에서는 LR과 LightGBM을 권장했다. 광고 플랫폼에서 일반적으로 쓰이는 두 모델이다. 하지만 이 프로젝트는 초기 버전이었고, 복잡한 튜닝과 운영 부담을 처음부터 짊어지고 싶지 않았다.
그래서 더 단순한 LR을 택했다.
언어 및 프레임워크 선택
Python과 sklearn으로 정했다. 학습 배치와 인퍼런스 서버 모두.
처음에는 ONNX + Go를 생각했다. 사내 백엔드가 성능을 이유로 Node에서 Go로 마이그레이션을 검토 중이었고, 새 프로젝트라면 Go로 시작해볼 수 있을 것 같았다. 인퍼런스는 ONNX로 빼면 framework 독립성과 성능 이점을 함께 얻는다.
그런데 사내 ML 운영 환경이 Python 중심이었다. 참고할 사례, 공유할 코드, 배포 패턴이 전부 Python이었다. 조언과 리뷰가 필요한 상황에서는 같은 언어가 맞겠다고 생각했다. 성능 이점은 뒤로 미루고 운영 연속성을 택했다.
프레임워크도 비슷한 논리였다. ONNX가 sklearn보다 인퍼런스 성능이 낫다는 건 알고 있었지만, LR 같은 경량 모델에서는 그 이득이 크지 않을 거라고 봤다. sklearn만으로 학습과 저장이 충분하다고 생각했고, 가벼운 모델에 무거운 파이프라인을 얹는 것은 과잉 설계라고 판단했다.
ML Lifecycle 아키텍처: 3단으로 나눴다
ML Lifecycle을 세 개의 컴포넌트로 분리했다.
- 학습 배치: 주기적으로 LR 모델을 학습하고, 학습된 모델을 모델 저장소에 push한다.
- 모델 저장소: MLflow 기반. 학습 배치가 쓴 모델을 버전별로 보관한다.
- 인퍼런스 서버: 모델 저장소에서 최신 모델을 로드하고, 실시간 predict를 제공한다.
flowchart LR
A["학습 배치"] -->|"① 모델 push
② champion alias 이동"| B["모델 저장소
(MLflow)"]
A -->|"③ Argo Rollouts API 호출"| C["인퍼런스 서버"]
B -.->|"④ POD 기동 시 champion 로드"| C
흐름은 단순하다. 학습 배치 → 모델 저장소 → 인퍼런스 서버. 세 컴포넌트는 모델 파일을 통해서만 연결되고, 학습 주기와 인퍼런스는 서로 독립적으로 동작한다.
학습 배치 내부와 Promotion Gate
학습 배치 안쪽은 그냥 “학습 → 저장"이 아니었다. 학습이 끝난 모델이 자동으로 배포되지 않고, Promotion Gate라는 품질 검증 단계를 통과해야 champion alias가 교체된다.
flowchart LR
A["데이터 로딩"] --> B["전처리"] --> C["학습"] --> D["평가"] --> E{"Promotion Gate"}
E -->|"PASS"| F["champion alias 교체
+ Rollout 트리거"]
E -->|"FAIL"| G["기존 champion 유지"]
기준은 단순했다. 학습된 모델의 평가 지표가 미리 정한 임계값을 넘으면 PASS, 그렇지 않으면 FAIL. PASS면 champion alias를 새 버전으로 옮기고 배포를 트리거한다. FAIL이면 새 모델은 registry에 기록만 남기고 기존 champion이 계속 서비스된다.
이 덕분에 성능이 떨어진 모델이 프로덕션에 실수로 나가는 상황을 코드 변경 없이 막을 수 있었다.
배포: Argo Rollouts
새 모델을 인퍼런스 서버에 반영하는 과정에서는 Argo Rollouts를 썼다. k8s 위에 있었기 때문에 자연스러운 선택이었다.
sequenceDiagram
participant T as 학습 배치
participant R as MLflow
participant I as 인퍼런스 서버 POD
T->>R: ① 새 모델 등록
T->>R: ② champion alias 새 버전으로 이동
T->>I: ③ Argo Rollouts API 호출
Note over I: ④ Rollout이 POD 순차 교체
I->>R: ⑤ 새 POD가 champion 모델 로드
R-->>I: 모델 + 메타데이터
Note over I: ⑥ 새 모델로 서비스 재개
MLflow의 alias는 모델 버전에 “champion” 같은 별칭을 붙여 현재 production 모델을 가리키는 기능이다. 학습 배치는 Promotion Gate에서 PASS를 받으면 champion alias를 새 버전으로 옮기고, 이어서 Argo Rollouts API를 호출해 배포를 트리거한다. Rollout이 인퍼런스 서버 POD를 순차 교체하고, 새 POD는 기동 시 champion alias가 달린 모델을 로드해서 서비스에 들어간다.
회고
LR + sklearn + MLflow 조합은 단순했지만 가볍고 빠르게 돌아갔다.
가장 아쉬웠던 것은 Python + sklearn을 택한 결정이었다. 현재는 FastAPI 기반 Python 서버를 k8s에 올려 운영 중이다. 각 POD는 싱글 코어로 동작하고, 모델을 각자 로드한다. Feature가 늘어나면 인퍼런스 비용이 올라가고, 가용 POD 수도 함께 늘어났다. ONNX + Go 조합으로 한 프로세스 안에서 멀티 코어를 활용했다면 같은 부하를 더 적은 POD로 처리할 수 있었을지 모른다. 당시에는 운영 연속성을 택하는 게 맞다고 판단했지만, 그 결정의 비용이 운영 단계에서 드러났다.
시작할 때 가장 큰 걱정은 “AI 배경이 없는데 할 수 있을까"였다. 끝나고 나니 필요한 건 조금 달랐다는 걸 알게 됐다. 중요했던 건 ML 알고리즘이나 인프라 전문성이 아니라, 도메인을 얼마나 정확히 이해하고 있는가, 그리고 그에 맞춰 어떤 feature를 어떻게 조합할지 판단하는 능력이었다. 데이터를 읽고 패턴을 찾는 분석 능력도 그만큼 필요했다.