1. 시스템 개요 & 목적 (System Overview)
본 시스템은 업비트(Upbit) 거래소의 시세 데이터를 백엔드(Spring Boot)가 중앙 집중식으로 관리·가공하고, 프론트엔드(다중 차트 UI) 및 FCM 푸시 알림 서버로 정제된 데이터를 분배하는 고성능 금융 트레이딩 데이터 디스패처 인프라 구축을 목적으로 한다.

핵심 설계 원칙:

프론트엔드는 데이터 가공 연산을 하지 않으며 오직 렌더링에만 집중한다.

백엔드 인메모리는 실시간 전략 감시에 필요한 최신 데이터(300개 제한)만 유지하여 RAM을 방어한다.

인메모리 범위를 벗어나는 과거 데이터는 온디맨드(On-Demand) REST API를 통해 페이징 중계한다.

2. 데이터 흐름 아키텍처 (Data Flow Pipeline)
[업비트 서버 (REST / WebSocket)]
       │
       │ (1. 고정 관심종목 20개 + 2. 동적 차트 오픈 종목 최대 8개)
       ▼
[Spring Boot 백엔드 서버]
   ├── ① UpbitWebSocketClient (수신 및 가상 스레드 분배)
   ├── ② Ta4jStorage (인메모리 Map<String, BarSeries> 관리, 300개 스케일 제한)
   ├── ③ HistoricalDataService (과거 캔들 페이징 중계기)
   ├── ④ LiveStrategyScheduler (3초 주기 고정 관심종목 조건 감시 엔진)
   └── ⑤ TradingWebSocketBroker (STOMP 기반 실시간 지표 분배기)
       │
       ├─ (STOMP Pub/Sub 실시간 갱신) ───────> [Frontend (Lightweight Charts)]
       └─ (FCM Open API / 비동기 스레드) ──> [FCM Server] ──> [안드로이드/웹 푸시]
3. 백엔드 상세 설계 (Backend Components)
3.1 인메모리 저장소 (Ta4jStorage)
구조: 멀티스레드 환경에서 안전한 ConcurrentHashMap 기반의 2중 맵 구조.

Map<String, BarSeries Map<String,>> storage ➔ Key 1: 종목코드(예: KRW-BTC), Key 2: 봉타입(예: 1m, 5m, 15m, 1h, 1d)

메모리 슬라이딩 윈도우 제한:

모든 BarSeries 생성 시 series.setMaximumBarCount(300);를 반드시 설정한다.

301번째 실시간 캔들이 들어오면 가장 오래된 1번째 캔들은 메모리에서 자동 소멸해야 한다.

3.2 캔들 조립기 (BarBuilder) & 멀티 시간봉 처리
업비트 웹소켓으로 수신한 초 단위 실시간 틱 데이터(Trade 또는 Ticker)를 바탕으로 분봉을 조립한다.

1분봉 조립 규칙:

수신한 틱의 타임스탬프가 현재 메모리 상 마지막 Bar의 시간 범위 내에 있다면: series.addPrice(price, volume) 호출하여 현재가(종가), 고가, 저가, 거래량을 실시간 갱신한다.

분이 변경되어 다음 시간 범위로 넘어가면: series.addBar(newBar)를 호출하여 새로운 캔들을 생성한다.

상위 시간봉(5m, 15m, 1h) 전파 규칙:

1분봉이 최종 마감(다음 분 00초 진입)되는 시점에, 해당 1분봉의 완결된 OHLCV 데이터를 상위 시간봉 BarSeries에 순차적으로 누적 산입(addPrice 또는 addBar)한다.

3.3 동적 구독 관리자 (DynamicSubscriptionManager)
고정 관심종목(20개) 외에 프론트엔드 유저가 멀티차트 모드 등으로 신규 종목을 조회할 때 작동하는 동적 확장 아키텍처를 구현한다.

구독 인터셉터 가동: 프론트엔드가 특정 채널(예: /sub/charts/KRW-SAND/1m)을 구독하는 이벤트를 감지한다.

초기화 (Warm-up): 메모리에 해당 종목의 BarSeries가 없다면 즉시 업비트 REST API를 호출하여 최근 200개의 캔들을 받아와 Ta4j 메모리를 생성한다.

웹소켓 채널 확장: 백엔드의 UpbitWebSocketClient 소켓 커넥션을 유지한 채, 구독 코드 목록(codes)에 KRW-SAND를 동적으로 추가하여 재요청 패킷을 보낸다.

