보안 프록시와 인메모리 캐시를 거쳐 스마트폰 앱으로 데이터가 안전하게 전송되는 과정을 묘사한 테크 3D 일러스트

공공데이터 API 앱 연동 시 필수 보안 아키텍처와 성능 최적화 가이드

공공데이터 API 앱 연동 시 필수 보안 아키텍처와 성능 최적화 가이드

💡 TL;DR (3줄 요약)

  • 보안 위협: 공공데이터 API의 인증키(serviceKey)를 모바일 앱에 직접 하드코딩해 호출하면 역컴파일러(JADX 등)에 의해 단 5분 안에 탈취당할 수 있으며, 타인의 무단 호출로 인한 쿼터 소진 및 서비스 중단 위험에 직면합니다.
  • 해결 대안 (프록시 서버): 모바일 앱과 공공데이터 API 사이에 백엔드 중계(Proxy) 서버를 배치하고 환경변수로 API Key를 숨기면, 보안 인증키를 완벽하게 은닉하고 브라우저 CORS 문제까지 원천 해결할 수 있습니다.
  • 성능 극대화 (인메모리 캐싱): 갱신 주기가 정해진 공공데이터 특성에 맞춰 프록시 서버에 메모리 캐시 레이어를 도입하면 일일 10,000건의 호출 쿼터 제한을 극복하고, 앱 응답 속도를 10ms 이하로 단축시킬 수 있습니다.

1. 왜 프록시 서버를 사용하는가? (직접 호출의 문제 해결)

다양한 모바일 서비스나 웹 애플리케이션을 개발할 때, 정부나 공공기관에서 제공하는 공공데이터포털(data.go.kr)의 Open API는 매우 유용한 자원입니다. 날씨, 대기오염, 전국 주차장 정보 등 실생활에 밀접한 데이터를 무료로 활용할 수 있기 때문입니다. 그러나 많은 초보 개발자나 1인 창업가들이 구현의 간편함만을 쫓아 모바일 앱(클라이언트) 코드 내에서 공공데이터 API 서버를 직접 호출하는 구조를 설계하곤 합니다. 이러한 아키텍처는 프로덕션 단계에서 다음과 같은 치명적인 문제를 야기합니다.

① 5분 만에 털리는 API 인증키 보안 취약점

가장 심각한 문제는 보안입니다. 공공데이터포털의 API를 호출하기 위해서는 개인별/서비스별로 발급받은 고유의 serviceKey(인증키)가 필요합니다. 만약 React Native, Flutter, Swift, Kotlin 등의 모바일 앱 소스 코드나 설정 파일 내에 이 인증키를 하드코딩하여 직접 요청을 보낸다면, 악의적인 크래커들에게 좋은 먹잇감이 됩니다.
모바일 앱 배포 파일(.apk 또는 .ipa)은 JADX, Apktool 등의 역컴파일(Decompilation) 도구를 사용하면 단 5분 이내에 내부 소스 코드와 리소스 텍스트가 평문으로 추출됩니다. 노출된 API 키를 해커가 무단 복제하여 자신들의 앱이나 트래픽 유발 공격에 도용할 경우, 원격 인증키 제재를 받거나 쿼터가 소진되어 정작 본인의 서비스가 마비되는 사태가 벌어집니다. 이는 OWASP API Security Top 10에서 강조하는 인증 제어 실패 및 자산 관리 소홀에 정확히 해당합니다.

② 일일 호출 제한(Quota) 초과로 인한 서비스 중단

공공데이터포털에서 발급되는 일반 API 인증키는 대개 일일 호출 제한 쿼터(보통 하루 10,000건 내외)가 설정되어 있습니다. 서비스 초기에는 사용자가 적어 문제가 없어 보이지만, 실사용자가 500명에서 1,000명만 되어도 앱이 실행되거나 새로고침할 때마다 API를 실시간 호출하게 되면 몇 시간 만에 하루 할당량이 소진됩니다. 쿼터가 소진되면 공공 API 서버는 에러를 반환하며, 내 앱은 모든 데이터 로딩이 불가능해져 서비스 불능 상태에 빠집니다.

③ 간헐적인 API 서버 다운 및 무거운 응답 속도

