[스타트업/기술] SSoT 기반 카탈로그 시스템 설계 | 파편화된 가격 데이터의 비효율성을 극복하고 프로모션 구조 구축하기


서비스를 고도화하는 작업을 진행하면 할수록 초기에 잘 동작하는 것처럼 보였던 설계가 발목을 잡는 순간이 온다. 최근 우리 서비스의 결제 및 가격 구조를 개편하면서 이 사실을 다시 한번 뼈저리게 느꼈다.

기존에는 각 콘텐츠마다 가격을 개별적으로 설정할 수 있도록 설계되어 있었다. 하지만 본격적으로 서비스 출시를 앞두고, 프로모션을 기획해야 하는 시점이 오자 이 구조는 기술 부채로 다가왔다. 오늘은 파편화된 데이터 구조의 한계를 SSoT(Single Source of Truth, 단일 진실 공급원) 원칙을 통해 어떻게 극복했는지, 그리고 이를 통해 어떻게 프로모션 시스템의 토대를 마련했는지 그 고민의 과정을 기록해 본다.


AS-IS: SSoT의 부재와 파편화된 데이터

처음 시스템을 설계할 때는 직관적이었다. 'A 스토리의 가격은 10코인, B 스토리의 가격은 20코인.' 데이터베이스의 스토리 문서(Document) 안에 가격 정보를 직접 박아 넣었다.

Story Document

하지만 이 AS-IS 구조는 치명적인 비효율성을 내포하고 있었다.

  1. 데이터 정합성 문제: 특정 카테고리의 스토리 가격을 일괄 인상해야 한다면? 지금은 콘텐츠가 많이 없어서 괜찮지만... 추후엔 수백, 수천 개의 스토리 문서를 일일이 찾아 업데이트해야 한다. 이 과정에서 단 하나의 문서라도 누락되면 사용자에게 잘못된 가격이 노출된다.

  2. 프로모션 기획의 한계: "론칭 첫 달 동안 특정 장르의 스토리를 30% 할인한다"는 이벤트를 진행하려면, 이벤트 시작 시점에 맞춰 각 스토리마다 일일이 DB를 수정하고, 이벤트가 끝나면 다시 롤백해야 한다. 이는 데이터베이스에 과도한 부하를 줄 뿐만 아니라 장애 발생 확률을 기하급수적으로 높인다.

결론적으로 진실의 원천(Source of Truth)이 수많은 스토리 데이터에 파편화되어 있었기 때문에 발생한 문제였다(열심히 Architect Agent와 씨름을 하다보니 알게 된 사실). 시스템이 '어떤 것이 진짜 가격인가?'를 판단할 중앙 통제소가 없었던 것이다.




TO-BE: '스토리'와 '가격 정책'의 분리

이 문제를 해결하기 위해 데이터의 책임을 분리하기로 했다. 스토리는 스토리의 본질(내용, 메타데이터)만 가지고, 가격은 '옵션(Option)'이라는 새로운 계층으로 분리하여 중앙에서 관리하는 TO-BE 구조를 설계했다.

핵심은 SSoT 원칙의 확립이었다.


1. 스토리 옵션 카탈로그 (Story Option Catalog) 도입

이제 개별 스토리는 가격을 직접 가지지 않는다. 대신 optionKey (예: option_A, option_B)만을 참조한다. 실제 가격 정보는 시스템 설정(System Configs)의 카탈로그 문서에서 중앙 집중식으로 관리된다.

Story Document

작성한 createCatalogCacheService 코드를 보면, DB에서 story_options 데이터를 읽어와 구조화하는 로직을 확인할 수 있다.

  function normalizeStoryOption(optionKey, rawOption) {
    // ... 유효성 검사 로직 생략 ...
    return {
      optionKey,
      purchaseCost,
      adRemovalCost,
      reportSubmitLimit,
      isActive,
      // ...
    };
  }

이제 가격을 변경하고 싶다면, 수천 개의 스토리를 건드릴 필요 없이 카탈로그의 optionKey 데이터 단 한 곳만 수정하면 된다. 진정한 의미의 SSoT가 구축된 것이다. 그리고 혹시나 새로운 가격 정책이 필요하다면 새로운 옵션을 만들고, 스토리에 해당 옵션을 지정하기만 하면 끝이었다!

Option Field


2. 프로모션 시스템의 기반 마련

