한국투자증권 OpenAPI 미국 주식 차트 성능 개선하기 (2) ::cisxo

조정현·2025년 8월 19일

증권프로젝트

목록 보기
2/8
post-thumbnail

이번 신한투자증권 프로디지털아카데미 프로젝트에서 구축한 실시간 차트 구현에서 초당 요청 거래 제한을 회피하는 방법과 성능 개선을 이야기하고자 한다.

차트 뷰

  • 좌측을 감지하면 해당 날짜로 요청을 보내 이전 100개의 주가 정보를 받아온다.

문제점

  • 한국투자증권 api는 초당 거래 요청이 10건으로 제한되어 있다. 그렇기에 안정성을 위해서 캐싱과 요청 지연이 필요했다.

성능 개선 구조

큐 시스템

문제점

  • 한국투자증권 api의 경우 초당 10회를 넘어가는 경우 제한된다.
  • 위 gif와 같이 빠르게 요청을 보내는 경우 여러번의 요청이 가게 된다. 그렇게 되면 초당 거래 요청 제한으로 몇 분간 요청을 보낼 수 없다.

해결방안

  • 클라이언트에서 보내는 요청들을 서버에서 만든 api에서 요청을 감지하고 큐를 통해 대기시킨다. 요청을 초당 5건으로 제한하도록 순차 처리를 진행시켰다.
  • 해당 방식에도 문제점이 있었다.
    • 하나의 서버에서 큐 시스템으로 요청을 받다 보니 동시에 요청이 가는 경우에 요청이 밀릴 수 있다.
    • 그렇기에 조금이라도 성능을 높이는 방안이 필요했다.

코드

    // 한투 API 요청 함수
    const makeHantuRequest = async () => {
      const queryParams = new URLSearchParams(cacheParams).toString();

      const myGetToken = await getPeriodToken();
      if (!myGetToken) {
        throw new Error("토큰이 없습니다.");
      }
      const getToken = myGetToken.access_token;

      const response = await fetch(
        `https://openapivts.koreainvestment.com:29443/uapi/overseas-price/v1/quotations/dailyprice?${queryParams}`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${getToken}`,
            "content-type": "application/json",
            appKey: appKey,
            appSecret: appSecret,
            tr_id: "HHDFS76240000",
          },
        }
      );
      const data = await response.json();

      // 성공적인 응답인 경우에만 캐시에 저장 (BYMD가 있을 때만)
      if ((data.rt_cd === "0" || data.rt_cd === 0) && shouldCache) {
        saveCachedData("daily", cacheParams, data);
      }

      return data;
    };

    // 큐를 통해 요청 처리
    const data = await hantuQueue.addRequest(makeHantuRequest);


-> hantuQueue에 요청을 대기시킨다.


class HantuRequestQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
    this.lastRequestTime = 0;
    this.minInterval = 200; // 최소 200ms 간격 (초당 5회 제한으로 조정)
  }

  async addRequest(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) {
      return;
    }

    this.processing = true;

    while (this.queue.length > 0) {
      const { requestFn, resolve, reject } = this.queue.shift();

      try {
        // 요청 간격 조절
        const now = Date.now();
        const timeSinceLastRequest = now - this.lastRequestTime;

        if (timeSinceLastRequest < this.minInterval) {
          const delay = this.minInterval - timeSinceLastRequest;
          await new Promise((resolve) => setTimeout(resolve, delay));
        }

        this.lastRequestTime = Date.now();

        const result = await requestFn();
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

캐싱 전략

문제점

  • 하나의 백엔드 서버에서 다수의 클라이언트들이 요청을 보내는 경우 지연이 발생했다.

해결방안

  • 과거 데이터(BYMD or KEYB 존재) → 영구 캐시
  • 최신 데이터(BYMD or KEYB 없음) → 5초 임시 캐시로 반복 요청 최소화
  • 메모리 기반 캐시 사용 → 캐시 비용 문제를 최소화하고 조회 속도 향상
    KEYB -> 분봉 차트 시간 파라미터, BYMD -> 일봉 차트 시간 파라미터

코드

// 메모리 기반 캐시 객체
const chartCache = new Map();

// 실시간 데이터 임시 캐시 (5초)
const realtimeCache = new Map();

...

function generateCacheKey(type, params) {
  const sortedParams = Object.keys(params)
    .sort()
    .map((key) => `${key}:${params[key]}`)
    .join("|");
  return `chart_${type}_${sortedParams}`;
}

// 캐시에서 데이터 조회
function getCachedData(type, params) {
  const cacheKey = generateCacheKey(type, params);
  const cached = chartCache.get(cacheKey);

  if (cached) {
    console.log(`Returning cached ${type} chart data`);
    return cached;
  }

  return null;
}

// 캐시에 데이터 저장
function saveCachedData(type, params, data) {
  const cacheKey = generateCacheKey(type, params);

  chartCache.set(cacheKey, data);
  console.log(`Cached ${type} chart data`);
}

// 실시간 1분봉 데이터 캐시 조회
function getRealtimeCache(symbol, params) {
  const cacheKey = `realtime_${symbol}_${JSON.stringify(params)}`;
  const cached = realtimeCache.get(cacheKey);

  if (cached && Date.now() - cached.timestamp < 5000) {
    // 5초 유효
    console.log(`Returning realtime cache for ${symbol}`);
    return cached.data;
  }

  return null;
}

// 실시간 1분봉 데이터 캐시 저장
function saveRealtimeCache(symbol, params, data) {
  const cacheKey = `realtime_${symbol}_${JSON.stringify(params)}`;
  realtimeCache.set(cacheKey, {
    data,
    timestamp: Date.now(),
  });
  console.log(`Cached realtime data for ${symbol}`);
}

그럼에도 불구하고 문제점

  • 다수 클라이언트가 서로 다른 종목을 동시에 요청하면, 초기 요청 시 지연 발생
  • 캐시가 적용되어 이후 동일 요청은 빠르게 응답 가능
  • 1분봉 데이터는 100개 단위로 캐시되지만, 시간이 조금 지난 요청은(5초 임시 캐시) 다른 요청으로 처리되므로 캐시 히트율이 낮음
    • 100개 단위이기에 이전에 1~99번대 시간을 요청하게 되면 새로운 요청이기 때문에 캐시가 히트 될 확률이 낮음

즉, 사용자가 많아지는 경우 발생하는 문제점을 개선하기 위해서는 종목별 차트 데이터를 DB에 저장하는 방식이 사실상 유일하다고 생각합니다. 그렇기에 소규모 프로젝트 단위에서는 캐싱과 큐를 통해 해결하는 방법을 추천합니다.

0개의 댓글