국내 공공 API 서버들은 주기적인 점검 시간(보통 새벽 시간대)이나 트래픽이 몰리는 아침 출근 시간에 서버 지연(Latency)이 매우 심해집니다. 직접 API를 호출할 경우 평소 300ms 수준이던 응답 속도가 5초 이상으로 지연되거나 HTTP 500/503 에러를 뿜는 일이 다반사입니다. 클라이언트 앱에서 네트워크 대기 시간이 과도하게 늘어나면 사용자는 앱이 멈춘 것처럼 느끼거나 강제 종료(ANR) 현상을 경험하게 됩니다.

따라서 안전하고 확장 가능한 서비스를 구축하기 위해서는 클라이언트와 공공데이터 서버 사이에 보안과 캐싱을 담당할 중계(Proxy) 서버 아키텍처를 적용하는 것이 필수적입니다. 이 과정에서 백엔드 배포 환경과 인증 프레임워크 등을 고려할 때, 복잡한 인프라 관리가 부담스럽다면 역방향 프록시와 SSL 자동 갱신을 지원하는 Caddy 서버 완벽 가이드 포스트를 참고하여 손쉽게 서버를 라우팅하고 배포 환경을 갖출 수 있습니다.


2. 공공데이터 API 앱 연동을 위한 보안 아키텍처 (Proxy 패턴)

이러한 한계점들을 근본적으로 해결하기 위한 표준 아키텍처가 바로 프록시(Proxy, 대리) 서버 패턴입니다. 앱 클라이언트가 공공데이터 서버와 통신하는 대신, 내가 직접 관리하는 백엔드 서버(Proxy)를 거쳐서 통신하는 방식입니다.

sequenceDiagram
    autonumber
    actor User as 앱 사용자
    participant App as 모바일 앱 (Client)
    participant Proxy as 자체 프록시 서버 (Node.js)
    participant Public as 공공데이터 포털 API

    User->>App: 앱 실행 (예: 날씨 정보 화면 진입)
    App->>Proxy: GET /api/weather (인증키 노출 없음)
    note over Proxy: 1. 보안 키(.env) 로드<br/>2. 메모리 캐시 유효 여부 검증
    alt 캐시 히트 (Cache Hit)
        Proxy-->>App: 10ms 이내 캐시 데이터 응답
    else 캐시 미스 (Cache Miss)
        Proxy->>Public: GET /getUltraSrtNcst?serviceKey=SECRET_KEY (서버 간 통신)
        Public-->>Proxy: XML/JSON 원본 데이터 반환 (지연 시간 500ms~3000ms)
        note over Proxy: 1. 필수 필드 정제 및 가공<br/>2. 메모리 캐시에 저장
        Proxy-->>App: 정제된 최소화 JSON 데이터 응답
    end
    App-->>User: 날씨 UI 렌더링

이 아키텍처가 제공하는 핵심 이점은 다음과 같습니다.
1. 인증키 은닉: 비밀 인증키(serviceKey)는 자체 프록시 서버의 환경변수(.env 파일이나 시스템 설정값)에 보관되므로, 클라이언트 패킷 분석이나 앱 디컴파일로 절대 노출되지 않습니다.
2. CORS 에러 해결: 웹 브라우저 환경(Hybrid App, PWA 등)에서 도메인이 달라 발생하는 Cross-Origin Resource Sharing(CORS) 오류를 프록시 서버 단에서 CORS 헤더를 적절히 열어줌으로써 손쉽게 우회할 수 있습니다.
3. 데이터 경량화: 공공데이터 API는 불필요하게 깊은 구조의 XML이나 사용하지 않는 수십 개의 필드를 함께 반환하여 네트워크 용량을 낭비하게 만듭니다. 프록시 서버가 중간에서 딱 앱에 띄울 필드(예: 기온, 풍속 등 3~4개)만 필터링한 가벼운 JSON 데이터로 포맷을 재정리하여 앱으로 보냄으로써 대역폭 비용과 클라이언트 파싱 연산을 줄여줍니다.
4. 안정성 및 모니터링: 백엔드와 클라이언트 사이의 트래픽을 스스로 제어할 수 있으므로, 어떤 유저가 비정상적인 횟수로 요청을 보내는지 파악하고 차단할 수 있습니다. 또한 백엔드 API 단에서 발생할 수 있는 잠재적 취약점이나 에이전트 간의 통신 검증을 고도화하고 싶다면 Promptfoo LLM 테스트 프레임워크 가이드를 통해 테스팅 가이드라인과 품질 검증을 수행해 보는 것을 추천합니다.


3. 순수 Node.js 기반 안전한 프록시 및 캐시 서버 구현 (Zero Dependency)

