1. 개요
지난 글에서는 마이데이터 서비스 운영 중 마주친 메인 화면 응답 지연을 캐시 우선 렌더링과 백그라운드 갱신, 즉 지금의 Stale-While-Revalidate(SWR) 패턴과 같은 방향의 전략으로 해결한 경험을 다뤘습니다. 이번 글은 같은 서비스 운영에서 그다음으로 마주친 과제를 다룹니다. 이번에는 성능이 아니라 비용이 문제였습니다.
외부 기관 조회 API의 과금 구조가 전환되면서 호출 건수 자체가 곧 운영 비용이 되었고, “필요할 때마다 전부 조회한다”는 기존 방식을 그대로 유지하기 어려워졌습니다. 이를 해결하기 위해 적용한 것이 조회 Timestamp 기반의 조건부 호출과 호출 시점의 재배치(배치 갱신 + Lazy Loading)였습니다. 그리고 이번에도 지나고 보니, 당시에는 이름을 알지 못했던 이 접근이 HTTP 프로토콜이 오래전부터 표준으로 제공해 온 조건부 요청(Conditional Request)과 같은 발상이었습니다. 이 글에서는 문제 상황과 해결 과정, 그리고 표준 메커니즘 관점에서의 재해석을 순서대로 정리합니다.
2. 문제 상황: 호출 건수가 곧 비용이 되다
마이데이터 서비스는 여러 외부 금융기관의 표준 API를 호출해 사용자의 자산·거래 정보를 수집하는 구조입니다. 하나의 갱신 흐름에서도 기관 목록 조회, 계좌 목록 조회, 계좌별 상세 조회, 거래 내역 조회처럼 하위 API가 연쇄적으로 호출되기 때문에, 사용자 한 명의 정보를 한 번 갱신하는 데에도 상당한 수의 호출이 발생합니다.
서비스 초기에는 이 호출량이 큰 부담이 아니었지만, 호출 건수 기반의 과금 구조로 전환되면서 상황이 달라졌습니다. 갱신 시점마다 변경 여부와 무관하게 전체를 다시 조회하는 기존 방식에서는, 실제로는 아무것도 바뀌지 않은 데이터를 다시 받아오는 호출에도 동일한 비용이 발생했습니다. 사용자 수가 늘어날수록 이 낭비도 함께 커지는 구조였습니다.
지난 글의 응답 지연이 사용자가 직접 체감하는 문제였다면, 이번 과제는 사용자 눈에는 보이지 않지만 운영 지속성에 직결되는 문제였습니다. 문제를 다시 정의하면 이렇습니다. “어차피 바뀌지 않은 데이터를 확인하기 위해, 매번 전체를 다시 받아올 필요가 있는가.” 결국 해결의 방향은 호출 자체를 없애는 것이 아니라, 불필요한 호출을 식별해서 생략하는 것이 되어야 했습니다.
3. 해결 전략 1: Timestamp 기반 조건부 호출
마이데이터 표준 API 스펙에는 조회 기준 시점을 다루는 Timestamp 개념이 있습니다. 이를 활용하면 “이전 조회 시점 이후로 변경이 있었는가”를 먼저 확인하고, 변경이 없다면 하위 API 호출을 생략한 채 기존에 저장해 둔 데이터를 그대로 사용할 수 있습니다. 변경이 있을 때만 상세 조회를 진행하는 것입니다.
처리 흐름을 의사코드로 정리하면 다음과 같습니다.
function 자산정보갱신(사용자ID, 기관ID):
이전조회시점 = 조회이력.최근조회시점(사용자ID, 기관ID)
변경여부 = 외부기관API.변경확인(사용자ID, 이전조회시점)
if 변경여부 == 없음:
return 저장소.기존데이터(사용자ID, 기관ID)
최신데이터 = 외부기관API.상세조회(사용자ID) // 과금 대상 호출
저장소.갱신(사용자ID, 기관ID, 최신데이터)
조회이력.기록(사용자ID, 기관ID, 현재시각)
return 최신데이터
이 구조의 핵심은 “물어보는 비용은 싸고, 받아오는 비용은 비싸다”는 비대칭을 활용한 것입니다. 변경 여부를 확인하는 가벼운 호출로 먼저 거르고, 실제 데이터 전체를 받아오는 무거운 호출은 변경이 확인된 경우로만 한정했습니다. 데이터가 자주 바뀌지 않는 사용자일수록 절감 효과가 커지는 구조이기도 합니다.
4. 해결 전략 2: 호출 시점의 재배치
조건부 호출이 “호출할 것인가”를 거르는 장치였다면, 두 번째 전략은 “언제 호출할 것인가”를 다시 설계하는 것이었습니다. 두 가지 방식을 조합했습니다.
첫째는 주기 기반 배치 갱신입니다. 사용자가 선택한 갱신 주기를 기준으로, 화면 진입과 무관하게 미리 정해진 시점에 데이터를 갱신해 두는 방식입니다. 갱신 시점이 예측 가능해지므로 호출량이 특정 시점에 몰리지 않고, 사용자가 화면에 진입했을 때는 이미 갱신된 데이터를 바로 보여줄 수 있습니다.
둘째는 상세 데이터의 지연 호출(Lazy Loading)입니다. 계좌별 거래 내역처럼 사용자가 실제로 들어가 봐야 의미가 있는 상세 데이터는, 메인 갱신 흐름에서 미리 받아오지 않고 해당 상세 화면에 진입하는 시점에 호출하도록 분리했습니다. 모든 사용자가 모든 계좌의 상세까지 확인하는 것은 아니기 때문에, 실제로 조회되지 않는 상세 데이터에 대한 호출을 구조적으로 제거할 수 있었습니다.
정리하면 “필요한 시점에, 필요한 만큼만” 호출하는 것입니다. 자주 보는 요약 정보는 주기 배치로 미리, 가끔 보는 상세 정보는 진입 시점에 지연 호출로, 그리고 모든 호출 앞에는 조건부 확인을 두는 3단 구성이었습니다.
4-1. 구조 비교
[표 1] 호출 구조 개선 전후 비교
|
구분 |
기존 구조 (As-Is) |
개선 구조 (To-Be) |
|---|---|---|
|
호출 판단 |
갱신 시점마다 전체 일괄 조회 |
변경 확인 후 필요한 경우만 조회 |
|
호출 시점 |
화면 진입·갱신 요청 시 즉시 |
주기 배치 + 상세 진입 시 지연 호출 |
|
호출량 특성 |
사용자 활동량에 비례해 증가 |
데이터 변경 빈도에 수렴 |
|
상세 데이터 |
메인 흐름에서 선제 조회 |
실제 진입한 화면에서만 조회 |
5. 지금 보면: HTTP 조건부 요청과의 연결
5-1. HTTP가 이미 갖고 있던 메커니즘
HTTP에는 조건부 요청(Conditional Request)이라는 표준 메커니즘이 있습니다. 클라이언트가 이전 응답과 함께 받아 둔 수정 시각(Last-Modified)이나 버전 식별자(ETag)를 다음 요청에 If-Modified-Since, If-None-Match 헤더로 담아 보내면, 서버는 그 이후 변경이 없을 경우 본문 없이 304 Not Modified 응답만 돌려줍니다. “바뀌었는지 먼저 묻고, 안 바뀌었으면 본문 전송을 생략한다”는 발상입니다.
이번 사례의 Timestamp 기반 조건부 호출은 이 메커니즘과 발상이 같습니다. 이전 조회 시점을 기준으로 변경 여부를 먼저 확인하는 것은 If-Modified-Since와 같은 역할이고, 변경이 없을 때 하위 API 호출을 생략하고 저장 데이터를 재사용하는 것은 304 응답과 같은 효과입니다. 다만 지난 글에서 구분했던 것과 마찬가지로, HTTP 헤더를 통해 프로토콜 레벨에서 자동으로 처리된 것이 아니라 표준 API의 Timestamp 스펙을 활용해 애플리케이션 코드로 직접 구현했다는 점에서 레이어가 다릅니다. 프로토콜이 제공하는 메커니즘을 차용한 것이 아니라, 같은 발상을 애플리케이션 레벨에서 독립적으로 구현한 사례입니다.
6. 지난 글과의 관계: 같은 캐싱 모델의 두 축
흥미로운 점은, 지난 글의 SWR 전략과 이번 글의 조건부 호출이 모두 HTTP 캐싱 모델 안에 나란히 존재하는 두 축이라는 것입니다. 방향은 서로 반대입니다. SWR은 “일단 오래된 데이터라도 먼저 주고, 검증은 백그라운드에서 나중에 한다”는 전략으로 응답 속도를 우선합니다. 조건부 요청은 “검증부터 하고, 바뀐 것이 없으면 아예 전송하지 않는다”는 전략으로 전송과 호출 비용을 우선합니다.
[표 2] 두 캐싱 전략 비교
|
항목 |
Stale-While-Revalidate (지난 글) |
조건부 요청 (이번 글) |
|---|---|---|
|
기본 동작 |
오래된 데이터 즉시 응답 후 백그라운드 검증 |
변경 확인 후 변경 시에만 데이터 수신 |
|
우선 가치 |
응답 속도, 사용자 체감 성능 |
호출·전송 비용 절감 |
|
아끼는 것 |
사용자의 대기 시간 |
불필요한 호출과 전송량 |
|
적용한 지점 |
메인 화면 진입 응답 |
외부 기관 조회 호출 |
실제 서비스에서는 두 전략이 서로 다른 지점에서 공존했습니다. 사용자가 마주하는 화면 응답에는 SWR 방향의 전략을, 외부로 나가는 호출에는 조건부 호출을 적용한 것입니다. 하나의 캐싱 이론 아래에서 “무엇을 아낄 것인가”에 따라 반대 방향의 전략이 각자의 자리에서 쓰인 셈입니다.
7. 한계와 트레이드오프
이 구조의 전제는 변경 여부 판단의 기준이 되는 Timestamp가 신뢰할 수 있다는 것입니다. 외부 기관이 변경 시점 정보를 정확하게 제공하지 않으면 변경을 놓치거나, 반대로 불필요한 재조회가 발생할 수 있습니다. 실제 운영에서는 외부 기관마다 스펙 준수 수준에 편차가 있었고, 이로 인한 데이터 품질 문제는 그 자체로 별도의 대응이 필요한 주제였습니다.
갱신 지연의 허용 범위도 계속 조율해야 하는 지점이었습니다. 조건부 생략과 주기 배치가 늘어날수록 비용은 줄지만 데이터의 최신성은 그만큼 뒤로 밀립니다. 지난 글의 캐시 유지 시간 기준과 마찬가지로, 데이터 성격별로 어느 정도의 지연까지 허용할 것인지를 정하는 일은 일괄적인 정답이 없는 개별 판단의 영역이었습니다. 배치 주기 역시 너무 잦으면 절감 효과가 희석되고 너무 뜸하면 신선도가 떨어지는 줄다리기였습니다.
8. 운영 기준으로의 정착
지난 글에서 화면 쪽 캐시 유지 시간을 데이터 유형별 기준표로 정리했던 것과 같은 방식으로, 이번에는 외부 호출 쪽에도 데이터 유형별 호출 전략 기준을 정리해 두었습니다. 새로운 데이터 항목이 추가될 때마다 호출 방식을 처음부터 고민하는 대신, 데이터의 성격을 기준에 대응시켜 결정할 수 있도록 한 것입니다.
[표 3] 데이터 유형별 호출 전략 기준 (예시)
|
데이터 유형 |
호출 시점 |
조건부 확인 |
|---|---|---|
|
자산 요약 (메인 노출) |
주기 기반 배치 갱신 |
적용 (변경 없으면 생략) |
|
계좌 상세·거래 내역 |
상세 화면 진입 시 지연 호출 |
적용 (변경 없으면 생략) |
|
신규 연동 기관 데이터 |
연동 직후 즉시 전체 조회 |
미적용 (비교할 이력 없음) |
이 기준표는 지난 글의 캐시 운영 기준표와 짝을 이룹니다. 화면 쪽에는 “얼마나 오래된 데이터까지 보여줄 것인가”라는 기준이, 호출 쪽에는 “언제, 어떤 조건에서 호출할 것인가”라는 기준이 각각 자리를 잡으면서, 데이터의 수집부터 노출까지 전 구간에 대해 팀이 같은 언어로 판단할 수 있게 되었습니다.
9. 마무리
지난 글의 응답 지연과 이번 글의 호출 비용은 겉보기에 다른 문제였지만, 풀고 나서 보니 결국 같은 질문으로 수렴했습니다. “이 호출이 지금 이 시점에 꼭 필요한가.” 그 질문에 화면 쪽에서 답한 것이 캐시 우선 렌더링이었고, 외부 호출 쪽에서 답한 것이 조건부 호출과 호출 시점의 재배치였습니다.
그리고 이번에도, 당시에는 조건부 요청이라는 표준 메커니즘의 이름을 알지 못한 채 문제에서 출발해 같은 구조에 도달했습니다. 표준과 패턴은 결국 같은 문제를 먼저 겪은 사람들이 정리해 둔 답이라는 것을, 두 번의 경험을 통해 더 분명하게 확인할 수 있었습니다.
cryss