자원 수거 (GC): 특정 동적 종목의 STOMP 구독자 수가 0이 되면, 업비트 웹소켓 구독 목록에서 제거하고 Ta4jStorage에서 삭제하여 메모리를 방어한다.

3.4 과거 데이터 페이징 중계기 (HistoricalDataService)
프론트엔드 차트 무한 과거 스크롤을 지원하기 위한 온디맨드 API 서브시스템이다.

동작 매커니즘:

프론트엔드가 요청한 기준 시간(to)이 현재 백엔드 Ta4j 인메모리(최근 300개) 범위 내에 있다면 ➔ 메모리 데이터를 슬라이싱하여 즉시 반환한다.

인메모리 범위를 벗어난 먼 과거의 데이터라면 ➔ 업비트 REST API(/v1/candles/minutes/{기반})를 호출한다. 이때 파라미터로 받은 to와 count=200을 그대로 업비트에 포워딩한다.

수신된 데이터를 가공하여 차트에 전달할 때, Ta4j 인메모리에 임시 임포트하여 해당 구간의 보조지표(RSI 등)를 계산한 뒤 값에 포함하여 반환한다.

3.5 전략 감시 및 비동기 푸시 엔진 (LiveStrategyScheduler)
라운드 로빈 스케줄러: 3초 주기로 고정 관심종목 20개의 최신 인메모리 지표를 스캔한다. (틱 수신 스레드와 철저히 분리)

FCM 발송 격리: 조건 만족 시 메인 트레이딩 루프가 멈추지 않도록 @Async("fcmExecutor") 처리된 전용 스레드 풀에서 구글 FCM 오픈 API를 비동기 호출한다.

4. 프론트엔드 상세 설계 (Frontend Specs)
4.1 다중 차트 독립 파이프라인 (최대 8개)
화면 전반에 최대 8개의 Lightweight Charts 인스턴스를 동적으로 생성 및 관리할 수 있는 구조를 가진다.

각 차트 인스턴스는 고유한 Context 또는 상태 공간을 가지며, 백엔드로부터 온 메시지를 분배받는 독립적인 STOMP 구독 채널을 유지한다.

4.2 과거 차트 무한 스크롤 및 레이지 로딩 (Lazy Loading)
이벤트 트리거: chart.timeScale().subscribeVisibleLogicalRangeChange() 핸들러를 바인딩한다.

임계점 판단: 유저가 차트를 왼쪽으로 스크롤하여 현재 화면에 보이는 최좌측 캔들의 인덱스가 남은 데이터 전체의 끝점 부근(logicalRange.from < 5)에 도달하면 역사 데이터 요청을 트리거한다.

API 호출: 현재 차트의 가장 오래된 캔들의 타임스탬프(to)를 파라미터로 얹어 백엔드 역사 데이터 API(GET /api/candles/history)를 호출한다.

데이터 접합 (Prepend): 서버로부터 받아온 과거 캔들 200개 배열을 차트 기존 데이터의 맨 앞(머리)에 이어 붙여 차트가 끊김 없이 왼쪽으로 확장되도록 구현한다.

중복 방지 락(Lock): API 통신이 진행 중인 동안에는 isLoading = true 상태를 유지하여 스크롤 오버랩에 의한 동일 타임스탬프 중복 요청을 원천 차단한다.

5. 데이터 인터페이스 포맷 (Data Interface Formats)
5.1 [REST API] 과거 데이터 조회 및 차트 초기화 용 데이터
Endpoint: GET /api/candles/history

Query Params: market=KRW-BTC, type=1m, to=2026-05-20T10:17:00Z, count=200

Response DTO (JSON):

JSON
[
  {
    "time": 1779229200, 
    "open": 92100000.0,
    "high": 92150000.0,
    "low": 92050000.0,
    "close": 92080000.0,
    "volume": 4.215,
    "rsi": 42.15
  }
]
5.2 [STOMP WebSocket] 실시간 경량 분배 데이터
Topic: /sub/charts/{market}/{type} (예: /sub/charts/KRW-BTC/1m)

Payload DTO (JSON):

JSON
{
  "t": 1779229260,
  "o": 92080000.0,
  "h": 92120000.0,
  "l": 92010000.0,
  "c": 92050000.0,
  "v": 1.845,
  "rsi": 38.40
}