본문 바로가기

코딩/트러블슈팅

"당신의 서비스가 갑자기 멈출 수 있습니다": 면접에서 깨달은 API 쿼타의 함정

🎯 면접에서 받은 깨달음

어느 날 면접을 보러 갔는데 감사하게도 제 프로젝트, 포트폴리오, 블로그 글을 모두 살펴봐주신 면접관님을 만나게 되었습니다.

그때 받은 질문 하나가 아래와 같습니다.

🧑🏻‍💻면접관: 제가 프로젝트를 다 살펴보았는데요. 날씨 앱에 아주 치명적인 문제가 있습니다. API는 많이 호출하면 호출할수록 서버 부하가 올 수 있기 때문에 정해진 쿼타가 있습니다. 그 쿼타의 존재를 알고 있었나요?
👩🏻 : ...몰랐습니다. 
🧑🏻‍💻면접관: 쿼타가 있기 때문에 많은 트래픽이 발생한다면 서비스가 멈출 수 있는 아주 위험한 상황이 올 수 있어요. 그 문제점을 이제라도 알게 되었으니 어떻게 수정할 생각인가요? 이미 답을 알고 있을 테니 한 번 생각해 보고 말해주세요.
👩🏻 : ...로컬스토리지나 세션스토리지 등을 이용하면 될 것 같습니다.
🧑🏻‍💻면접관: 맞습니다.

 

📱 기존 날씨 앱의 문제점

제가 만든 날씨 앱은 간단한 앱이었지만, 총 6개의 API를 사용하고 있었습니다.

사용 중인 API들

  • HTML5 Geolocation: 사용자의 현재 위치 좌표 획득을 위해 사용
  • 카카오 맵 API: 위도/경도를 주소로 변환을 위해 사용
  • 기상청 종관예보 API: 어제의 온도를 가져오기 위해 사용 (11시 갱신)
  • 기상청 단기예보 API: 오늘, 내일, 글피, 그글피의 데이터(온도, 날씨)를 가져오기 위해 사용
  • 기상청 중기육상예보 API: 주간 날씨 데이터를 가져오기 위해 사용 (6시/18시 갱신)
  • 기상청 중기기온조회 API: 주간 온도 데이터를 가져오기 위해 사용 (6시/18시 갱신)

주가 되는 기상청 API 4개는 모두 데이터 갱신 주기가 달랐습니다. 그렇기 때문에 실시간으로 데이터를 받아와야 한다고 생각해 캐싱 기능을 고려하지 않았습니다. 그 잘못된 생각으로 사용자가 페이지를 새로고침할 때마다 API를 매번 새롭게 호출되고 있었습니다..

 

😱 쿼타의 존재를 알게 된 후

면접 중이라 크게 당황했으나 그 와중에서 굉장히 좋은 경험을 하고 있다는 것을 깨달았습니다. 이렇게까지 친절하게 알려주신다는 것에  감동받았습니다. 그때의 일을 계기로 이 날씨 앱을 수정해야겠다는 마음을 먹게 되었고, 그리 어렵지 않게 코드를 수정하게 되었습니다.

 

API 쿼타의 존재를 알게 된 후, 제가 사용하고 있는 기상청 API는 모두 아래와 같은 안내를 하고 있었습니다.

왜 그때는 보이지 않았는지🤦🏻‍♀️...

기상청 api 신청가능 트래픽

 

🤔 어떤 캐싱 방법이 최적일까?

이제는 로컬스토리지, 세션스토리지 등 여러 방법 중 '어떤 방법이 나의 프로젝트에 가장 적합할까?'를 고민하게 되었습니다.

 

1. LocalStorage

  • 장점: 영구 저장 (직접 삭제하기 전까지), 5-10MB 용량, 구현 간단
  • 단점: 동기적 처리 (대용량 데이터시 느림)
  • 날씨 앱에 적합한 이유: 날씨 데이터는 작고, 일정 시간 캐싱이 필요

2. SessionStorage

  • 장점: 탭/브라우저 닫으면 자동 삭제, 임시 데이터에 적합
  • 단점: 새 탭에서는 데이터 없음
  • 사용 시나리오: 임시 검색 결과, 폼 데이터 등
  • 결론: 브라우저 탭을 닫으면 데이터가 삭제되므로 날씨 앱에는 적합하지 않음

3. Cookie

  • 장점: 서버로 자동 전송, 만료 시간 설정 가능
  • 단점: 4KB 용량 제한, 매 요청마다 전송되어 트래픽 증가
  • 사용 시나리오: 인증 토큰, 작은 설정값

4. IndexedDB

  • 장점: 더 큰 데이터 저장 가능
  • 단점: 구현이 복잡함

따라서 간단한 날씨 앱에는 로컬 스토리지가 적절한 선택이었습니다.

 

🔧 캐시 구현하기

그리하여 캐시에 관련된 cache.ts를 만들어 캐시를 가져오는 함수와 없다면 세팅하는 함수를 만들었습니다.

첫 번째 구현

import { userLocation } from "../types/type";

const CACHE_DURATION = 1000 * 60 * 60 * 24;

