캐시의 CPU와 메모리는 여유로운데 네트워크 전송량만 높았다.

광고 서버 여러 대가 캠페인 설정 정보를 캐시에서 주기적으로 조회하고 있었다. 캠페인 설정은 자주 바뀌지 않는 데이터였다. 그런데 변경 여부와 관계없이 매번 전체 데이터를 가져오는 구조였다. 서버 수가 늘면서 네트워크 전송량이 인스턴스의 허용 범위에 근접했고, 스케일 다운도 할 수 없는 상황이었다.

데이터 분리

캐시에 저장된 캠페인 데이터를 들여다보니 성격이 다른 데이터가 하나로 묶여 있었다.

메타데이터. 캠페인 메타 정보, 타겟팅 조건 같은 데이터는 변경 빈도가 낮다. 광고주가 캠페인을 수정할 때만 바뀐다.

상태 데이터. 예산 소진 현황 같은 데이터는 광고가 노출될 때마다 갱신된다. 항상 최신 상태를 유지해야 한다.

공유 데이터. 광고 소재 정보는 여러 캠페인에서 동일한 콘텐츠를 사용할 수 있다. 캠페인에 포함시키면 중복이 발생한다.

세 가지를 분리했다. 메타데이터와 공유 데이터는 변경분만 갱신하고, 상태 데이터는 매번 갱신하는 구조로 바꿨다.

변경분 갱신

전량 갱신을 변경분 갱신으로 바꾸려면 “무엇이 변경되었는가"를 알 수 있어야 한다.

배치가 DB에서 최신 데이터를 가져온 뒤, 캐시에 저장된 데이터와 비교한다. 내용이 다른 항목만 캐시에 쓴다. 변경 시점은 변경 감지용 인덱스에 timestamp 로 기록해두고, 읽는 쪽에서 마지막 갱신 시점 이후 변경된 항목만 가져온다.

flowchart LR
    subgraph Write ["쓰기 경로"]
        DB["DB"] --> BATCH["배치"]
        BATCH --> CMP{"캐시와
비교"} CMP -->|"변경됨"| WRITE["캐시 갱신
+ timestamp 기록"] CMP -->|"동일"| SKIP["건너뜀"] end subgraph Read ["읽기 경로"] SVC["서비스"] --> TS{"마지막 갱신
이후 변경분?"} TS -->|"있다"| FETCH["변경분만 조회"] TS -->|"없다"| LOCAL["로컬 데이터 유지"] end

쓰기 경로와 읽기 경로를 분리한 것이 핵심이었다. 배치는 변경분만 쓰고, 서비스는 변경분만 읽는다. 패턴의 상세한 원리는 별도로 정리했다.

결과

네트워크 전송량이 크게 줄었다. 변경이 없는 주기에는 거의 전송이 발생하지 않게 됐다. 캐시 인스턴스를 한 단계 낮은 타입으로 스케일 다운할 수 있었다.

돌아보면 이 작업의 시작점은 “무엇이 병목인가"를 정확히 파악한 것이었다. CPU나 메모리가 아니라 네트워크가 병목이라는 것을 먼저 확인했기 때문에, 데이터 분리와 변경분 갱신이라는 방향이 자연스럽게 나왔다.

참고