loading 오류 수정

This commit is contained in:
Macbook
2026-05-31 23:15:19 +09:00
parent 7f33640b86
commit 5adb22721e
6 changed files with 296 additions and 20 deletions
@@ -0,0 +1,154 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* 업비트 마켓 목록·전체 ticker — 서버 캐시·레이트리밋 후 클라이언트에 1회 응답.
* (브라우저가 /upbit-api 로 수십 회 나눠 호출하면 429 발생)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MarketQuoteService {
private final WebClient.Builder webClientBuilder;
private final ObjectMapper objectMapper;
@Value("${upbit.api.base-url:https://api.upbit.com}")
private String upbitBaseUrl;
@Value("${upbit.api.request-delay-ms:110}")
private long requestDelayMs;
private static final long MARKETS_TTL_MS = 3_600_000L;
private static final long TICKERS_TTL_MS = 12_000L;
private static final int TICKER_BATCH_SIZE = 100;
private final Object lock = new Object();
private volatile CacheEntry<List<JsonNode>> marketsCache;
private volatile CacheEntry<List<JsonNode>> allTickersCache;
private volatile List<String> krwMarketCodes = List.of();
private record CacheEntry<T>(T data, long cachedAtMs) {
boolean fresh(long ttlMs) {
return System.currentTimeMillis() - cachedAtMs < ttlMs;
}
}
/** GET /api/markets/all */
public List<JsonNode> getMarkets(boolean details) throws Exception {
synchronized (lock) {
if (marketsCache != null && marketsCache.fresh(MARKETS_TTL_MS)) {
return marketsCache.data();
}
String path = details ? "/v1/market/all?isDetails=true" : "/v1/market/all";
JsonNode arr = fetchJsonArray(path);
List<JsonNode> krw = StreamSupport.stream(arr.spliterator(), false)
.filter(n -> n.path("market").asText("").startsWith("KRW-"))
.toList();
krwMarketCodes = krw.stream().map(n -> n.path("market").asText()).toList();
marketsCache = new CacheEntry<>(krw, System.currentTimeMillis());
return krw;
}
}
/**
* GET /api/markets/tickers
* @param markets 비어 있으면 KRW 전 종목(캐시), 있으면 해당 종목만(소량은 즉시, 대량은 캐시 필터)
*/
public List<JsonNode> getTickers(List<String> markets) throws Exception {
if (markets == null || markets.isEmpty()) {
return getAllTickersCached();
}
Set<String> want = markets.stream()
.map(String::trim)
.filter(s -> s.startsWith("KRW-"))
.collect(Collectors.toCollection(LinkedHashSet::new));
if (want.isEmpty()) return List.of();
if (want.size() <= TICKER_BATCH_SIZE) {
return fetchTickersFromUpbit(new ArrayList<>(want));
}
List<JsonNode> all = getAllTickersCached();
return all.stream()
.filter(n -> want.contains(n.path("market").asText(n.path("code").asText(""))))
.toList();
}
private List<JsonNode> getAllTickersCached() throws Exception {
synchronized (lock) {
if (allTickersCache != null && allTickersCache.fresh(TICKERS_TTL_MS)) {
return allTickersCache.data();
}
if (krwMarketCodes.isEmpty()) {
getMarkets(true);
}
List<JsonNode> data = fetchTickersBatched(krwMarketCodes);
allTickersCache = new CacheEntry<>(data, System.currentTimeMillis());
log.info("[MarketQuote] KRW ticker 캐시 갱신: {} 종목", data.size());
return data;
}
}
private List<JsonNode> fetchTickersBatched(List<String> codes) throws Exception {
List<JsonNode> out = new ArrayList<>();
WebClient client = webClientBuilder.baseUrl(upbitBaseUrl).build();
for (int i = 0; i < codes.size(); i += TICKER_BATCH_SIZE) {
if (i > 0) Thread.sleep(requestDelayMs);
List<String> batch = codes.subList(i, Math.min(i + TICKER_BATCH_SIZE, codes.size()));
String joined = String.join(",", batch);
String json = client.get()
.uri("/v1/ticker?markets=" + joined)
.retrieve()
.bodyToMono(String.class)
.block();
if (json == null || json.isBlank()) continue;
JsonNode arr = objectMapper.readTree(json);
if (arr.isArray()) {
arr.forEach(out::add);
}
}
return out;
}
private List<JsonNode> fetchTickersFromUpbit(List<String> codes) throws Exception {
if (codes.isEmpty()) return List.of();
WebClient client = webClientBuilder.baseUrl(upbitBaseUrl).build();
String joined = String.join(",", codes);
String json = client.get()
.uri("/v1/ticker?markets=" + joined)
.retrieve()
.bodyToMono(String.class)
.block();
if (json == null || json.isBlank()) return List.of();
JsonNode arr = objectMapper.readTree(json);
if (!arr.isArray()) return List.of();
return StreamSupport.stream(arr.spliterator(), false).toList();
}
private JsonNode fetchJsonArray(String path) throws Exception {
WebClient client = webClientBuilder.baseUrl(upbitBaseUrl).build();
String json = client.get()
.uri(path)
.retrieve()
.bodyToMono(String.class)
.block();
if (json == null || json.isBlank()) {
return objectMapper.createArrayNode();
}
JsonNode node = objectMapper.readTree(json);
return node.isArray() ? node : objectMapper.createArrayNode();
}
}