LINE 「기획서 없이 내재화하기: 검증 로직으로 동일함을 증명하다」 리뷰 — 블랙박스는 입력과 출력만 정의하면 통계로 같음을 증명할 수 있다
원문: 기획서 없이 내재화하기: 검증 로직으로 동일함을 증명하다 — LY Corporation Tech Blog, 장효택(LINE Plus Global E-Commerce), 2026-03-20
한 줄 요약
외부 시스템에 의존하던 모듈을 내재화하는데 기획서도 없고 기존 코드도 볼 수 없는 블랙박스였다. “우리가 만든 것이 기존과 정말 같은가”에 답하려면 코드가 아니라 같음을 증명하는 시스템이 필요했다. 뼈대는 하나다. 트리거(CDC·요청·파일 수신을 Kafka로 비동기 전파) → 실행·비교(같은 입력을 양쪽에 주입해 산출물 대조) → 가공·적재(불일치를 OpenSearch에 쌓고 ksqlDB로 실시간 집계해 Slack 알림) → 분석·개선(대시보드 보고 고치고 다시). 이 루프를 조회 API(응답 필드 100개 이상), 통계 업데이트(비동기 시차와 트리거 누락), 수신 파이프라인 E2E(SKU + merchantUpdateDate로 스냅숏 키) 세 케이스에 적용해 불일치를 0으로 수렴시켰다.
배경: 상품·카탈로그·수신
‘상품’은 판매자가 등록한 개별 아이템(여러 판매자의 ‘iPhone 17’은 각각 별개), ‘카탈로그’는 같은 모델을 묶어 가공된 가치를 주는 상위 객체, ‘수신’은 판매자 파일을 시스템으로 가져오는 첫 관문이다. 수천만 카탈로그와 수억 상품이 얽혀 있어 데이터 하나의 영향이 크고, 하루 수백 번 도는 수신 스케줄까지 사람이 수동으로 검증하는 것은 불가능했다.
뼈대: 입력과 출력을 정의한 검증 루프
검증의 목적은 틀린 곳을 찾는 것이 아니라 개발자가 무엇이 왜 틀렸는지 즉시 파악해 고치고, 전후 차이가 0에 수렴하게 하는 환경이다. 핵심은 “무엇을 넣고 무엇을 비교할 것인가”다. 입력은 양쪽이 같은 상태에서 시작하도록 보장하는 값(같은 ID, 같은 시점 스냅숏, 같은 파일), 출력은 시스템 성격에 따른 최종 산출물(API 응답, DB 갱신 결과, 등록된 상품). 이 둘을 연결하면 내부가 아무리 복잡한 블랙박스라도 통계로 같음을 증명할 수 있고, 불일치가 0에 수렴하는 순간 내재화 완료를 선언한다. Kafka는 대량 트래픽을 중계하면서 검증 로직이 실서비스 성능에 영향을 주지 않게 물리적으로 격리하는 보호막이다.
케이스 1: 조회 API — 순서와 기본값
타사 조회 기능을 옮기는데 사양서·기획서·코드가 전혀 없었고 응답 필드가 100개 넘고 필터·정렬이 얽혀 있었다. 카탈로그 DB의 CDC를 트리거로 삼아 변경이 생길 때마다 기존 API와 신규 API를 호출해 응답을 끊임없이 비교했다. 가장 자주 드러난 것은 모르던 정렬 조건과 DB 값과 다른 반환 기본값이었다. 정렬을 몰라 순서만 미세하게 어긋나는 경우는 안의 실제 값을 개별 추출해 대조했다.
비교 로직은 응답을 클래스로 매핑하지 않고 Map<String, Object>로 바꿔 재귀 탐색해 필드명 기준으로 값을 대조했다. 그래야 구조가 어떻든 하나의 로직으로 된다. 리스트 순서만 다른데 불일치로 잡히는 것은 따로 처리했다. 불일치는 표준 메시지로 Kafka에 넣고 OpenSearch Connector로 인덱싱하며, ksqlDB로 실시간 집계해 이상 징후를 Slack으로 보낸다. 초기에는 수백만 건이 쏟아졌는데 같은 필드의 중복 오류는 잡음이라 필드당 분당 추출 건수를 제한했다. 부수 효과로 기존·신규 API를 같은 시점에 병렬 호출하니 실트래픽에서 성능 비교가 자연히 됐다.
케이스 2: 통계 업데이트 — 시차와 누락
상품이 변경될 때마다 카탈로그 단위 통계를 갱신하는 로직은 상태 변화를 다룬다. 조회 검증 틀에 ‘통계 시뮬레이션’을 넣어, 카탈로그 필드 변경 CDC가 오면 신규 로직으로 예상값을 미리 계산하고 기존 로직이 DB에 쓴 실제값과 대조했다.
걸림돌은 비동기 지연이었다. 기존 통계 로직과 검증 로직이 모두 Kafka로 비동기라 검증 시점에 기존 로직의 DB 갱신이 아직 안 끝나 일시적 불일치가 빈번했다. 먼저 통계와 무관한 필드 변경은 필터링해 비교 자체를 줄였고, 남는 시차는 N회차 재시도 큐로 풀었다. 불일치면 즉시 오류가 아니라 큐 뒤로 보내 다시 본다. 기존 스트림 끝에 검증을 붙여 지연을 없애는 방법도 고민했지만 기존 스트림에 부하를 줄 수 있어 별도 프로세스로 유지했다.
모든 데이터가 맞아 가던 중 사각지대가 나왔다. 갱신이 일어났어야 하는데 아무 일도 안 일어난 케이스는 비교 로직으로 잡을 수 없다. 감지 로직을 리팩터링하며 트리거가 누락된 것이다. 그래서 실시간과 별개로 Iceberg에 최신화하던 카탈로그 정보에 하루 한 번 쿼리해 모든 카탈로그 통계를 전수 조사하는 배치 검증을 두었고, 실시간이 못 잡은 누락 트리거를 여럿 찾았다. 실시간 검증이 정확도를, 배치 검증이 트리거의 완결성을 확보한 뒤에야 같은 생명 주기로 동작한다고 확신했다.
케이스 3: 수신 파이프라인 — 섀도우와 스냅숏 키
판매자 파일 다운로드부터 유효성 검사와 최종 등록까지 긴 여정이고, 판매자마다 수십 개 필드 중 일부만 보내므로 어떤 조합이 와도 같게 처리하는 것이 목표다. 구조가 같은 신규 파이프라인을 병렬로 띄우고(섀도우), 같은 시점에 같은 판매자 파일을 양쪽이 수집하게 했다.
문제는 상품이 파일로 하루 여러 번, API로도 수시로 바뀌어 조금만 시차가 나도 다른 버전을 비교하게 된다는 것. SKU + merchantUpdateDate를 스냅숏 키로 묶어 각 산출물이 어느 시점 파일에서 나왔는지 특정했다. 초반의 방대한 불일치에는 실제 오류가 아닌 의도된 사양 차이가 섞여 있어 알림에 무뎌질 위험이 있었고, 확인이 끝난 차이와 불필요한 필드는 비교 대상에서 빼는 선택적 필터링을 넣었다. 필드 단위 외에 스케줄별로 유효 건수·오류 건수·단계별 상태 같은 큰 단위 정합성도 비교했다. 테이블 구조가 완전히 달라져 DB 레벨 비교가 불가능했기 때문이다.
도구: 대시보드와 관리자 페이지
OpenSearch의 diff는 로그가 아니라 통계다. 일치 건수가 100%로 수렴하는 추이로 완료 시점을 객관적으로 가늠하고, 오류를 필드·유형별로 그룹화해 100만 건 오류의 원인이 ‘특정 필드 타입 변환’ 하나인 것을 드러내며, 타임라인으로 특정 배포나 외부 변화의 영향을 추적한다. 관리자 페이지는 필드 100개 중 원하는 것만 검색하거나 값이 다른 것만 보고, 불일치는 붉은색·순서만 다른 것은 노란색으로 보여 주며, 카탈로그 ID로 신규 통계를 시뮬레이션하고, 수신 단계별 상태와 상품 수를 좌우로 비교한다. 예전엔 프런트엔드를 따로 배워야 했지만 이제 AI 덕에 간단한 관리자 페이지는 쉽게 만들 수 있으니, 수천만 건을 검증하는 장기 관점에서 초기에 전용 도구를 만드는 것이 압도적으로 효율적이라고 한다.
읽고 남는 질문
- “0에 수렴”의 마지막 단계에서 남은 불일치를 어떻게 처리했는지가 없다. 기존 시스템의 버그였던 것(재현하지 않기로 한 것)과 의도적 개선의 목록이 있으면 “동일함”의 정의가 완결된다.
- 조회 검증은 CDC 트리거라 변경이 없는 카탈로그는 검증되지 않는다. 오래 변경되지 않은 데이터의 조회 동일성은 별도 샘플링으로 확인했는지 궁금하다.
- 섀도우 파이프라인을 병렬로 돌리는 비용(수신 처리량이 두 배)과 기간이 얼마였는지, 그리고 최종 전환(컷오버) 방식이 무엇이었는지가 있으면 재현에 도움이 된다.
한 줄로 가져가기
사양 없는 내재화의 답은 사양을 복원하는 것이 아니라 같은 입력을 양쪽에 넣고 출력을 통계로 대조하는 루프를 만드는 것이다. 그 루프에는 비동기 시차를 견디는 재시도, 잡음을 줄이는 rate limit과 필드 필터, 그리고 “아무 일도 안 일어난 것”을 잡는 전수 배치가 있어야 한다.
댓글
아직 댓글이 없습니다