[quantHelper] 개발 - API3
9일차
■ 구현 내용
-
KisAccessToken 클래스 ```java @Component @Slf4j @Getter public class KisAccessToken { private String accessToken;
/**
- 한국 투자 증권 API에 접근하기 위한 Access Token를 받아와서 빈 내부에 저장
- webClient 빈, baseUrl, appKey, appSecretKey는 스프링이 주입해 준다.
-
@return String AccessToken */ public KisAccessToken(WebClient webClient, @Value(“${spring.kis-api.endpoint-url}”) String baseUrl, @Value(“${spring.kis-api.app-key}”)String appKey, @Value(“${spring.kis-api.app-secret-key}”) String appSecretKey) throws JsonProcessingException { Map<String, String> bodyMap = new HashMap<>(); bodyMap.put(“grant_type”, “client_credentials”); bodyMap.put(“appkey”, appKey); bodyMap.put(“appsecret”, appSecretKey);
String fullUrl = baseUrl + “/oauth2/tokenP”; // 호스트와 경로를 조합 Mono
monoAccess = webClient.post() .uri(fullUrl) // 전체 URL을 명시적으로 지정 .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(bodyMap)) .retrieve() .bodyToMono(String.class); String accessTokenJson = monoAccess.block(); ObjectMapper objectMapper = new ObjectMapper(); Map<String, String> AccessTokenMap = objectMapper.readValue(accessTokenJson, Map.class); accessToken = AccessTokenMap.get(“access_token”); log.info(accessToken); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<br>
2. StockPriceService, StockPriceRepository, StockPriceDTO, StockPrice 클래스
```java
// StockPrice 클래스
// 나머지 클래스는 생략
@Entity
@Getter
public class StockPrice {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "stock_price_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stockId", referencedColumnName = "stockId")
private Stock stock;
private LocalDate date;
private Long maxPriceDay;
private Long minPriceDay;
private Long openPrice;
private Long closePrice;
private Long tradingVolume;
@Builder
public StockPrice(LocalDate date, Long maxPriceDay, Long minPriceDay, Long openPrice, Long closePrice, Long tradingVolume) {
this.date = date;
this.maxPriceDay = maxPriceDay;
this.minPriceDay = minPriceDay;
this.openPrice = openPrice;
this.closePrice = closePrice;
this.tradingVolume = tradingVolume;
}
public StockPrice() {
}
public void changeStock(Stock stock){
this.stock = stock;
stock.getStockPriceList().add(this);
}
}
- GetStockPriceRequest 클래스
- 요청 파라미터를 바인딩하기 위한 클래스이다.
- 요청 파라미터의 이름과 클래스의 필드 명이 일치해야 한다.
- 반드시 기본 생성자가 존재해야 한다. ```java @Getter @Setter @NoArgsConstructor public class GetStockPriceRequest { @NotBlank(message = “{not_blank}”) @Schema( name = “stockName”, description = “stock name”, type = “String”, requiredMode = Schema.RequiredMode.REQUIRED, example = “SK하이닉스” ) private String stockName;
@NotBlank(message = “{not_blank}”) @Schema( name = “startDate”, description = “가격 조회 시작 일자”, type = “String”, requiredMode = Schema.RequiredMode.REQUIRED, example = “20240415” ) private String startDate;
@NotBlank(message = “{not_blank}”) @Schema( name = “endDate”, description = “가격 조회 종료 일자”, type = “String”, requiredMode = Schema.RequiredMode.REQUIRED, example = “20240415” ) private String endDate;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<br>
4. StockController의 stockPrice 메소드
- 주식 이름, 조회할 시작 날짜, 조회할 끝 날짜를 쿼리로 전달하면 해당 날짜에 맞는 주식의 가격을 DB에 저장하는 메소드이다.
- 원래는 시작~종료 기간의 모든 주식 가격 정보를 가져올 수 있어야 하지만, 이 메소드에서는 특정한 날의 가격 정보만 가져올 수 있게 제한했다.<br>예를 들어, 시작날짜가 20240415면, 종료날짜도 반드시 20240415를 입력해야 잘 작동한다.
```java
public ResponseEntity<StockPriceDTO> stockPrice(@ModelAttribute GetStockPriceRequest request) throws JsonProcessingException {
StockDTO byStockName = stockService.findByStockName(request.getStockName());
String stockCode = byStockName.getStockCode();
if (stockCode.length() < 6){
System.out.println("stock code should be longer than 6");
return ResponseEntity.badRequest().body(null);
}
Mono<String> stockPriceByCodeAndDate = kisService.getStockPriceByCodeAndDate(stockCode, request.getStartDate(), request.getEndDate()); // 주식 코드로 주식 정보 조회
String stockPriceResponse = stockPriceByCodeAndDate.block();
ObjectMapper objectMapper = new ObjectMapper();
List<Map<String,String>> stockPriceJson = (List<Map<String, String>>) objectMapper.readValue(stockPriceResponse, Map.class).get("output2");
Map<String, String> stockPriceMap = stockPriceJson.get(0);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
StockPriceDTO stockPriceDTO = StockPriceDTO.builder()
.date(LocalDate.parse(request.getStartDate(), formatter))
.stockId(stockService.findStockIdByStockCode(stockCode))
.maxPriceDay(Long.valueOf(stockPriceMap.get("stck_hgpr")))
.minPriceDay(Long.valueOf(stockPriceMap.get("stck_lwpr")))
.openPrice(Long.valueOf(stockPriceMap.get("stck_oprc")))
.closePrice(Long.valueOf(stockPriceMap.get("stck_clpr")))
.tradingVolume(Long.valueOf(stockPriceMap.get("acml_vol"))).
build();
stockPriceService.saveStockPrice(stockPriceDTO);
return ResponseEntity.ok().body(stockPriceDTO);
}
응답받는 JSON의 형태가 복잡했다. startDate와 endDate를 다르게 준 경우 이런식으로 응답이 왔다. 물론, 위의 프로젝트에서는 startDate와 endDate가 같아야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"output1": {
"prdy_vrss": "-2000",
"prdy_vrss_sign": "5",
"prdy_ctrt": "-2.39",
},
"output2": [
{
"stck_bsop_date": "20240402",
"stck_clpr": "85000",
"stck_oprc": "82900",
"stck_hgpr": "85000",
},
{
"stck_bsop_date": "20240401",
"stck_clpr": "82000",
"stck_oprc": "83200",
}
],
"rt_cd": "0",
"msg_cd": "MCA00000",
"msg1": "정상처리 되었습니다."
}
필요한 것은 output2 내부에 있는 정보였기 때문에, ObjectMapper를 사용해서 key:output2에 해당되는 value를 ArrayList<Map<String,String>> 형식으로 받아왔다.
■ 배운점
-
AccessToken 같이, 어플리케이션을 동작시키기 위해 반드시 필요한 요소는 스프링 빈으로 만들어서 실행과 동시에 저장하도록 하자.
- 스프링 빈에서 다른 빈을 의존하는 경우, 생성자의 매개변수를 통해 의존관계를 주입받아야 한다.
AccessToken에서 baseUrl, appKey, appSecretKey를 주입받아야 했다.
이 값들은 application-dev.yml에 저장되어 있기 때문에 스프링을 사용해서 의존관계를 주입받아야 했다.1 2 3 4 5 6
@Component public class KisAccessToken { @Value("${spring.kis-api.endpoint-url}") String baseUrl, @Value("${spring.kis-api.app-key}")String appKey, @Value("${spring.kis-api.app-secret-key}") String appSecretKey ...
처음에는 위와 같은 방식으로 필드에서 주입받으려 했다. 하지만, KisAccessToken 빈이 생성될 때는 “생성자”를 통해서 만들어진다. 생성자를 실행할 때 저 값들을 주입해주지 않고, 생성자 내부에서 저 필드들을 사용한다면 당연히
NullPointerException이 터질 것이다. 따라서, 생성자의 매개변수로 설정하여 의존관계를 주입받아야 한다.1 2 3 4 5 6 7 8
public class KisAccessToken { private String accessToken; public KisAccessToken(WebClient webClient, @Value("${spring.kis-api.endpoint-url}") String baseUrl, @Value("${spring.kis-api.app-key}")String appKey, @Value("${spring.kis-api.app-secret-key}") String appSecretKey) throws JsonProcessingException { ...
- Controller의 쿼리 파라미터, 즉 클라이언트와 연결되는 Web계층의 인터페이스는 직관적이어야 한다. StockCode를 알고 있는 사람보다는 StockName을 알고 있는 사람이 많다. SK하이닉스의 종목 번호가 000660인 것을 아는사람은 얼마 없을 것이다. 따라서, API를 호출할 때 StockCode가 필요하다고 해도, 클라이언트로 부터는 StockName을 받아야 한다. 서버 내부에서 StockName으로 StockCode를 조회해야 API를 호출하도록 해야 한다.
■ 오류
-
Error creating bean with name 'entityManagerFactory' defined in class path resource
해결: @GeneratedValue(strategy = GenerationType.IDENTITY) postgresql은 sequence 객체를 사용해서 id 자동 증가를 구현한다. 따라서, strategy = GenerationType.AUTO 말고, GenerationType.IDENTITY를 사용해줘야 한다. -
query did not return a unique result: 2 results were returned말 그대로 중복되는 값이 있을 때 발생하는 오류이다.findByStockName()메소드를 호출 했는데, 똑같은 이름이 2개 존재한다면 위와 같은 오류가 발생한다.
해결:findByStockName()의 리턴 타입을 ArrayList로 변경한다. 또는 DB에서 중복되는 값을 찾아서 하나를 삭제한다. -
ModelAttribute 바인딩 오류
해결: “파라미터 키 이름” == “바인딩하는 객체의 필드 이름” 예시) 파라미터가 stockName=SK&startDate=20240415로 넘어옴 그럼, 바인딩 할 객체에서 필드 이름은 stockName과 startDate로 존재해야함.
update 상황 발생
python을 활용해서 DB에 미리 stockId, stockCode, stockName, corpCode를 저장해두었다.
Stock 엔티티에 존재하는 필드는 추가로 몇가지 컬럼이 더 존재한다. 만약, StockController에서 StockName으로 증권사 서버에서 정보를 받아온다면?
1. stockName으로 stockCode를 받아옴
- 해당 stockCode로 API를 날림
- API 결과를 DTO에 담음.
- DTO를 Entity로 변환해서 DB에 저장
여기서 문제가 발생한다. 똑같은 주식 정보가 2개 생겨버리는 것이다. DTO를 새로 생성해서 저장하면, id 값만 다르고 나머지 값은 똑같은 레코드가 하나 더 생겨버린다. 따라서, stockId, stockCode, stockName, corpCode은 냅두고 나머지 컬럼만 추가하는 update 쿼리를 보낼 필요가 있다.
1 2 3 4 5 6 7 8 9 10 //StockRepository에 다음과 같은 메소드를 추가한다. @Modifying @Query("UPDATE Stock s SET s.stockCode = :stockCode, s.stockPriceIndex = :stockPriceIndex, s.price = :price, s.theme = :theme, s.status = :status, s.corpCode = :corpCode WHERE s.stockName = :stockName") int updateStockByStockName(@Param("stockName") String stockName, @Param("stockCode") String stockCode, @Param("stockPriceIndex") String stockPriceIndex, @Param("price") Long price, @Param("theme") String theme, @Param("status") String status, @Param("corpCode") String corpCode);위의 메소드를 사용하면, stockName으로 레코드를 하나 가져와서 거기에 새로운 값을 넣고 update 쿼리를 보낸다.
내일 할것:
stockService 버그 수정. ArrayList<> 2개 조회되는 버그