프록시 서버를 구현하기 위해 Express나 Axios 같은 무거운 프레임워크를 반드시 사용할 필요는 없습니다. 특히 1인 창업가나 소규모 MVP를 제작할 때는 외부 라이브러리 설치와 유지보수 비용을 줄이는 것이 이상적입니다.

아래 코드는 외부 패키지 설치 없이 Node.js 18 버전 이상에 기본 내장된 네이티브 HTTP 모듈과 fetch API, 그리고 인메모리 Map 구조를 활용해 캐싱과 데이터 정제를 완벽하게 수행하는 프록시 서버 예제입니다.

/**
 * 
 * 공공데이터 API 앱 연동을 위한 네이티브 Node.js 프록시 서버
 * - 별도의 npm install 없이 즉시 실행 가능 (Zero-Dependency)
 * - 환경변수를 활용한 API 키 보호
 * - 인메모리 Map을 통한 캐싱 레이어 구현 (TTL 1시간)
 * - 불필요한 공공데이터 필드를 걸러내는 데이터 경량화 탑재
 */

const http = require('http');
const { URL } = require('url');

// 1. 환경변수에서 보안 인증키(serviceKey) 로드
// 실행 시: PUBLIC_DATA_SERVICE_KEY="인증키" node server.js
const SERVICE_KEY = process.env.PUBLIC_DATA_SERVICE_KEY || 'YOUR_SECURE_API_KEY_HERE';

// 2. 인메모리 캐시 저장소 구축
const cacheStore = new Map();
const CACHE_TTL_MS = 60 * 60 * 1000; // 캐시 유지 시간: 1시간 (기상청 단기예보 갱신 주기에 최적화)

const server = http.createServer(async (req, res) => {
  // CORS 정책 설정 (모바일 앱 및 브라우저 교차 요청 허용)
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
  res.setHeader('Content-Type', 'application/json; charset=utf-8');

  // Preflight(예비 요청) 처리
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  const parsedUrl = new URL(req.url, `http://${req.headers.host}`);

  // 날씨 API 중계 엔드포인트: GET /api/weather
  if (req.method === 'GET' && parsedUrl.pathname === '/api/weather') {
    const cacheKey = 'weather_data';
    const cachedItem = cacheStore.get(cacheKey);

    // 3. 캐시 유효성 검사 (Cache Hit 판단)
    if (cachedItem && (Date.now() - cachedItem.timestamp < CACHE_TTL_MS)) {
      res.writeHead(200);
      res.end(JSON.stringify({
        source: 'local_proxy_cache',
        fetchedAt: new Date(cachedItem.timestamp).toISOString(),
        data: cachedItem.data
      }));
      return;
    }

    // 4. 캐시가 없거나 만료된 경우 (Cache Miss) 공공 API 직접 호출
    try {
      // 기상청 단기예보(초단기실황조회) API 엔드포인트 조립
      const baseUrl = 'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst';
      const queryParams = new URLSearchParams({
        serviceKey: SERVICE_KEY,     // 환경변수에서 불러온 보안 인증키 자동 주입
        pageNo: '1',
        numOfRows: '10',
        dataType: 'JSON',
        base_date: '20260713',        // (예시 수치) 실무에서는 날짜 유동적 생성 필요
        base_time: '0600',
        nx: '55',                     // 서울 중심부 가상 좌표
        ny: '127'
      });

      const targetUrl = `${baseUrl}?${queryParams.toString()}`;

      // Node.js 18+ 내장 fetch 사용
      const response = await fetch(targetUrl);

      if (!response.ok) {
        throw new Error(`공공 API 호출 에러 (HTTP Status: ${response.status})`);
      }

      const rawData = await response.json();

      // 공공데이터 API 내부 에러코드 예외 처리
      const responseCode = rawData.response?.header?.resultCode;
      if (responseCode !== '00') {
        throw new Error(`공공 API 비즈니스 에러: ${rawData.response?.header?.resultMsg || '알 수 없는 오류'}`);
      }

      // 5. 공공 API의 원본 페이로드 정제 및 가공 (필요한 데이터만 필터링)
      const rawItems = rawData.response?.body?.items?.item || [];
      const refinedData = rawItems.map(item => {
        // 날씨 값 매핑 맵 정의 (T1H: 기온, REH: 습도, WSD: 풍속 등)
        let categoryName = item.category;
        if (item.category === 'T1H') categoryName = 'temperature';
        if (item.category === 'REH') categoryName = 'humidity';
        if (item.category === 'WSD') categoryName = 'wind_speed';

        return {
          metric: categoryName,
          value: parseFloat(item.obsrValue)
        };
      });

      // 6. 가공된 최신 데이터를 캐시에 저장
      cacheStore.set(cacheKey, {
        timestamp: Date.now(),
        data: refinedData
      });

      // 클라이언트에 최종 응답 전달
      res.writeHead(200);
      res.end(JSON.stringify({
        source: 'public_data_portal',
        fetchedAt: new Date().toISOString(),
        data: refinedData
      }));

    } catch (error) {
      console.error('프록시 서버 에러 로그:', error.message);

      // 만약 공공 API 점검 등으로 서버가 맛이 갔다면, 에러를 내뿜기보다 
      // 만료된 이전 캐시 데이터가 있을 시 그것을 반환하는 회복력(Resilience) 설계 적용
      if (cachedItem) {
        res.writeHead(200);
        res.end(JSON.stringify({
          source: 'expired_fallback_cache',
          fetchedAt: new Date(cachedItem.timestamp).toISOString(),
          warning: '공공 API 장애로 인해 만료된 캐시 데이터를 반환했습니다.',
          data: cachedItem.data
        }));
        return;
      }

      // 백업 캐시마저 없다면 최후의 수단으로 500 에러 처리
      res.writeHead(500);
      res.end(JSON.stringify({
        error: '공공데이터 동기화 실패',
        details: error.message
      }));
    }
  } else {
    // 404 경로 예외 처리
    res.writeHead(404);
    res.end(JSON.stringify({ error: '요청하신 엔드포인트를 찾을 수 없습니다.' }));
  }
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`[안전 프록시 서버 구동 완료] http://localhost:${PORT}`);
});

