1111import java .util .Collections ;
1212import java .util .List ;
1313import java .util .Map ;
14+ import java .util .concurrent .ConcurrentHashMap ;
1415
1516import org .slf4j .Logger ;
1617import org .slf4j .LoggerFactory ;
2930import lombok .RequiredArgsConstructor ;
3031import org .springframework .cache .Cache ;
3132import org .springframework .cache .CacheManager ;
32- import org .springframework .cache .annotation .Cacheable ;
3333import reactor .core .publisher .Mono ;
3434
3535@ Service
@@ -43,6 +43,7 @@ public class SecurityService {
4343 private final ObjectMapper objectMapper ;
4444 private final CacheManager cacheManager ;
4545 private static final String STOCK_PRICE_CACHE = "stockPrice" ;
46+ private final Map <String , Mono <StockInfoResponse >> inFlightPriceRequests = new ConcurrentHashMap <>();
4647
4748 /**
4849 * 국내, 해외 주식 정보 조회
@@ -152,9 +153,7 @@ private Mono<StockInfoResponse> parseFStockInfoKorea2(String response, Integer i
152153
153154 /**
154155 * 국내, 해외 주식 정보 조회
155- * Redis 캐시: 30초간 동일한 결과 반환 (실시간 가격 변동 고려)
156156 */
157- @ Cacheable (value = "stockPrice" , key = "#symbol + '_' + #exchangenum.name()" , unless = "#result == null" )
158157 public Mono <StockInfoResponse > getSecurityStockInfoKorea (Integer id , String symbolName , String securityName , String symbol , EXCHANGENUM exchangenum , COUNTRY country ) {
159158 if (country == COUNTRY .KOREA ) {
160159 return webClient .get ()
@@ -251,9 +250,14 @@ private Mono<StockInfoResponse> parseFStockInfoKorea(String response, Integer id
251250 return Mono .error (new UnsupportedOperationException ("주가 정보가 없습니다 (output node missing, symbol: " + symbol + ")" ));
252251 }
253252
254- log .debug ("Successfully parsed StockInfo (inquire-price) - symbol: {}, price: {}, yesterdayPrice: {}" ,
255- symbol , stockInfoResponse .getPrice (), stockInfoResponse .getYesterdayPrice ());
256- return Mono .just (stockInfoResponse );
253+ StockInfoResponse normalized = normalizePriceInfo (stockInfoResponse );
254+ if (normalized == null ) {
255+ return Mono .error (new UnsupportedOperationException ("주가 정보가 없습니다 (symbol: " + symbol + ")" ));
256+ }
257+
258+ log .debug ("Successfully parsed StockInfo (inquire-price) - symbol: {}, price: {}, yesterdayPrice: {}" ,
259+ symbol , normalized .getPrice (), normalized .getYesterdayPrice ());
260+ return Mono .just (normalized );
257261 } catch (Exception e ) {
258262 log .error ("Failed to parse StockInfo response - symbol: {}, response: {}, error: {}" ,
259263 symbol , response , e .getMessage (), e );
@@ -290,6 +294,10 @@ private Mono<StockInfoResponse> parseFStockInfoOversea(String response, Integer
290294 if (rate != null && diff != null ) {
291295 // 해외 diff는 절대값으로 오는 경우가 많아 부호를 rate 기준으로 정규화
292296 stockInfoResponse .setPriceDiff (rate < 0 ? -Math .abs (diff ) : Math .abs (diff ));
297+ } else if (diff != null ) {
298+ stockInfoResponse .setPriceDiff (diff );
299+ }
300+ if (rate != null ) {
293301 stockInfoResponse .setPriceDiffPerCent (rate );
294302 }
295303
@@ -302,7 +310,12 @@ private Mono<StockInfoResponse> parseFStockInfoOversea(String response, Integer
302310 return Mono .error (new UnsupportedOperationException ("해외 종목 정보가 없습니다 (output node missing, symbol: " + symbol + ")" ));
303311 }
304312
305- return Mono .just (stockInfoResponse );
313+ StockInfoResponse normalized = normalizePriceInfo (stockInfoResponse );
314+ if (normalized == null ) {
315+ return Mono .error (new UnsupportedOperationException ("해외 종목 주가 정보가 없습니다 (symbol: " + symbol + ")" ));
316+ }
317+
318+ return Mono .just (normalized );
306319 } catch (Exception e ) {
307320 return Mono .error (new UnsupportedOperationException ("해외 종목 정보가 없습니다" ));
308321 }
@@ -336,6 +349,60 @@ private Double parseFiniteDouble(JsonNode node, String symbol, String fieldName)
336349 }
337350 }
338351
352+ private StockInfoResponse normalizePriceInfo (StockInfoResponse response ) {
353+ if (response == null ) {
354+ return null ;
355+ }
356+
357+ Double price = toPositiveFiniteOrNull (response .getPrice ());
358+ Double yesterdayPrice = toPositiveFiniteOrNull (response .getYesterdayPrice ());
359+ Double priceDiff = toFiniteOrNull (response .getPriceDiff ());
360+ Double priceDiffPercent = toFiniteOrNull (response .getPriceDiffPerCent ());
361+
362+ priceDiff = derivePriceDiff (priceDiff , price , yesterdayPrice );
363+ priceDiffPercent = derivePriceDiffPercent (priceDiffPercent , priceDiff , yesterdayPrice );
364+
365+ response .setPrice (price );
366+ response .setYesterdayPrice (yesterdayPrice );
367+ response .setPriceDiff (priceDiff );
368+ response .setPriceDiffPerCent (priceDiffPercent );
369+
370+ if (price == null && yesterdayPrice == null ) {
371+ return null ;
372+ }
373+ return response ;
374+ }
375+
376+ private Double derivePriceDiff (Double currentDiff , Double price , Double yesterdayPrice ) {
377+ if (currentDiff != null ) {
378+ return currentDiff ;
379+ }
380+ if (!isPositiveFinite (price ) || !isPositiveFinite (yesterdayPrice )) {
381+ return null ;
382+ }
383+ double derived = price - yesterdayPrice ;
384+ return Double .isFinite (derived ) ? derived : null ;
385+ }
386+
387+ private Double derivePriceDiffPercent (Double currentDiffPercent , Double priceDiff , Double yesterdayPrice ) {
388+ if (currentDiffPercent != null ) {
389+ return currentDiffPercent ;
390+ }
391+ if (!isPositiveFinite (yesterdayPrice ) || priceDiff == null ) {
392+ return null ;
393+ }
394+ double derived = (priceDiff / yesterdayPrice ) * 100.0 ;
395+ return Double .isFinite (derived ) ? derived : null ;
396+ }
397+
398+ private Double toFiniteOrNull (Double value ) {
399+ return value != null && Double .isFinite (value ) ? value : null ;
400+ }
401+
402+ private Double toPositiveFiniteOrNull (Double value ) {
403+ return isPositiveFinite (value ) ? value : null ;
404+ }
405+
339406 private boolean isPositiveFinite (Double value ) {
340407 return value != null && Double .isFinite (value ) && value > 0 ;
341408 }
@@ -1048,19 +1115,25 @@ private Mono<List<PriceInfo>> parseFStockChartPriceOverseas(String response) {
10481115 }
10491116
10501117 public Mono <StockInfoResponse > getRealTimeStockPrice (Stock stock ) {
1118+ if (stock == null || stock .getSymbol () == null || stock .getExchangeNum () == null ) {
1119+ return Mono .error (new IllegalArgumentException ("유효하지 않은 종목 정보입니다." ));
1120+ }
1121+
10511122 StockInfoResponse cached = getCachedRealTimeStockPrice (stock );
10521123 if (cached != null ) {
10531124 return Mono .just (cached );
10541125 }
10551126
1056- return getSecurityStockInfoKorea (
1057- stock .getId (),
1058- stock .getSymbolName (),
1059- stock .getSecurityName (),
1060- stock .getSymbol (),
1061- stock .getExchangeNum (),
1062- getCountryFromExchangeNum (stock .getExchangeNum ())
1063- ).doOnNext (response -> putStockPriceCache (stock , response ));
1127+ String cacheKey = buildStockPriceCacheKey (stock );
1128+ if (cacheKey == null ) {
1129+ return requestAndCacheRealTimeStockPrice (stock );
1130+ }
1131+
1132+ return inFlightPriceRequests .computeIfAbsent (cacheKey , key ->
1133+ requestAndCacheRealTimeStockPrice (stock )
1134+ .doFinally (signalType -> inFlightPriceRequests .remove (key ))
1135+ .cache ()
1136+ );
10641137 }
10651138
10661139 public StockInfoResponse getCachedRealTimeStockPrice (Stock stock ) {
@@ -1081,16 +1154,30 @@ public StockInfoResponse getCachedRealTimeStockPrice(Stock stock) {
10811154
10821155 Object value = wrapper .get ();
10831156 if (value instanceof StockInfoResponse ) {
1084- return ( StockInfoResponse ) value ;
1157+ return normalizePriceInfo (( StockInfoResponse ) value ) ;
10851158 }
10861159 if (value instanceof Map ) {
1087- return objectMapper .convertValue (value , StockInfoResponse .class );
1160+ StockInfoResponse converted = objectMapper .convertValue (value , StockInfoResponse .class );
1161+ return normalizePriceInfo (converted );
10881162 }
10891163 return null ;
10901164 }
10911165
1166+ private Mono <StockInfoResponse > requestAndCacheRealTimeStockPrice (Stock stock ) {
1167+ return getSecurityStockInfoKorea (
1168+ stock .getId (),
1169+ stock .getSymbolName (),
1170+ stock .getSecurityName (),
1171+ stock .getSymbol (),
1172+ stock .getExchangeNum (),
1173+ getCountryFromExchangeNum (stock .getExchangeNum ())
1174+ ).map (this ::normalizePriceInfo )
1175+ .doOnNext (response -> putStockPriceCache (stock , response ));
1176+ }
1177+
10921178 private void putStockPriceCache (Stock stock , StockInfoResponse response ) {
1093- if (response == null || response .getPrice () == null || response .getPrice () <= 0 ) {
1179+ StockInfoResponse normalized = normalizePriceInfo (response );
1180+ if (normalized == null || !isPositiveFinite (normalized .getPrice ())) {
10941181 return ;
10951182 }
10961183
@@ -1101,7 +1188,7 @@ private void putStockPriceCache(Stock stock, StockInfoResponse response) {
11011188
11021189 String cacheKey = buildStockPriceCacheKey (stock );
11031190 if (cacheKey != null ) {
1104- cache .put (cacheKey , response );
1191+ cache .put (cacheKey , normalized );
11051192 }
11061193 }
11071194
0 commit comments