가격을 중앙에서 통제할 수 있게 되자, 자연스럽게 프로모션 로직을 구현할 수 있는 길이 열렸다. storyPromotionCampaigns 상태를 추가하여, 특정 조건이 만족할 때 기존 카탈로그의 가격을 '덮어쓰기(Overwrite)' 하는 구조를 만들었다.

  function normalizeStoryPromotionCampaign(rawCampaign) {
    // 특정 옵션키나 스토리 코드를 타겟팅
    const targetOptionKeys = hasTargetOptionKeys ? targetOptionKeysRaw : [];
    const targetStoryCodes = hasTargetStoryCodes ? targetStoryCodesRaw : [];
    // 프로모션 기간 설정
    const startsAtMs = toEpochMs(startsAt);
    const endsAtMs = endsAt === null ? null : toEpochMs(endsAt);
    
    // ...

이제 특정 기간(startsAtMs ~ endsAtMs) 동안 특정 타겟(targetOptionKeys)에 대해 할인가(purchaseCost, adRemovalCost)를 적용하는 캠페인을 데이터베이스에 하나만 등록해 두면 된다. 서버는 실시간으로 현재 시간이 프로모션 기간에 해당하는지, 사용자가 요청한 스토리가 타겟에 포함되는지 평가하여 동적으로 가격을 계산한다.

Promotion Field


3. 인메모리 캐싱(In-memory Caching)을 통한 병목 해결

SSoT를 구축하면서 마지막으로 신경을 썼던 부분은 결제 및 조회 요청이 카탈로그 DB로 몰리지 않도록 하는 것이었다. 카탈로그 데이터는 모든 사용자가 빈번하게 조회하지만, 변경은 어드민에 의해 어쩌다 한 번 일어난다. (이러한 특성은 이전에 모델에 캐릭터의 페르소나를 입혀 서빙하는 서버를 구축했을 때도 나타났었다. 이번에도 같은 방법, "캐싱"을 활용했다)

따라서 매번 DB를 조회하는 대신, 메모리 상에 데이터를 올려두고 TTL(Time-To-Live) 기반으로 갱신하는 캐시 서비스(CatalogCacheService)를 구현했다. refreshIfNeeded 함수를 통해 캐시가 만료되었을 때만 백그라운드에서 데이터를 동기화하도록 처리하여, 데이터의 일관성과 응답 성능이라는 두 마리 토끼를 모두 잡을 수 있었다.


마치며

이번 리팩토링은 단순히 코드를 깔끔하게 정리하는 수준을 넘어, 비즈니스의 요구사항(프로모션, 동적 가격 정책)을 기술적으로 소화할 수 있는 기반을 다지는 과정이었다.

처음부터 완벽한 설계는 없다는 것을 안다. 초기에는 빠르게 개발하여 시장의 반응을 보는 것이 중요했기에 이전의 구조가 틀렸다고 생각하지는 않는다. 하지만 비즈니스가 다음 단계로 넘어가야 할 때, 기존 시스템의 근본적인 한계(단일 진실 공급원의 부재)를 파악하고 이를 과감하게 재설계하는 결단력이 필요하다는 점을 깊이 깨달았다. 빠른 프로토타이핑과 robust한 시스템 사이... 사람들의 반응을 보고 검증하는 건 끊임없이 해왔다면 이제 시스템을 점검할 필요가 있었다.

이렇게 탄탄하게 구축된 카탈로그와 프로모션 캐시 레이어 위에서, 앞으로 기획팀이 어떤 이벤트를 가져오더라도 가볍게 대응할 수 있을 것 같아 든든하다.



추천글

[스타트업/기술] Node.js & Google Play 인앱결제 | 결제 시스템 아키텍처 설계 (Part 1)

[스타트업/기술] Node.js & App Store 인앱결제 | 결제 시스템 아키텍처 (Part 2)

[스타트업/기술] FastAPI와 클라우드 네이티브 | Cloud Run 도입과 백엔드 전환기 (Part 2)


hyeon_B

안녕하세요! AI 기술을 이용해 더 나은 세상을 만들어 나가고 싶은 과기원생 Hyeon이라고 합니다. 저는 앞으로 인공지능 시대에는 지식을 '활용'하는 능력이 중요해질 것이라고 생각합니다. 대부분의 일들은 인공지능이 뛰어난 모습을 보이지만, 인공지능은 데이터로 부터 연관관계를 학습하기 때문에 지식들을 새로 통합해서 활용하는 능력이 부족합니다. 인공지능이 뉴턴 전에 만들어졌다면 사과가 떨어지는 이유에 대답하지 못했을 것이고, 아인슈타인 전에 만들어졌다면 중력이 어떻게 생기는지 설명하지 못했을 것입니다. 따라서 앞으로 우리는 '본질'을 탐구하고 그 본질로부터 다른 곳에 적용하며 인공지능을 현명하게 활용해야 할 것입니다. 함께 인공지능 시대를 준비합시다!

댓글 쓰기

다음 이전

POST ADS1

POST ADS 2