4. 공공데이터 API 앱 연동을 위한 구체적인 유즈케이스와 실무 시나리오

설계한 아키텍처가 정상적으로 작동하고 예외 상황을 완벽하게 극복하는지 확인하기 위해 다음의 3가지 실무 시나리오를 바탕으로 검증 프로세스를 진행합니다.

시나리오 구분 사용자 상황 및 입력 중계 서버 동작 및 처리 단계 기대 출력 (Expected Output) 성공 검증 지표 실패 대응 방안
① 최초 요청 및 캐시 미스 앱이 켜진 후 최초로 날씨 정보를 요청함. (GET /api/weather 호출) 프록시가 캐시를 검사했으나 데이터가 없음 (Cache Miss). 공공 API 서버로 serviceKey를 실시간 탑재해 직접 요청함. source: "public_data_portal" 헤더와 함께 기온 및 습도가 정제된 JSON 형식으로 전달됨. 원격 서버 응답 속도가 800ms 내외로 반환되고, 캐시 맵에 데이터가 저장됨. 공공 API 서버 장애 시, 하드코딩된 기본값(서울 평균값 등)을 반환하는 Fallback 로직 실행.
② 단기 재호출 및 캐시 히트 앱 사용자가 화면을 아래로 당겨 강제로 새로고침을 시도함. 프록시가 메모리를 체크하고, 1시간(TTL)이 지나지 않았음을 확인(Cache Hit). 공공데이터 서버로 요청을 보내지 않고 즉시 반환. source: "local_proxy_cache" 헤더와 함께 기존 저장되었던 날씨 데이터 전달. 응답 지연 시간이 10ms 이하(0ms~3ms 수준)로 수렴하고 공공 API 호출 카운트가 오르지 않음. 캐시 맵이 가득 차 메모리 누수가 발생하지 않도록 TTL 검사 후 주기적으로 청소하는 래퍼 적용.
③ 공공 API 장애 대비 안전 모드 공공데이터 포털 서버가 정기 점검으로 인해 HTTP 503 에러를 발생시킴. 프록시가 공공 API 호출 실패를 감지함. 에러를 뿜는 대신 메모리에 보관 중이던 1시간이 넘은 지난 캐시 데이터를 재활용하기로 결정함. source: "expired_fallback_cache" 와 경고 메시지(warning)가 포함된 응답 반환. 공공 API가 죽어있는 상태에서도 앱 클라이언트는 튕김 현상 없이 이전 데이터를 정상 렌더링함. 캐시 데이터조차 없는 아예 극초기 기동 상태일 시, 클라이언트에 빈 배열 대신 “서버 점검 중입니다”라는 사용자 친화적 문구를 내려줌.

5. 프록시 아키텍처 도입 시 트레이드오프와 대안 비교