export const getCachedWeather = (location: userLocation) => {
  const key = `weather_${location.x}_${location.y}`;
  const cached = localStorage.getItem(key);

  if (!cached) return null;
  const { data, time } = JSON.parse(cached);

  if (Date.now() - time > CACHE_DURATION) {
    localStorage.removeItem(key);
    return null;
  }

  return data;
};

export const setCachedWeather = (location: userLocation, data: any) => {
  const key = `weather_${location.x}_${location.y}`;
  const cacheData = {
    data,
    time: Date.now(),
  };
  localStorage.setItem(key, JSON.stringify(cacheData));
};

🚨 문제 발견!

하지만 한 가지 더 문제점이 발견되었습니다.

00:00시, 다시 말해 자정이 지나면 날짜가 바뀌는데 캐시는 여전히 "어제에 해당하는 날짜에 오늘"을 보여줄 수 있는 문제가 발생했습니다.

6월 18일 23:30에 캐시 저장
어제: 6/17, 오늘: 6/18, 내일: 6/19

6월 19일 00:10에 접속 (날짜 바뀜!)
실제 - 어제: 6/18, 오늘: 6/19, 내일: 6/20
하지만 캐시는 - 어제: 6/17, 오늘: 6/18, 내일: 6/20 ❌

 

캐시 key에 날짜 정보가 없어서, 6월 18일에 저장된 캐시가 6월 19일에도 그대로 사용되는 문제인 것입니다.

key에 오늘 날짜를 주어 해결하여 자정이 되면 캐시를 다시 저장하게 수정하였습니다.

최종 구현

import { DATES } from "../constants/date";
import { userLocation } from "../types/type";
import { getFormattedDate } from "./date";

type CacheType = "shortTerm" | "weekly";

// 캐시 지속 시간 30분
const CACHE_DURATION = 30 * 60 * 1000;

export const getCachedWeather = (location: userLocation, type: CacheType) => {
  const today = getFormattedDate(0); // 오늘 날짜 가져오기
  const key = `weather_${type}_${today}_${location.x}_${location.y}`; // 날짜와 타입 포함
  const cached = localStorage.getItem(key);

  if (!cached) return null;
  const { data, time } = JSON.parse(cached);

  // 30분 지났으면 캐시 삭제
  if (Date.now() - time > CACHE_DURATION) {
    localStorage.removeItem(key);
    return null;
  }

  return data;
};

export const setCachedWeather = (
  location: userLocation,
  data: any,
  type: CacheType
) => {
  const today = getFormattedDate(0); // 오늘 날짜 가져오기
  const key = `weather_${type}_${today}_${location.x}_${location.y}`; // 날짜와 타입 포함
  const cacheData = {
    data,
    time: Date.now(),
  };
  localStorage.setItem(key, JSON.stringify(cacheData));
};

 

🎯 타입 안전성 개선

그리고 `setCachedWeather`의 매개변수 `data`가 `any`로 되어 있는 점이 굉장히 찜찜했습니다.

여기에서 `data`는 `CacheType`에 따라 완전히 다른 데이터 구조를 가지게 됩니다. 단기예보 데이터일 때는 배열 형태의 날씨 데이터, 주간예보 데이터일 때는 주간 온도와 주간 날씨 아이콘을 가진 튜플 형태인데, 이런 복잡한 타입을 정의하기 어려워 급하게 `any` 타입을 쓰게 되었습니다.

하지만 `any`를 쓰는 것은 TypeScript를 쓰는 이유가 없어지는 것으로 지양해야 하므로 `unknown` 타입으로 수정하게 되었습니다.

`any`와 `unknown` 차이

any unknown
모든 타입 할당 가능 모든 타입 할당 가능
타입 체크 없이 모든 연산 허용 타입 가드 후에만 연산 허용
타입 안전성 ❌ 타입 안전성 ✅

 

📈 최종 결과

Before (캐싱 전)

  • 사용자가 새로고침할 때마다 4개 API 모두 호출
  • API 쿼타 초과 위험성 높음

After (캐싱 후)

  • 같은 위치, 같은 날짜는 캐시 활용
  • API 호출량 대폭 감소
  • 사용자 경험 개선 (빠른 로딩)

🎓 배운 점

  1. API 쿼타의 중요성: 모든 외부 API에는 제한이 있다는 것을 항상 고려하기
  2. 캐싱 전략: 데이터의 특성을 고려한 설계의 중요성
  3. 타입 안전성: `any` 대신 `unknown`으로 더 안전한 코드 작성
  4. 사용자 관점: 개발자 편의가 아닌 실제 서비스 운영 관점에서 생각하기

면접에서 받은 피드백 덕분에 더 나은 개발자가 될 수 있었습니다.

이제는 모든 외부 API 사용 시 쿼타 및 그에 따른 해결책을 고려하게 되었고, 사용자 관점에서 서비스를 바라보는 시각을 기를 수 있었습니다. 또한 좋은 면접관을 만나는 것도 정말 큰 행운이라는 걸 깨달았습니다. 면접 때도 감사하다고 말씀드렸지만, 이 자리를 빌려 다시 한번 감사합니다🙇🏻‍♀️