Skip to content

Commit 933f471

Browse files
authored
Merge pull request #80 #79-나스닥 주식 저장 속도 리팩토링
- 비동기, 병렬, 벌크 연산을 통한 나스닥 주식 정보 저장
2 parents 3aef7b2 + 771b1b8 commit 933f471

File tree

3 files changed

+179
-102
lines changed

3 files changed

+179
-102
lines changed

src/main/java/naughty/tuzamate/domain/stock/controller/StockController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class StockController {
2929
private final KrxInquireService krxInquireService;
3030
private final KrxFinancialService krxFinancialService;
3131
private final StockInfoService stockInfoService;
32+
private final AsyncNasdaqStockFetcher asyncNasdaqStockFetcher;
3233

3334
@PostMapping("/post-stock-codes")
3435
@Operation(summary = "주식 코드 저장", description = "코스피, 코스닥, 나스닥 주식 코드를 저장합니다.")
@@ -48,7 +49,7 @@ public CustomResponse<?> saveStockCodes() {
4849
description = "나스닥 주식 한 개의 기본 정보를 가져옵니다. 주식 코드를 입력해야 합니다.")
4950
public NasdaqDto.NasdaqInfoDto getNasdaqStockInfo(@PathVariable("nasdaqStockCode") String nasdaqStockCode) {
5051

51-
return nasdaqService.getCurrentNasdaqInfo(nasdaqStockCode);
52+
return asyncNasdaqStockFetcher.getCurrentNasdaqInfo(nasdaqStockCode);
5253
}
5354

5455
@PostMapping("/nasdaq/all")
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package naughty.tuzamate.domain.stock.service;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.google.common.util.concurrent.RateLimiter;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import naughty.tuzamate.auth.hantu.service.HantuApiTokenService;
9+
import naughty.tuzamate.domain.stock.dto.StockInfoDto;
10+
import naughty.tuzamate.domain.stock.dto.nasdaq.NasdaqDto;
11+
import naughty.tuzamate.domain.stock.entity.NasdaqStockInfo;
12+
import naughty.tuzamate.domain.stock.strategy.FilterStrategy;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.http.*;
15+
import org.springframework.scheduling.annotation.Async;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.web.client.RestTemplate;
18+
import org.springframework.web.util.UriComponentsBuilder;
19+
20+
import java.util.Optional;
21+
import java.util.concurrent.CompletableFuture;
22+
import java.util.concurrent.TimeUnit;
23+
24+
@Service
25+
@RequiredArgsConstructor
26+
@Slf4j
27+
public class AsyncNasdaqStockFetcher {
28+
29+
private final RestTemplate restTemplate;
30+
private final ObjectMapper objectMapper;
31+
private final HantuApiTokenService hantuApiTokenService;
32+
private final StockInfoService stockInfoService;
33+
private final FilterStrategy filterStrategy;
34+
35+
private final RateLimiter rateLimiter = RateLimiter.create(15.0, 1, TimeUnit.SECONDS);
36+
37+
@Value("${tuza.api.APP_KEY}")
38+
private String appKey;
39+
40+
@Value("${tuza.api.APP_SECRET_KEY}")
41+
private String appSecret;
42+
43+
@Async("taskExecutor")
44+
public CompletableFuture<Optional<NasdaqStockInfo>> fetchStock(String stockCode) {
45+
46+
try {
47+
rateLimiter.acquire(3);
48+
49+
50+
HttpHeaders header = createHeaders();
51+
String url = "https://openapi.koreainvestment.com:9443/uapi/overseas-price/v1/quotations/price-detail";
52+
HttpEntity<?> httpEntity = new HttpEntity<>(header);
53+
54+
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
55+
.queryParam("AUTH", "")
56+
.queryParam("EXCD", "NAS")
57+
.queryParam("SYMB", stockCode);
58+
59+
ResponseEntity<String> response = restTemplate.exchange(
60+
builder.toUriString(),
61+
HttpMethod.GET,
62+
httpEntity,
63+
String.class
64+
);
65+
66+
NasdaqDto.NasdaqInfoDto currentNasdaqInfo = parsingCurrentNasdaqInfo(response.getBody(), stockCode);
67+
68+
// 한국 주식과 마찬가지로 필터링
69+
if (filterStrategy.shouldSkipNasdaq(currentNasdaqInfo)) {
70+
log.info("PER or PBR or EPS is zero: {}", stockCode);
71+
return CompletableFuture.completedFuture(Optional.empty());
72+
}
73+
74+
StockInfoDto.InfoDto currentStockInfo = stockInfoService.getStockInfo(stockCode, "512");
75+
76+
NasdaqStockInfo entity = currentNasdaqInfo.toEntity(currentNasdaqInfo, currentStockInfo);
77+
78+
return CompletableFuture.completedFuture(Optional.of(entity));
79+
80+
} catch (Exception e) {
81+
log.error("Nasdaq 정보 조회 중 오류 발생. StockCode: {}, Error: {}", stockCode, e.getMessage());
82+
return CompletableFuture.completedFuture(Optional.empty()); // 오류 발생 시 빈 Optional 반환
83+
}
84+
}
85+
86+
private HttpHeaders createHeaders() {
87+
HttpHeaders httpHeaders = new HttpHeaders();
88+
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
89+
String accessToken = hantuApiTokenService.getCurrentAccessToken();
90+
91+
httpHeaders.setBearerAuth(accessToken);
92+
httpHeaders.set("appkey", appKey);
93+
httpHeaders.set("appsecret", appSecret);
94+
httpHeaders.set("tr_id", "HHDFS76200200");
95+
httpHeaders.set("custtype", "P");
96+
97+
return httpHeaders;
98+
}
99+
100+
private NasdaqDto.NasdaqInfoDto parsingCurrentNasdaqInfo(String response, String stockCode) {
101+
102+
try {
103+
JsonNode rootNode = objectMapper.readTree(response);
104+
JsonNode node = rootNode.path("output");
105+
106+
NasdaqDto.NasdaqInfoDto outputDto = new NasdaqDto.NasdaqInfoDto();
107+
outputDto.setCode(stockCode);
108+
outputDto.setPerx(node.path("perx").asText());
109+
outputDto.setPbrx(node.path("pbrx").asText());
110+
outputDto.setEpsx(node.path("epsx").asText());
111+
outputDto.setE_icod(node.path("e_icod").asText());
112+
outputDto.setLast(node.path("last").asText());
113+
114+
return outputDto;
115+
} catch (Exception e) {
116+
throw new RuntimeException("Error parsing response: " + e.getMessage(), e);
117+
}
118+
}
119+
120+
// 현재 나스닥 주식 정보 조회 메소드
121+
// 없어도 되지만 컨트롤러의 getNasdaqStockInfo 메소드의 나스닥 단일 코드로 조회하는 테스트 용 메소드이다.
122+
public NasdaqDto.NasdaqInfoDto getCurrentNasdaqInfo(String stockCode) {
123+
124+
125+
HttpHeaders header = createHeaders();
126+
127+
String url = "https://openapi.koreainvestment.com:9443/uapi/overseas-price/v1/quotations/price-detail";
128+
129+
HttpEntity<?> httpEntity = new HttpEntity<>(header);
130+
131+
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
132+
.queryParam("AUTH", "")
133+
.queryParam("EXCD", "NAS")
134+
.queryParam("SYMB", stockCode);
135+
136+
ResponseEntity<String> response = restTemplate.exchange(
137+
builder.toUriString(),
138+
HttpMethod.GET,
139+
httpEntity,
140+
String.class
141+
);
142+
143+
return parsingCurrentNasdaqInfo(response.getBody(), stockCode);
144+
145+
}
146+
}

src/main/java/naughty/tuzamate/domain/stock/service/NasdaqService.java

Lines changed: 31 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -15,130 +15,60 @@
1515
import org.springframework.beans.factory.annotation.Value;
1616
import org.springframework.http.*;
1717
import org.springframework.stereotype.Service;
18+
import org.springframework.transaction.annotation.Transactional;
1819
import org.springframework.web.client.RestTemplate;
1920
import org.springframework.web.util.UriComponentsBuilder;
2021

2122
import java.util.List;
23+
import java.util.Optional;
24+
import java.util.concurrent.CompletableFuture;
25+
import java.util.stream.Collectors;
2226

2327
@Service
2428
@RequiredArgsConstructor
2529
@Slf4j
2630
public class NasdaqService {
2731

28-
private final RestTemplate restTemplate;
29-
private final ObjectMapper objectMapper;
3032
private final NasdaqCodeRepository nasdaqCodeRepository;
3133
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;
10935

36+
@Transactional
11037
public void saveNasdaqStocksInfo() {
111-
List<NasdaqStockCode> stockCodeList = nasdaqCodeRepository.findAll();
38+
log.info("미국 주식 정보 저장/업데이트 시작");
39+
long start = System.currentTimeMillis();
11240

41+
// 기존 데이터 삭제
11342
nasdaqStockInfoRepository.deleteAllInBatch();
11443

115-
for (NasdaqStockCode stockCode : stockCodeList) {
116-
try {
117-
118-
Thread.sleep(100);
44+
List<NasdaqStockCode> stockCodeList = nasdaqCodeRepository.findAll();
11945

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();
12250

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());
12752

128-
NasdaqStockInfo entity = currentNasdaqInfo.toEntity(currentNasdaqInfo, currentStockInfo);
53+
// 모든 CompletableFuture가 완료될 때까지 기다린다
54+
CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();
55+
log.info("모든 나스닥 주식 정보 요청 완료");
12956

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();
13163

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+
}
13369

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));
14172

142-
}
14373
}
14474
}

0 commit comments

Comments
 (0)