모든 기술 아키텍처는 이득이 있으면 대가가 따르는 법입니다. 프록시 서버 도입의 이해득실과 인프라 대안들을 명확히 비교하여 서비스 규모에 맞는 합리적인 의사결정을 내려야 합니다.

비교 항목 클라이언트 직접 호출 (Ad-hoc) 자체 백엔드 프록시 (Node.js) Serverless API Gateway (AWS)
인증키 보안성 매우 낮음 (디컴파일러로 5분 내 유출) 높음 (서버 환경변수에 숨김) 매우 높음 (AWS Parameter Store 연동)
초기 비용 및 공수 없음 (서버 세팅 필요 없음) 보통 (소형 서버 1대 세팅 및 배포) 높음 (AWS 콘솔 및 CloudFormation 복잡도)
쿼터 소모 절감 불가능 (유저 수 비례 급격한 증가) 탁월함 (인메모리 캐싱으로 95% 이상 절감) 우수함 (CloudFront/APIGW 캐시 적용)
CORS 해결 여부 에러 발생 (공공포털 서버가 브라우저 차단) 완벽히 가능 (CORS 헤더 제어 가능) 완벽히 가능 (인프라 수준 설정)
적합한 권장 시점 포털 내 API 데이터 구조를 단순히 로컬에서 테스트해볼 때 1인 개발자, 스타트업 MVP 출시 및 대중 서비스 기동 시 대규모 트래픽 분산과 고부하 분리가 필요한 엔터프라이즈 환경

“절대 프록시 캐시를 사용하면 안 되는 상황은 없을까?”

만약 제공하려는 서비스가 ‘실시간 지진 경보’, ‘재난 긴급 알림’ 또는 ‘실시간 도로 교통 통제 현황’과 같이 데이터의 1초 지연이 중대한 인명 및 재산 피해로 직결되는 도메인이라면, 1시간짜리 로컬 캐싱은 치명적인 부작용을 초래합니다. 이러한 경우에는 캐싱 TTL을 0초로 설정하거나, 프록시 캐시를 스킵하고 즉시 원격을 호출하되, 쿼터 제한을 확장하기 위해 공공데이터포털에 공식 양식을 작성하여 “특별 트래픽 제한 완화 승인(Quota Limit Increase)”을 받아내는 절차를 진행해야 합니다.

또한, 글로벌 스케일로 서비스를 확장하면서 프록시 서버 주변의 인프라 보안과 규제 이슈가 복잡해진다면, 미국 상무부의 사상 첫 수출 통제 사태로 불거진 Claude Sonnet 5 출시와 미국의 AI 통제 선언 포스트를 읽어보고 글로벌 공급망 제약에 대한 통찰을 미리 갖추는 것도 비즈니스 리스크 관리에 큰 도움이 됩니다.


6. 결론: 24시간 이내에 지금 당장 시작해야 할 실행 로드맵

앱 출시 전, 소 잃고 외양간 고치기 전에 바로 오늘 시작할 수 있는 구체적인 3단계 실행 가이드라인입니다.

  • Step 1 — 로컬 환경에서 프록시 서버 코드 실행해보기:
    • 위 3절의 순수 Node.js 프록시 코드를 복사하여 내 컴퓨터에 server.js로 저장합니다.
    • 터미널에서 PUBLIC_DATA_SERVICE_KEY="내공공데이터키" node server.js 명령어로 서버를 띄운 후 브라우저에서 http://localhost:3000/api/weather로 접속하여 데이터가 정상 가공되는지 확인합니다.
  • Step 2 — 클라이언트 API 엔드포인트 전면 전환:
    • 모바일 앱 클라이언트 코드 전체를 검색(Ctrl+F 또는 Cmd+F)하여 공공데이터포털 주소(apis.data.go.kr)가 들어간 부분을 모두 제거합니다.
    • 대신 내 프록시 서버의 도메인(예: https://my-api-server.com/api/weather)으로 API 엔드포인트 Base URL을 전면 교체합니다.
  • Step 3 — 무료 클라우드 플랫폼에 배포하여 서비스 안정화:
    • AWS의 프리티어나 Vercel, Supabase Edge Functions 등 가볍고 무료 크레딧을 제공하는 클라우드 호스팅 영역에 이 프록시 코드를 배포합니다.
    • 환경변수에 공공데이터 인증키를 세팅하고 작동 상태를 모니터링하여 보안 100% 보장 및 일일 호출 쿼터 세이빙을 보장받으세요.

참고 자료