현재 포트폴리오와 멤버 API 개발을 마치고, 본격적으로 주식 API를 개발에 들어갔다. 왜 이제 했냐면... (서류도 쓰고, 코딩테스트 준비도 하고... 요즘은 코딩테스트 언어도 자율이 아니라 자바로 딱 정해서 보는 곳이 많아졌다... 백수인데도 참 바쁜 것 같다.)
증권사 API는 한국투자증권의 API를 사용하게 되었다. 원래는 키움증권 API를 사용해보려고 했는데, 로그인 단계에서 보안프로그램 설치에서 막혔다.(보안 프로그램을 설치했는데도 계속 해당 페이지로 이동하고 다음으로 안 넘어갔다. 이유는 몰?루)
증권사 API를 사용하는 방법은 일단 해당 증권사의 계좌를 하나 만든 다음에 API 신청을 하면 된다. 한국투자증권의 경우 아래 링크
https://apiportal.koreainvestment.com/intro
KIS Developers
한국투자증권 Open API 포탈
apiportal.koreainvestment.com
그러면 아마 appkey와 appsecret 키를 발급해 줄 것이다. 아마 API 신청을 해본 사람들은 뭔지 알 것이다. 중요한 것은 이 키값들은 유출되어서는 안된다.
그리고 이 키 값을 application.yml에 저장해두면 된다.
kis:
base-url: "https://openapi.koreainvestment.com:9443"
auth-url: "/oauth2/tokenP"
app-key: "요기에 appkey"
app-secret: "요기에 appsecretkey"
참고로 저 우측 값에는 " "로 묶어줘야 한다(어떻게 아냐고? 나도 알고싶지 않았다...).
그럼 이제 본격적으로 개발하기에 앞서 토큰을 발급해야 한다. 토큰이란 시스템이 사용자의 신원을 확인하고, 특정 권한을 부여하는 임시 열쇠라고 생각하면 된다. 즉, "나 한국투자증권 API 신청한 사람이에요~~. 조회나 여러 권한을 요청할 건데, 나는 그 권한이 있는 사람이에요." 라는 것을 인증하는 열쇠같은 거다. JWT 토큰할 때, 그 토큰과 개념적으로 똑같은 거다.
package com.AISA.AISA.kisStock;
import com.AISA.AISA.global.exception.BusinessException;
import com.AISA.AISA.kisStock.dto.KisAuthRequest;
import com.AISA.AISA.kisStock.dto.KisAuthResponse;
import com.AISA.AISA.kisStock.exception.KisApiErrorCode;
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.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class KisAuthService {
@Value("${kis.base-url}")
private String baseUrl;
@Value("${kis.appkey}")
private String appkey;
@Value("${kis.appsecret}")
private String appsecret;
@Value("${kis.auth-url}")
private String authUrl;
private String accessToken;
private LocalDateTime tokenExpiresAt;
private final WebClient webClient;
public String getAccessToken() {
// 토큰이 없거나 만료되면 새로 발급
if (accessToken == null || tokenExpiresAt == null ||
LocalDateTime.now().isAfter(tokenExpiresAt)) {
log.info("토큰이 없거나 만들어지지 않았습니다. 토큰을 발급합니다.");
return refreshAccessToken();
}
log.debug("기존의 액세스 토큰을 재사용합니다.");
return accessToken;
}
private String refreshAccessToken() {
KisAuthRequest authRequest = KisAuthRequest.builder()
.grantType("client_credentials")
.appkey(appkey)
.appsecret(appsecret)
.build();
KisAuthResponse authResponse = webClient.post()
.uri(uriBuilder -> uriBuilder.path(authUrl).build())
.bodyValue(authRequest)
.retrieve()
.bodyToMono(KisAuthResponse.class)
.block();
if (authResponse != null && authResponse.getAccessToken() != null) {
this.accessToken = "Bearer " + authResponse.getAccessToken();
this.tokenExpiresAt = LocalDateTime.now().plusSeconds(authResponse.getExpiresIn() - 60);
log.info("KIS API 액세스 토큰이 발급되었습니다. 만기 시간은 : {}", this.tokenExpiresAt);
return this.accessToken;
} else {
log.error("KIS API 액세스 토큰 발급에 실패했습니다. 응답: {}", authResponse);
throw new BusinessException(KisApiErrorCode.TOKEN_ISSUANCE_FAILED);
}
}
}
나는 이렇게 코드를 작성해서 액세스 토큰을 발급받고 관리했다. 한국투자증권 API의 접근 토큰(access token)의 경우 유효기간이 24시간이기 때문에(1일 1회발급 원칙), 새로 발급받는 기능이 필요하다.
@Value("${kis.base-url}")
private String baseUrl;
@Value("${kis.appkey}")
private String appkey;
@Value("${kis.appsecret}")
private String appsecret;
@Value("${kis.auth-url}")
private String authUrl;
먼저 @Value 어노테이션을 사용해서 우리가 application.yml에 정의한 값들을 주입한다. 여기서 baseUrl은 KIS API의 기본 URL, appkey와 appsecret은 우리가 API 신청 시 발급받은 키, authUrl은 토큰 발급 엔드포인트 경로다.
private String accessToken;
private LocalDateTime tokenExpiresAt;
발급받은 액세스 토큰과 만료 시간 관리용 변수이다. 매번 API를 호출할 때마다 새로 발급받지 않고, 토큰이 만료되었을 때만 재발급받으면 된다.
private final WebClient webClient;
Spring WebFlux의 WebClient 를 사용했다. 주가 정보의 경우에는 우리 DB에 저장된 값을 불러오는 것이 아닌, 외부 서버에 요청해서 값을 불러오기 때문에 WebClient를 사용해야 한다. 아마 좀 예전부터 개발을 해오셨던 분들이라면 RestTemplate를 많이 사용하셨을텐데, 요즘은 WebClient가 거의 표준이라고 한다. GPT가 정리해준 표는 아래와 같다.
| RestTemplate VS WebClient | ||
| 구분 | RestTemplate | WebClient |
| 패러다임 | 동기(Synchronous, 블로킹) | 비동기(Asynchronous, 논블로킹) |
| 스레드 점유 | 요청당 스레드 1개 점유 | 이벤트 루프 기반 → I/O 효율적 |
| Spring 버전 | Spring 3~5 초반까지 주력 | Spring 5 이상부터 공식 권장 |
| 성능 | 단건 요청엔 무난 | 대량 호출, 병렬 처리에 매우 유리 |
| 코드 스타일 | 단순하고 직관적 | 리액티브 체인(Mono, Flux) 사용 |
| 대체 여부 | 공식적으로 “deprecated 예정” | RestTemplate의 후속 API |
| 주식 API 적합성 | 소규모 요청엔 충분 | 대량 종목 조회나 실시간 처리에 최적 |
RestTemplate의 경우 블로킹 방식으로 동작하기 때문에 요청 처리 동안 해당 스레드가 I/O를 기다리는 상태가 된다. 주식의 경우 실시간 WebSocekt + Rest의 혼합 구조이기 때문에 논블로킹 방식으로 동작해서 많은 요청을 효율적으로 동시에 처리할 수 있는 WebClient를 선택하는 것이 더 알맞은 선택이라고 판단했다.
package com.AISA.AISA.kisStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.baseUrl("https://openapi.koreainvestment.com:9443").build();
}
}
참고로 나는 WebClient를 Bean으로 등록해서 사용했다. Bean으로 등록하면 한 곳에서 관리할 수 있어 굉장히 편리하다. 또한, 여러 서비스에서 외부 API를 호출할 때, 매번 WebClient.builder().build()로 새로 만들 필요 없이 같은 Bean을 주입받아 사용할 수 있고, DI로 설정이 끝난 WebClient Bean을 바로 주입받을 수 있다.
public String getAccessToken() {
if (accessToken == null || tokenExpiresAt == null ||
LocalDateTime.now().isAfter(tokenExpiresAt)) {
log.info("토큰이 없거나 만들어지지 않았습니다. 토큰을 발급합니다.");
return refreshAccessToken();
}
log.debug("기존의 액세스 토큰을 재사용합니다.");
return accessToken;
}
토큰이 없거나 만료되면 refreshAccessToken 메소드를 호출해서 새로 발급받고, 그렇지 않으면 기존에 발급받았던 토큰을 재사용한다.
private String refreshAccessToken() {
KisAuthRequest authRequest = KisAuthRequest.builder()
.grantType("client_credentials")
.appkey(appkey)
.appsecret(appsecret)
.build();
KisAuthResponse authResponse = webClient.post()
.uri(uriBuilder -> uriBuilder.path(authUrl).build())
.bodyValue(authRequest)
.retrieve()
.bodyToMono(KisAuthResponse.class)
.block();
액세스 토큰 발급 부분이다. 한투 API에서 액세스 토큰을 새로 발급받는 기능을 한다.
KisAuthRequest 객체를 생성해서 토큰 요청 시 필요한 정보(grantType : OAuth에서 토큰 발급 시, 어떤 방식으로 권한을 얻을지, appkey와 appsecret은 우리의 key)를 담아 토큰 발급 요청용 데이터를 준비한다.
그 다음, WebClient POST 요청으로 KIS API에 토큰 발급을 요청한다.그리고 토큰 발급 응답이 올 때까지 기다리고, 최종적으로 authResponse 객체를 반환한다.
if (authResponse != null && authResponse.getAccessToken() != null) {
this.accessToken = "Bearer " + authResponse.getAccessToken();
this.tokenExpiresAt = LocalDateTime.now().plusSeconds(authResponse.getExpiresIn() - 60);
log.info("KIS API 액세스 토큰이 발급되었습니다. 만기 시간은 : {}", this.tokenExpiresAt);
return this.accessToken;
} else {
log.error("KIS API 액세스 토큰 발급에 실패했습니다. 응답: {}", authResponse);
throw new BusinessException(KisApiErrorCode.TOKEN_ISSUANCE_FAILED);
}
토큰 저장 및 만료 처리를 하는 부분이다. 발급에 성공하면 accessToken에 "Bearer "접두사를 붙여서 저장한다. 추후 API를 호출할 때, HTTP Authorization의 헤더에 넣어서 사용된다.
만료 시간은 토큰이 만료되기 1분 전에 재발급을 하게 설정해놓았다.
만약 발급에 실패하면 로그 기록 후, 이전에 만들어놓은 BusinessException을 사용해서 예외 처리하고 메시지를 출력한다.
본격적인 주식 API 개발은 다음 시간에 돌아오겠다. (근데 취준 중이여서 언제할지는 몰?루)
'개발 일지' 카테고리의 다른 글
| 주식과 지수 정보 조회 기능 (1) | 2025.11.23 |
|---|---|
| 한국투자증권 API 주식 조회 기능 (0) | 2025.11.21 |
| 회원 가입 중복 방지 중 발생한 DataIntegrityViolationException 예외 처리 (1) | 2025.11.05 |
| 2025-11-03 근황 (0) | 2025.11.03 |