개발 slecs

구매 링크 도메인을 설정값으로 분리해 운영팀 셀프 변경 실현

목차

파트너 포털에서 구매 링크 기능을 점검하다가 이상한 걸 발견했다. 도메인이 메서드 안에 문자열로 그냥 박혀있었음. 스테이징/운영 환경이 다르고, 화이트라벨 파트너마다 노출 도메인이 갈리는데도 한 줄로 고정된 상태였다.

배포 환경이 늘어날 때마다 분기가 같이 늘어나는 구조였고, 신규 파트너 도메인 하나 붙이려면 코드 수정 후 재배포가 필요했다. 결국 운영팀에서 도메인 한 글자 바꾸겠다고 매번 개발팀을 호출하는 상황까지 왔다.

이게 단순히 불편한 수준의 문제가 아닌 이유는, 구매 링크가 고객에게 직접 노출되는 진입점이기 때문이다. 결제 흐름의 첫 단추인 URL이 잘못된 도메인을 물고 있으면 전환 자체가 끊긴다. 근데 그걸 고치는 데 최소 30분~1시간이 걸린다면, 대응 비용이 비즈니스 속도를 못 따라가는 구조다. 하드코딩이라는 선택이 처음엔 "빠르니까"로 정당화되지만, 파트너 수가 늘고 환경이 분화되면 그 빚이 이자 붙어서 돌아온다.

접근 방식

이미 환경 설정용 저장소가 있었고, 다른 운영 변수들도 거기서 읽고 있었다. 자연스럽게 그 흐름을 타기로 했다. 도메인 키 하나 추가해서 컨트롤러가 조회하도록 바꿨다.

  • Before: 메서드 안에서 문자열 상수 반환
  • After: 설정 저장소 조회 → 캐시 → 반환
  • 폴백: 캐시/조회 실패 시 기본 도메인으로 내려가서 서비스 중단 방지
링크 생성 요청
   ↓
도메인 설정 조회 (캐시 우선)
   ↓
프로토콜 + 도메인 + 경로 조합
   ↓
파트너 응답

폴백을 넣은 이유가 있다. 설정 저장소가 일시적으로 응답 못 하거나, 운영팀이 실수로 키를 날려버리는 상황에서 링크 생성 자체가 500으로 죽으면 안 된다. 기본 도메인으로 내려가는 게 사용자 입장에서 가장 덜 나쁜 선택이다. 완벽하진 않지만 서비스가 멈추는 것보다는 낫다.

코드 구조로 보면 이런 식이다:

def get_purchase_domain(partner_id: str) -> str:
    cache_key = f"purchase_domain:{partner_id}"
    cached = cache.get(cache_key)
    if cached:
        return cached

    try:
        domain = config_store.get(f"partner.{partner_id}.purchase_domain")
    except ConfigStoreError:
        domain = None

    result = domain or DEFAULT_PURCHASE_DOMAIN
    cache.set(cache_key, result, ttl=300)
    return result

실제 코드는 다르지만 흐름은 이렇다. 핵심은 예외를 삼켜서 폴백으로 이어지게 만드는 것, 그리고 캐시 키를 파트너 단위로 분리해서 개별 무효화가 가능하게 한 것.

캐시 무효화에서 걸렸던 지점

단순 치환으로 끝날 줄 알았는데 캐시 무효화에서 한 번 막혔다. 운영팀이 설정을 바꿔도 캐시 TTL이 살아있는 동안은 옛 도메인이 그대로 나가는 문제였다. "바꿨는데 왜 안 바뀌냐"는 문의가 바로 올 게 뻔했다.

설정 변경 이벤트 시점에 해당 캐시 키를 명시적으로 비우는 훅을 추가해서 정리했다. 이게 없으면 TTL 기반 캐시는 설정 저장소와 무결성을 보장하지 못한다. 사용자 눈에는 그냥 "도메인이 느리게 바뀌는 것"처럼 보이지만, 실제로는 캐시가 진실을 가리고 있는 상태다.

캐시를 도입할 때 무효화 경로를 같이 설계해야 한다는 원칙을 여기서 다시 확인했다. TTL 하나만 믿고 운영하면, 변경 즉시 반영이 필요한 운영 값에는 맞지 않는다. 아래처럼 두 축이 같이 굴러가야 한다:

무효화 전략 장점 단점
TTL 만료 구현 단순, 자동 만료 변경 후 지연 반영
명시적 evict (훅) 즉시 반영 훅 누락 시 불일치
TTL + 훅 병행 안전망 이중화 구현 복잡도 소폭 증가

이번엔 두 가지를 병행했다. 훅이 실패하더라도 TTL이 백업 역할을 한다.

결과적으로 변경 흐름은 이렇게 바뀌었다:

항목 Before After
도메인 변경 코드 수정 + 재배포 설정값 업데이트
환경별 분기 if-else 하드코딩 환경별 레코드
운영 개입 개발팀 호출 운영팀 셀프 처리
평균 변경 시간 30분~1시간 30초

회고

외부에 노출되는 URL은 하드코딩 순간부터 빚이 된다. 고객 응대 진입점이나 결제대행사 콜백처럼 환경/파트너별로 갈리는 값은 처음부터 설정으로 빼는 게 맞다. 이번처럼 나중에 뜯어고치면 캐시 무효화 같은 파생 문제가 따라오고, 그걸 다 잡는 데 시간이 더 든다. 처음 설계할 때 "이 값이 환경마다 다를 수 있나?"를 한 번만 더 물어봤으면 애초에 생기지 않았을 이슈다.

비슷한 패턴이 코드베이스 여기저기 더 있을 가능성이 높다. 결제 콜백 URL, 파트너 전용 리다이렉트 경로, 외부 API 엔드포인트 같은 값들이 대표적인 후보다. 이런 것들을 한꺼번에 정리하는 게 이상적이지만, 일단 이번 건처럼 통증이 느껴지는 지점부터 하나씩 치워나가는 것도 현실적인 접근이다.

작아 보이는 fix였는데 체감 효과가 컸다. 운영팀이 직접 만질 수 있는 영역이 한 칸 늘었다는 게 단순히 티켓 한 장이 줄어드는 게 아니라, 앞으로 생길 티켓 수십 장을 미리 지운다는 의미기 때문이다. 개발팀 입장에서도 긴급 배포를 줄이는 쪽이 훨씬 이득이고, 운영팀 입장에서도 대기 없이 바로 조정할 수 있다는 게 실제 체감으로 다르다. 이런 종류의 작업이 화려하지 않아서 묻히기 쉬운데, 팀 전체 운영 부하를 줄이는 데는 직접적으로 연결된다.

댓글 0

첫 댓글 달아줘.