|
15 | 15 | import org.springframework.beans.factory.annotation.Value; |
16 | 16 | import org.springframework.http.*; |
17 | 17 | import org.springframework.stereotype.Service; |
| 18 | +import org.springframework.transaction.annotation.Transactional; |
18 | 19 | import org.springframework.web.client.RestTemplate; |
19 | 20 | import org.springframework.web.util.UriComponentsBuilder; |
20 | 21 |
|
21 | 22 | import java.util.List; |
| 23 | +import java.util.Optional; |
| 24 | +import java.util.concurrent.CompletableFuture; |
| 25 | +import java.util.stream.Collectors; |
22 | 26 |
|
23 | 27 | @Service |
24 | 28 | @RequiredArgsConstructor |
25 | 29 | @Slf4j |
26 | 30 | public class NasdaqService { |
27 | 31 |
|
28 | | - private final RestTemplate restTemplate; |
29 | | - private final ObjectMapper objectMapper; |
30 | 32 | private final NasdaqCodeRepository nasdaqCodeRepository; |
31 | 33 | private final NasdaqStockInfoRepository nasdaqStockInfoRepository; |
32 | | - private final HantuApiTokenService hantuApiTokenService; |
33 | | - private final StockInfoService stockInfoService; |
34 | | - private final FilterStrategy filterStrategy; |
35 | | - |
36 | | - @Value("${tuza.api.APP_KEY}") |
37 | | - private String appKey; |
38 | | - |
39 | | - @Value("${tuza.api.APP_SECRET_KEY}") |
40 | | - private String appSecret; |
41 | | - |
42 | | - private String accessToken; |
43 | | - |
44 | | - private HttpHeaders createHeaders() { |
45 | | - HttpHeaders httpHeaders = new HttpHeaders(); |
46 | | - httpHeaders.setContentType(MediaType.APPLICATION_JSON); |
47 | | - accessToken = hantuApiTokenService.getCurrentAccessToken(); |
48 | | - |
49 | | - httpHeaders.setBearerAuth(accessToken); |
50 | | - httpHeaders.set("appkey", appKey); |
51 | | - httpHeaders.set("appsecret", appSecret); |
52 | | - httpHeaders.set("tr_id", "HHDFS76200200"); |
53 | | - httpHeaders.set("custtype", "P"); |
54 | | - |
55 | | - return httpHeaders; |
56 | | - } |
57 | | - |
58 | | - private NasdaqDto.NasdaqInfoDto parsingCurrentNasdaqInfo(String response, String stockCode) { |
59 | | - NasdaqDto.NasdaqInfoDto data = new NasdaqDto.NasdaqInfoDto(); |
60 | | - |
61 | | - try { |
62 | | - JsonNode rootNode = objectMapper.readTree(response); |
63 | | - JsonNode node = rootNode.path("output"); |
64 | | - |
65 | | - if (node != null) { |
66 | | - NasdaqDto.NasdaqInfoDto outputDto = new NasdaqDto.NasdaqInfoDto(); |
67 | | - |
68 | | - outputDto.setCode(stockCode); |
69 | | - outputDto.setPerx(node.path("perx").asText()); |
70 | | - outputDto.setPbrx(node.path("pbrx").asText()); |
71 | | - outputDto.setEpsx(node.path("epsx").asText()); |
72 | | - outputDto.setE_icod(node.path("e_icod").asText()); |
73 | | - outputDto.setLast(node.path("last").asText()); |
74 | | - |
75 | | - |
76 | | - data = outputDto; |
77 | | - } |
78 | | - return data; |
79 | | - } catch (Exception e) { |
80 | | - log.error("error is : {}", e.getMessage()); |
81 | | - throw new RuntimeException(); |
82 | | - } |
83 | | - } |
84 | | - |
85 | | - public NasdaqDto.NasdaqInfoDto getCurrentNasdaqInfo(String stockCode) { |
86 | | - |
87 | | - |
88 | | - HttpHeaders header = createHeaders(); |
89 | | - |
90 | | - String url = "https://openapi.koreainvestment.com:9443/uapi/overseas-price/v1/quotations/price-detail"; |
91 | | - |
92 | | - HttpEntity<?> httpEntity = new HttpEntity<>(header); |
93 | | - |
94 | | - UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url) |
95 | | - .queryParam("AUTH", "") |
96 | | - .queryParam("EXCD", "NAS") |
97 | | - .queryParam("SYMB", stockCode); |
98 | | - |
99 | | - ResponseEntity<String> response = restTemplate.exchange( |
100 | | - builder.toUriString(), |
101 | | - HttpMethod.GET, |
102 | | - httpEntity, |
103 | | - String.class |
104 | | - ); |
105 | | - |
106 | | - return parsingCurrentNasdaqInfo(response.getBody(), stockCode); |
107 | | - |
108 | | - } |
| 34 | + private final AsyncNasdaqStockFetcher asyncNasdaqStockFetcher; |
109 | 35 |
|
| 36 | + @Transactional |
110 | 37 | public void saveNasdaqStocksInfo() { |
111 | | - List<NasdaqStockCode> stockCodeList = nasdaqCodeRepository.findAll(); |
| 38 | + log.info("미국 주식 정보 저장/업데이트 시작"); |
| 39 | + long start = System.currentTimeMillis(); |
112 | 40 |
|
| 41 | + // 기존 데이터 삭제 |
113 | 42 | nasdaqStockInfoRepository.deleteAllInBatch(); |
114 | 43 |
|
115 | | - for (NasdaqStockCode stockCode : stockCodeList) { |
116 | | - try { |
117 | | - |
118 | | - Thread.sleep(100); |
| 44 | + List<NasdaqStockCode> stockCodeList = nasdaqCodeRepository.findAll(); |
119 | 45 |
|
120 | | - NasdaqDto.NasdaqInfoDto currentNasdaqInfo = getCurrentNasdaqInfo(stockCode.getCode()); |
121 | | - StockInfoDto.InfoDto currentStockInfo = stockInfoService.getStockInfo(stockCode.getCode(), "512"); |
| 46 | + // 비동기 API 호출 |
| 47 | + List<CompletableFuture<Optional<NasdaqStockInfo>>> completableFutures = stockCodeList.stream() |
| 48 | + .map(stockCode -> asyncNasdaqStockFetcher.fetchStock(stockCode.getCode())) |
| 49 | + .toList(); |
122 | 50 |
|
123 | | - if (filterStrategy.shouldSkipNasdaq(currentNasdaqInfo)) { |
124 | | - log.info("PER or PBR or EPS is zero: {}", stockCode.getCode()); |
125 | | - continue; // 필터 전략에 의해 스킵된 경우 다음 주식 코드로 넘어감 |
126 | | - } |
| 51 | + log.info("{}개의 나스닥 주식 정보 요청 시작", stockCodeList.size()); |
127 | 52 |
|
128 | | - NasdaqStockInfo entity = currentNasdaqInfo.toEntity(currentNasdaqInfo, currentStockInfo); |
| 53 | + // 모든 CompletableFuture가 완료될 때까지 기다린다 |
| 54 | + CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join(); |
| 55 | + log.info("모든 나스닥 주식 정보 요청 완료"); |
129 | 56 |
|
130 | | - nasdaqStockInfoRepository.save(entity); |
| 57 | + // 결과를 Optional<NasdaqStockInfo>로 변환하여 리스트로 수집한다 |
| 58 | + List<NasdaqStockInfo> stockInfoList = completableFutures.stream() |
| 59 | + .map(CompletableFuture::join) |
| 60 | + .filter(Optional::isPresent) |
| 61 | + .map(Optional::get) |
| 62 | + .toList(); |
131 | 63 |
|
132 | | - log.info("Saved stocks is : {}", stockCode.getCode()); |
| 64 | + // 데이터 DB에 일괄 저장 |
| 65 | + if (!stockInfoList.isEmpty()) { |
| 66 | + log.info("{} 개의 나스닥 주식 정보를 DB에 저장 시작", stockInfoList.size()); |
| 67 | + nasdaqStockInfoRepository.saveAll(stockInfoList); |
| 68 | + } |
133 | 69 |
|
134 | | - } catch (InterruptedException e) { |
135 | | - Thread.currentThread().interrupt(); // 현재 스레드 인터럽트 상태 복구 |
136 | | - log.info("Thread Interrupted : {}", e.getMessage()); |
137 | | - break; |
138 | | - } catch (Exception e) { |
139 | | - log.info("Error stock code is {} : {} and pass!", stockCode.getCode(), e.getMessage()); |
140 | | - } |
| 70 | + long end = System.currentTimeMillis(); |
| 71 | + log.info("나스닥 주식 정보 저장/업데이트 완료, 소요 시간: {} ms", (end - start)); |
141 | 72 |
|
142 | | - } |
143 | 73 | } |
144 | 74 | } |
0 commit comments