Skip to content

Commit

Permalink
[fix] 액세스 토큰 재발급 문제 해결 (#446)
Browse files Browse the repository at this point in the history
* test: 액세스 토큰 만료시 예외케이스 추가

* fix: Flux 방식에서 단순 반복문으로 변경

- 병렬 과정에서 액세스 토큰 만료시 무한 시도 문제 해결을 위해서 단순화

* test: 테스트 수정

* test: todo 주석 추가

* feat: kisException 객체로 매핑하여 반환하도록 변경

* test: 테스트 실패 해결

* test: 초당 거래 건수 초과 에러 응답하는 예외 케이스 추가

* feat: returnCode, messageCode 필드 추가

* feat: KisException의 서브 클래스 추가

* feat: onErrorResume 및 retryWhen 연산 수정

- 액세스 토큰이 만료되는 경우에는 Mono.empty() 반환
- retryWhen 연산에서 요청건수 초과인 경우에만 재시도하도록 함

* feat: KisClient의 에러 핸들 처리 메서드(handleError) 수정

- 한국투자증권서버로부터 에러 응답을 수신 시 KisErrorResponse로 매핑한 다음에 Exception으로 변환

* feat: 한국투자증권 서버의 에러 응답 객체 구

* rename: 클래스명 변경

* refactor: toException 메서드 변경

- switch 문으로 개선

* test: Exception 생성 부분을 정적 팩토리 호출로 변경

* refactor: null 체크 조건문 삭제

* feat: 재시도 연산이 실패했을시 onErrorResume 추가

* test: 재시도 연산이 실패시 예외 테스트 추가

* test: kisClient 모킹 처리

* test: 다수 종목 갱신 테스트 추가

* refactor: Flux 방식으로 변경

* refactor: onErrorResume 연산 수정

* test: 테스트 코드 수정

* feat: 액세스 토큰 관련 예외 추가

* feat: accessToken 널 체크 추가

* feat: doOnSuccess 연산 추가

* feat: CredeintailsTypeException 조건 추가

* fix: @CheckKisAccessToken 애노테이션 제거

- 비동기로 실행중 액세스 토큰 제거 또는 만료시 aop의 blockoptional에 의해서 에러 발생하여 kisService로 이동하고자 애노테이션을 제거함

* feat: deleteAccessToken api 추가

* feat: kisService에 액세스 토큰 체크 애노테이션 추가

* test: 레디스 클리너 추가

* fix: 메서드명 변경

* refactor: fetchDividend 반환 타입 변경

* rename: 메서드명 변경

* rename: 메서드명 변경

* refactor: delayManager로 변경

* test: delayManager 추가하여 테스트 실패 해결

* test: delayManager를 @SpyBean으로 변경

* test: holidayManager 필드 제거

* feat: 액세스 토큰 재발급 관련 수정
  • Loading branch information
yonghwankim-dev authored Aug 25, 2024
1 parent 6fcabf7 commit d06b81a
Show file tree
Hide file tree
Showing 29 changed files with 429 additions and 246 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void reloadStockDividend() {
// 0. 올해 말까지의 배당 일정을 조회
LocalDate now = localDateTimeService.getLocalDateWithNow();
LocalDate to = now.with(TemporalAdjusters.lastDayOfYear());
List<KisDividend> kisDividends = kisService.fetchDividendAll(now, to);
List<KisDividend> kisDividends = kisService.fetchDividendsBetween(now, to);

// 1. 새로운 배당 일정 탐색
Map<String, Stock> stockMap = getStockMapBy(kisDividends);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ public void listenPortfolioHolding(PortfolioHoldingEvent event) {
log.info("포트폴리오 종목 추가로 인한 종목 현재가 및 종가 갱신 수행");
String tickerSymbol = event.getTickerSymbol();
kisService.refreshStockCurrentPrice(List.of(tickerSymbol));
kisService.refreshLastDayClosingPrice(List.of(tickerSymbol));
kisService.refreshClosingPrice(List.of(tickerSymbol));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
import codesquad.fineants.domain.kis.client.KisClient;
import codesquad.fineants.domain.kis.repository.KisAccessTokenRepository;
import codesquad.fineants.domain.kis.service.KisAccessTokenRedisService;
import codesquad.fineants.global.common.delay.DelayManager;
import codesquad.fineants.global.common.time.LocalDateTimeService;
import codesquad.fineants.global.errors.errorcode.KisErrorCode;
import codesquad.fineants.global.errors.exception.FineAntsException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@Aspect
@Component
Expand All @@ -28,14 +32,19 @@ public class AccessTokenAspect {
private final KisClient client;
private final KisAccessTokenRedisService redisService;
private final LocalDateTimeService localDateTimeService;
private final DelayManager delayManager;

@Before(value = "@annotation(CheckedKisAccessToken) && args(..)")
public void checkAccessTokenExpiration() {
LocalDateTime now = localDateTimeService.getLocalDateTimeWithNow();
// 액세스 토큰이 만료 1시간 이전이라면 토큰을 재발급한다
if (manager.isTokenExpiringSoon(now)) {
client.fetchAccessToken()
.blockOptional(Duration.ofMinutes(10))
.doOnSuccess(kisAccessToken -> log.debug("success the kis access token issue : {}", kisAccessToken))
.retryWhen(Retry.fixedDelay(5, delayManager.fixedAccessTokenDelay()))
.onErrorResume(Exceptions::isRetryExhausted, throwable -> Mono.empty())
.onErrorResume(throwable -> Mono.empty())
.blockOptional(delayManager.timeout())
.ifPresent(newKisAccessToken -> {
redisService.setAccessTokenMap(newKisAccessToken, now);
manager.refreshAccessToken(newKisAccessToken);
Expand All @@ -61,6 +70,9 @@ public void checkAccessTokenExpiration() {

private Optional<KisAccessToken> handleNewAccessToken() {
Optional<KisAccessToken> result = client.fetchAccessToken()
.doOnSuccess(kisAccessToken -> log.debug("success the kis access token issue : {}", kisAccessToken))
.retryWhen(Retry.fixedDelay(5, delayManager.fixedAccessTokenDelay()))
.onErrorResume(throwable -> Mono.empty())
.blockOptional(Duration.ofMinutes(10));
log.info("new access Token Issue : {}", result);
return result;
Expand Down
19 changes: 6 additions & 13 deletions src/main/java/codesquad/fineants/domain/kis/client/KisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;

import codesquad.fineants.domain.kis.aop.CheckedKisAccessToken;
import codesquad.fineants.domain.kis.domain.dto.response.KisClosingPrice;
import codesquad.fineants.domain.kis.domain.dto.response.KisDividend;
import codesquad.fineants.domain.kis.domain.dto.response.KisDividendWrapper;
import codesquad.fineants.domain.kis.domain.dto.response.KisErrorResponse;
import codesquad.fineants.domain.kis.domain.dto.response.KisIpoResponse;
import codesquad.fineants.domain.kis.domain.dto.response.KisSearchStockInfo;
import codesquad.fineants.domain.kis.properties.KisAccessTokenRequest;
Expand All @@ -36,7 +36,6 @@
import codesquad.fineants.domain.kis.properties.kiscodevalue.imple.GB1;
import codesquad.fineants.domain.kis.properties.kiscodevalue.imple.PrdtTypeCd;
import codesquad.fineants.domain.kis.repository.KisAccessTokenRepository;
import codesquad.fineants.global.errors.exception.KisException;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
Expand Down Expand Up @@ -71,7 +70,6 @@ public Mono<KisAccessToken> fetchAccessToken() {
.retrieve()
.onStatus(HttpStatusCode::isError, this::handleError)
.bodyToMono(KisAccessToken.class)
.retryWhen(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofMinutes(1)))
.log();
}

Expand All @@ -97,7 +95,6 @@ public Mono<KisCurrentPrice> fetchCurrentPrice(String tickerSymbol) {
}

// 직전 거래일의 종가 조회
@CheckedKisAccessToken
public Mono<KisClosingPrice> fetchClosingPrice(String tickerSymbol) {
MultiValueMap<String, String> header = KisHeaderBuilder.builder()
.add(AUTHORIZATION, manager.createAuthorization())
Expand Down Expand Up @@ -134,7 +131,6 @@ private String yesterday() {
* @param tickerSymbol 종목의 단축코드
* @return 종목의 배당 일정 정보
*/
@CheckedKisAccessToken
public Mono<KisDividendWrapper> fetchDividendThisYear(String tickerSymbol) {
LocalDate today = LocalDate.now();
// 해당 년도 첫일
Expand Down Expand Up @@ -168,11 +164,10 @@ private Mono<KisDividendWrapper> fetchDividend(String tickerSymbol, LocalDate fr
header,
queryParam,
KisDividendWrapper.class
).retryWhen(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(5)));
);
}

@CheckedKisAccessToken
public Mono<List<KisDividend>> fetchDividendAll(LocalDate from, LocalDate to) {
public Mono<List<KisDividend>> fetchDividendsBetween(LocalDate from, LocalDate to) {
MultiValueMap<String, String> header = KisHeaderBuilder.builder()
.add(CONTENT_TYPE, APPLICATION_JSON_UTF8)
.add(AUTHORIZATION, manager.createAuthorization())
Expand All @@ -198,7 +193,6 @@ public Mono<List<KisDividend>> fetchDividendAll(LocalDate from, LocalDate to) {
).map(KisDividendWrapper::getKisDividends);
}

@CheckedKisAccessToken
public Mono<KisIpoResponse> fetchIpo(LocalDate from, LocalDate to) {
MultiValueMap<String, String> header = KisHeaderBuilder.builder()
.add(CONTENT_TYPE, APPLICATION_JSON_UTF8)
Expand Down Expand Up @@ -228,7 +222,6 @@ private String basicIso(LocalDate localDate) {
return localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
}

@CheckedKisAccessToken
public Mono<KisSearchStockInfo> fetchSearchStockInfo(String tickerSymbol) {
MultiValueMap<String, String> header = KisHeaderBuilder.builder()
.add(CONTENT_TYPE, APPLICATION_JSON_UTF8)
Expand Down Expand Up @@ -265,8 +258,8 @@ private <T> Mono<T> performGet(String urlPath, MultiValueMap<String, String> hea
}

private Mono<? extends Throwable> handleError(ClientResponse clientResponse) {
return clientResponse.bodyToMono(String.class)
.doOnNext(log::error)
.flatMap(body -> Mono.error(() -> new KisException(body)));
return clientResponse.bodyToMono(KisErrorResponse.class)
.doOnNext(kisErrorResponse -> log.error(kisErrorResponse.toString()))
.flatMap(kisErrorResponse -> Mono.error(kisErrorResponse::toException));
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package codesquad.fineants.domain.kis.controller;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import codesquad.fineants.domain.kis.client.KisAccessToken;
import codesquad.fineants.domain.kis.client.KisCurrentPrice;
import codesquad.fineants.domain.kis.domain.dto.request.StockPriceRefreshRequest;
import codesquad.fineants.domain.kis.domain.dto.response.KisClosingPrice;
Expand All @@ -23,7 +23,6 @@
import codesquad.fineants.global.success.KisSuccessCode;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@RestController
@RequestMapping("/api/kis")
Expand Down Expand Up @@ -64,7 +63,7 @@ public Mono<ApiResponse<KisCurrentPrice>> fetchCurrentPrice(
@PostMapping("/closing-price/all/refresh")
@Secured(value = {"ROLE_MANAGER", "ROLE_ADMIN"})
public ApiResponse<List<KisClosingPrice>> refreshAllLastDayClosingPrice() {
List<KisClosingPrice> responses = service.refreshAllLastDayClosingPrice();
List<KisClosingPrice> responses = service.refreshAllClosingPrice();
return ApiResponse.success(KisSuccessCode.OK_REFRESH_LAST_DAY_CLOSING_PRICE, responses);
}

Expand All @@ -74,7 +73,7 @@ public ApiResponse<List<KisClosingPrice>> refreshAllLastDayClosingPrice() {
public ApiResponse<List<KisClosingPrice>> refreshLastDayClosingPrice(
@RequestBody StockPriceRefreshRequest request
) {
List<KisClosingPrice> responses = service.refreshLastDayClosingPrice(request.getTickerSymbols());
List<KisClosingPrice> responses = service.refreshClosingPrice(request.getTickerSymbols());
return ApiResponse.success(KisSuccessCode.OK_REFRESH_LAST_DAY_CLOSING_PRICE, responses);
}

Expand All @@ -90,9 +89,11 @@ public ApiResponse<Set<StockDataResponse.StockIntegrationInfo>> fetchStockInfoIn
@GetMapping("/dividend/{tickerSymbol}")
@Secured(value = {"ROLE_MANAGER", "ROLE_ADMIN"})
public ApiResponse<List<KisDividend>> fetchDividend(@PathVariable String tickerSymbol) {
return ApiResponse.success(KisSuccessCode.OK_FETCH_DIVIDEND, service.fetchDividend(tickerSymbol)
.retryWhen(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(5)))
.blockOptional(Duration.ofMinutes(1))
.orElseGet(Collections::emptyList));
return ApiResponse.success(KisSuccessCode.OK_FETCH_DIVIDEND, service.fetchDividend(tickerSymbol));
}

@DeleteMapping("/access-token")
public ApiResponse<KisAccessToken> deleteAccessToken() {
return ApiResponse.ok("액세스 토큰을 삭제하였습니다.", service.deleteAccessToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package codesquad.fineants.domain.kis.domain.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;

import codesquad.fineants.global.errors.exception.kis.CredentialsTypeKisException;
import codesquad.fineants.global.errors.exception.kis.ExpiredAccessTokenKisException;
import codesquad.fineants.global.errors.exception.kis.KisException;
import codesquad.fineants.global.errors.exception.kis.RequestLimitExceededKisException;
import codesquad.fineants.global.errors.exception.kis.TokenIssuanceRetryLaterKisException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.ToString;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
public class KisErrorResponse {
@JsonProperty("rt_cd")
private String returnCode;
@JsonProperty("msg_cd")
private String messageCode;
@JsonProperty("msg1")
private String message;

public KisException toException() {
return switch (messageCode) {
case "EGW00201" -> new RequestLimitExceededKisException(returnCode, messageCode, message);
case "EGW00133" -> new TokenIssuanceRetryLaterKisException(returnCode, messageCode, message);
case "EGW00123" -> new ExpiredAccessTokenKisException(returnCode, messageCode, message);
case "EGW00205" -> new CredentialsTypeKisException(returnCode, messageCode, message);
default -> new KisException(returnCode, messageCode, message);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package codesquad.fineants.domain.kis.repository;

import static codesquad.fineants.domain.kis.service.KisService.*;

import java.time.Duration;
import java.util.Optional;

Expand All @@ -12,6 +10,7 @@
import codesquad.fineants.domain.kis.aop.AccessTokenAspect;
import codesquad.fineants.domain.kis.client.KisClient;
import codesquad.fineants.domain.kis.domain.dto.response.KisClosingPrice;
import codesquad.fineants.global.common.delay.DelayManager;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
Expand All @@ -20,9 +19,9 @@ public class ClosingPriceRepository {

private static final String format = "lastDayClosingPrice:%s";
private final RedisTemplate<String, String> redisTemplate;
private final KisAccessTokenRepository accessTokenManager;
private final KisClient kisClient;
private final AccessTokenAspect accessTokenAspect;
private final DelayManager delayManager;

public void addPrice(String tickerSymbol, long price) {
redisTemplate.opsForValue().set(String.format(format, tickerSymbol), String.valueOf(price), Duration.ofDays(2));
Expand All @@ -46,7 +45,7 @@ public Optional<Money> getClosingPrice(String tickerSymbol) {

private void handleClosingPrice(String tickerSymbol) {
kisClient.fetchClosingPrice(tickerSymbol)
.blockOptional(TIMEOUT)
.blockOptional(delayManager.timeout())
.ifPresent(this::addPrice);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package codesquad.fineants.domain.kis.repository;

import static codesquad.fineants.domain.kis.service.KisService.*;

import java.util.Arrays;
import java.util.Optional;

Expand All @@ -11,6 +9,7 @@
import codesquad.fineants.domain.common.money.Money;
import codesquad.fineants.domain.kis.client.KisClient;
import codesquad.fineants.domain.kis.client.KisCurrentPrice;
import codesquad.fineants.global.common.delay.DelayManager;
import lombok.RequiredArgsConstructor;
import reactor.util.retry.Retry;

Expand All @@ -20,6 +19,7 @@ public class CurrentPriceRedisRepository implements PriceRepository {
private static final String CURRENT_PRICE_FORMAT = "cp:%s";
private final RedisTemplate<String, String> redisTemplate;
private final KisClient kisClient;
private final DelayManager delayManager;

@Override
public void savePrice(KisCurrentPrice... currentPrices) {
Expand Down Expand Up @@ -49,8 +49,8 @@ private Optional<String> getCachedPrice(String tickerSymbol) {

private Optional<KisCurrentPrice> fetchAndCachePriceFromKis(String tickerSymbol) {
return kisClient.fetchCurrentPrice(tickerSymbol)
.retryWhen(Retry.fixedDelay(Long.MAX_VALUE, DELAY))
.blockOptional(TIMEOUT)
.retryWhen(Retry.fixedDelay(5, delayManager.fixedDelay()))
.blockOptional(delayManager.timeout())
.map(this::savePrice);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public void refreshAccessToken(KisAccessToken accessToken) {
}

public String createAuthorization() {
if (accessToken == null) {
return null;
}
return accessToken.createAuthorization();
}

Expand Down
Loading

0 comments on commit d06b81a

Please sign in to comment.