이 서비스는 소규모 연구팀이 만든 AI 캐릭터 채팅의 개념 검증용 시제품(PoC)에서 시작했다. 참여 당시 핵심 대화 기능은 동작하고 있었고, 연구팀은 모델과 프롬프트, 대화 흐름을 바꾸며 서비스의 가능성을 확인하고 있었다.
기존에 개발하던 다른 AI 제품이 유지보수 단계로 들어가면서 이 PoC의 제품화를 맡게 됐다. 클라이언트와 앱 개발을 맡은 엔지니어와 함께 출시를 준비하며, 백엔드와 인프라를 담당했다. 개발 범위에는 사용자 인증과 OAuth 연동, 결제와 재화 처리, 콘텐츠 반영 자동화가 포함됐다. 기존 코드의 구조와 데이터베이스 변경 이력을 정리하고, 타입 검사와 테스트를 개발 과정에 연결하는 일도 함께 진행했다.
제품화를 진행하는 동안 연구팀의 기능 실험도 계속됐다. 새로운 대화 동작과 콘텐츠가 추가됐고, 제품화 과정에서 수익 모델(BM)도 바뀌었다. 처음 계획한 기능을 구현하는 동안에도 요구사항은 달라졌으므로, 새 동작을 추가하면서 기존 데이터와 이용 흐름을 어떻게 이어갈지 함께 다뤄야 했다.
기능 개발과 구조 정비
새 기능을 추가하는 동안 기존 코드도 바뀌었다
PoC에서는 외부 모델 API 호출, 응답 해석과 서비스 로직이 한데 연결돼 있었다. 프롬프트나 처리 흐름을 바꿔 결과를 확인할 수 있었지만, 기능이 늘어나면서 API 연동을 수정할 때도 그 결과를 사용하는 서비스 로직을 함께 살펴봐야 했다.
이미 동작하는 코드를 바꾸려면, 어떤 배경에서 만들어졌고 무엇을 해결하기 위해 그런 구조를 선택했는지부터 파악해야 했다. 체스터턴의 울타리도 울타리를 걷어내기에 앞서 그것을 세운 이유를 이해해야 한다는 이야기다. 새 구조에서도 기존 구조가 해결하던 문제를 해결할 수 있어야 했고, 유지해야 할 동작이 빠져서도 안 됐다.
And until we know what the reason was, we really cannot judge whether the reason was reasonable.
그 이유가 무엇이었는지 알기 전까지는, 그 이유가 타당했는지 판단할 수 없다.
새 구조에서는 변경이 잦은 부분과 상대적으로 안정적인 부분을 구분하고, 각 부분이 담당할 역할을 나누고자 했다. 동시에 하나의 기능을 수정할 때 함께 바뀌어야 하는 코드가 여러 곳에 흩어지지 않도록 해야 했다. 작은 변경에도 여러 곳을 고쳐야 하는 상황을 샷건 서저리(shotgun surgery)라고 한다. 수정할 코드가 흩어져 있으면 어디를 고쳐야 하는지 찾기 어렵고 일부를 빠뜨리기도 쉽다.
every time you make a change, you have to make a lot of little edits to a lot of different classes.
변경할 때마다 여러 클래스에 걸쳐 수많은 작은 수정을 해야 한다.
연구팀은 새로운 동작을 계속 실험하고 있었으므로, 코드를 정리하는 동안 기능 개발을 멈추기는 어려웠다. 필요한 부분부터 외부 API 호출과 서비스 로직을 분리하고, 각 부분이 주고받는 데이터와 호출 방법을 정리했다. 새 기능도 정리한 구조에 연결하도록 하면서 기존 코드와 새 코드가 함께 동작하게 했다.
이 과정에서는 새 기능을 어디에 추가해야 하는지, 기존 동작을 수정할 때 어느 범위까지 살펴봐야 하는지가 명확해지는지도 고려했다. 기존 요구사항을 충족하면서도 다음 변경을 수용하기 쉽고, 코드를 다루는 사람이 실수할 여지가 적은 구조로 나아가고자 했다.
구조를 바꾸는 과정에서는 기존 동작에 영향을 줬는지 확인할 수 있어야 했다. Python 타입 힌트를 보강하고 mypy의 검사 조건을 강화했으며, 린트와 포맷 도구를 통일했다. 테스트를 CI에 연결해 변경을 검토할 때 실행 결과도 함께 확인할 수 있도록 했다.
연구팀이 기능을 추가할 때 참고할 구현 예시와 테스트, API 호출 방법도 함께 정리했다. 새 구조에서 어떤 방식으로 기능을 구현하고 검증해야 하는지 알 수 있어야, 다른 작업자도 그 구조에 맞춰 개발을 이어갈 수 있기 때문이다.
기능 변경을 반영할 데이터베이스
갈라진 마이그레이션 이력부터 정리하기
인증과 결제, 콘텐츠 관련 기능을 추가하고 수정하려면 데이터베이스도 함께 바뀌어야 했다. 데이터베이스 변경 도구인 Alembic을 사용하고 있었지만, 마이그레이션 이력이 여러 갈래로 나뉜 뒤 제대로 합쳐지지 않은 상태였다. 새 변경을 적용하기 전에 마이그레이션 파일의 연결 관계와 실제 스키마부터 대조해야 했다.
서비스가 현재 데이터베이스에서 동작하더라도, 빈 데이터베이스에 마이그레이션을 차례로 적용해 같은 스키마를 만들 수 있는지는 별도로 확인해야 했다. 실제 스키마와 각 변경 파일을 대조해 갈라진 이력을 정리했다. 되돌리는 과정에서 외래 키가 잘못된 테이블을 참조하는 부분을 수정하고, 외래 키를 제거한 뒤 인덱스와 테이블을 삭제하도록 작업 순서도 맞췄다.
검증에는 삭제하고 다시 만들어도 되는 테스트용 데이터베이스를 사용했다. 먼저 아무 마이그레이션도 적용하지 않은 base에서 최신 변경인 head까지 upgrade했다. 이후 base까지 downgrade한 뒤 다시 head까지 올렸다. 이를 통해 현재 스키마를 재현할 수 있는지와 각 파일에 정의한 되돌리기 작업이 순서대로 실행되는지를 확인했다.
이력을 정리한 뒤에는 새 마이그레이션을 작성하고 검토하는 방법도 팀 가이드로 남겼다. 같은 문제가 반복되지 않으려면 새 변경을 추가하는 사람도 기존 이력과 자동 생성 결과를 확인할 수 있어야 했다.
콘텐츠 반영
기획 문서에서 서비스 데이터까지 연결하기
대화 기능을 개발하는 동안 시나리오와 진행 규칙도 계속 추가·수정됐다. 기획자와 콘텐츠 작성자는 문서와 데이터 시트를 사용했지만, 서비스에서 사용하려면 그 내용을 데이터베이스에 반영해야 했다. 사람이 내용을 옮기고 SQL을 수정하는 과정에서는 원본 문서와 실제 반영된 데이터가 달라질 수 있었다.
게임 개발에서도 비슷한 작업을 경험했다. 기획자가 작성하는 콘텐츠에는 정해진 형식과 항목 간 관계가 있고, 서비스에 반영하기 전에 확인해야 하는 조건이 있다. 따라서 기획 문서를 원본으로 관리하고, 그 내용을 검증해 서비스에 반영할 데이터로 변환하는 절차가 필요했다.
기획 데이터를 담은 시트를 읽어 필수값이 빠졌는지, 각 값의 자료형이 맞는지 검사하도록 했다. 관련 데이터가 서로 대응하는지도 확인했다. 예를 들어 콘텐츠와 그에 연결된 번역 데이터 중 한쪽만 존재한다면, 그대로 SQL을 생성하지 않고 오류를 내도록 했다.
입력값을 읽다가 오류가 발생하면 처리 중이던 행 번호와 원인을 함께 표시했다. 그래야 생성된 SQL에서 문제를 찾는 대신 원본 시트의 해당 항목을 수정할 수 있었다. 검증한 데이터로 입력·갱신용 SQL과 콘텐츠 반영 스크립트, API 호출을 확인할 요청 파일을 생성했다. 원본 시트에서 필요한 파일을 생성하도록 해, 사람이 같은 내용을 여러 곳에 옮겨 적는 단계를 줄였다.
인증·결제와 실제 이용
로그인과 구매를 서비스의 이용 흐름에 연결하기
클라이언트와 앱에서 계정을 사용하고 상품을 구매할 수 있도록 백엔드의 인증·결제 기능을 개발했다. OAuth를 통한 외부 계정 로그인도 연결했다. 결제에서는 주문과 구매 결과를 확인하고, 구매한 상품에 맞는 재화를 지급하는 과정까지 처리해야 했다.
웹 결제와 모바일 구매는 연동 방식이 달랐다. 각 경로에서 구매 결과를 확인한 뒤 서비스의 결제 기록과 재화 지급으로 연결했다. 같은 거래의 구매 결과가 다시 전달되더라도 재화를 중복 지급하지 않도록, 거래 식별자로 이미 처리한 거래인지 확인했다.
출시 전에는 해외 이용자들을 위한 웹 결제 경로를 우선시했다. 하지만 실제로는 기대가 낮았던 모바일 쪽의 결제 전환이 더 높았다. 구매 경로의 우선순위는 구현 전에 세운 예상만으로 정할 수 없었고, 실제 이용자의 구매 행동을 확인하며 다시 검토해야 했다.
BM 변경과 재화 시스템 전환
재화 체계가 바뀌어도 이용자의 기록은 이어져야 했다
제품화 과정에서 진행한 실험에 따라 BM이 바뀌면서, 구현해 둔 재화의 지급·사용 방식도 개편해야 했다. 새 정책에 맞는 계산을 추가하는 것과 함께, 이전 정책에서 이용자가 지급받고 사용한 내역을 새 체계로 이어가는 일이 필요했다. 개편 이후에도 재화가 어디서 생기고 어디에 사용됐는지 추적하고, 그 기록과 잔액을 대조할 수 있어야 했다.
개편 전에는 결제, 이벤트, 콘텐츠 보상과 사용 내역을 각 기능의 테이블에 기록하고, 잔액을 조회할 때 이 기록들을 합산했다. 개편 후에도 개별 내역은 관련 기능의 기록으로 남겼다. 대신 잔액 계산에는 일별로 집계한 지급·사용 금액과 잔액을 사용하도록 바꿨다. 과거 내역 전체를 매번 합산하는 대신, 이전 잔액에 당일 지급·사용분을 반영하는 방식이었다.
일별 집계는 지급이나 사용이 발생할 때마다 해당 날짜의 항목별 합계와 잔액을 갱신하는 방식으로 구성했다. 하루 단위로 기록을 나누되, 재화의 변동은 각 요청을 처리할 때 반영했다. 개별 기록으로는 어떤 결제나 보상, 콘텐츠 이용이 있었는지 확인하고, 일별 집계로는 그날의 변동과 잔액을 확인할 수 있었다. 따라서 이전 잔액에 당일 지급분을 더하고 사용분을 뺀 값이 집계된 잔액과 일치하는지 날짜별로 대조할 기준이 생겼다.
전환 시에는 과거의 지급 출처별 금액과 사용 내역을 집계해 무료·유료 재화의 잔액을 계산하고, 이를 새 집계의 시작값으로 저장했다. 잔액을 계산할 때 이미 새 집계 기록이 있으면 이를 사용하고, 아직 없다면 과거 기록을 집계하도록 했다. 한동안 접속하지 않아 이 과정을 거치지 않은 이용자의 기록은 별도 마이그레이션으로 전환했다.
이후에 발생하는 지급·사용도 개별 기록과 집계에 함께 반영돼야 했다. 이를 위해 결제·보상·콘텐츠 이용을 공통 재화 계산 함수에 연결했다. 결제나 보상 수령 기록과 잔액 증가, 콘텐츠 이용 권한과 재화 차감을 각각 하나의 트랜잭션으로 묶어, 관련 기록과 잔액 중 한쪽만 반영된 상태로 남지 않도록 했다.
결제와 보상 수령, 콘텐츠 이용은 서로 다른 기능이지만 같은 사용자의 잔액을 변경했다. 요청이 겹치면 잔액을 확인한 뒤 변경을 저장하는 동안 다른 요청이 같은 잔액을 사용할 수 있었다. 이 구간의 동시 실행을 제어하기 위해 사용자별 잠금을 사용했다.
재화 체계는 달라졌지만, 이용자의 잔액이 어떤 지급과 사용을 거쳐 만들어졌는지는 계속 설명할 수 있어야 했다. 과거 기록을 새 집계의 시작값으로 옮기고 이후 요청도 같은 기준으로 반영한 것은, 정책이 바뀐 뒤에도 기록과 잔액을 대조할 수 있도록 하기 위해서였다.
이 서비스의 제품화는 모델과 콘텐츠, BM을 계속 실험하는 가운데 진행됐다. 기능을 추가하는 동안 코드 구조와 검증 절차를 정비했고, 정책이 달라지면 기존 이용자의 데이터를 어떻게 이어갈지도 함께 다뤘다. 새로운 시도를 서비스에 반영하려면 바꿀 부분과 유지할 동작을 구분하고, 이미 쌓인 기록이 변경 이후에도 일관되게 처리되도록 해야 했다. PoC를 서비스로 운영해 가면서 계속 확인해야 했던 것은, 새 기능이 동작하는지와 함께 기존 이용자의 데이터와 이용 흐름도 그 변경을 따라갈 수 있는지였다.