Skip to content

Commit

Permalink
[fix] 액세스 토큰 갱신 구조 변경 (#449)
Browse files Browse the repository at this point in the history
* feat: TIMEOUT 필드 이름 변경

* fix: 종목 조회 및 배당 일정 조회 메서드를 Mono 타입으로 반환하도록 변경

* test: 테스트 실패 해결

* test: 상장 종목 조회의 예외 케이스 추가

* fix: Flux 연산 수행중 별도의 쓰레드에서 동작시 blocking하지 않도록 변경

* test: 테스트 실패 해결

* test: todo 추가 및 새로운 테스트 추가

- 도중에 blocking되지 않아야함

* fix: fetchDividend 메서드 반환타입을 Flux로 변경

* test: 테스트 추가

* test: 액세스 토큰 재발급 실패시 에외 케이스 테스트 추가

* fix: checkAccessToken 애노테이션으로 변경

* fix: onErrorResume 연산 추가

* feat: 로깅 추가

* test: 테스트 제거

* feat: 액세스 토큰을 재발급하는 스케줄러 구현

* test: 액세스 토큰 재발급하는 테스트 추가

* feat: 스케줄러 주기 변경

* fix: aop 제거 및 애노테이션 제거

* refactor: 종가 저장소 개선

* feat: 애노테이션 제거

* rename: 패키지 이동

* style: 코드 정리
  • Loading branch information
yonghwankim-dev authored Aug 27, 2024
1 parent 61b49a3 commit cfe38e0
Show file tree
Hide file tree
Showing 33 changed files with 528 additions and 425 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package codesquad.fineants.domain.exchangerate.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import codesquad.fineants.domain.exchangerate.service.ExchangeRateService;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ExchangeRateScheduler {

private final ExchangeRateService service;

@Scheduled(cron = "0 0 * * * *") // 매일 자정에 한번씩 수행
@Transactional
public void updateExchangeRates() {
service.updateExchangeRates();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -57,11 +55,20 @@ private ExchangeRate findBaseExchangeRate(String defaultCode) {
public ExchangeRateListResponse readExchangeRates() {
List<ExchangeRateItem> items = exchangeRateRepository.findAll().stream()
.map(ExchangeRateItem::from)
.collect(Collectors.toList());
.toList();
return ExchangeRateListResponse.from(items);
}

@Scheduled(cron = "0 0 * * * *") // 매일 자정에 한번씩 수행
@Transactional
@Secured("ROLE_ADMIN")
public void patchBase(String code) {
// 기존 기준 통화의 base 값을 false로 변경
findBaseExchangeRate().changeBase(false);
// code의 base 값을 true로 변경
findExchangeRateBy(code).changeBase(true);
this.updateExchangeRates();
}

@Transactional
public void updateExchangeRates() {
List<ExchangeRate> originalRates = exchangeRateRepository.findAll();
Expand All @@ -88,16 +95,6 @@ private ExchangeRate findBaseExchangeRate(List<ExchangeRate> rates) {
.orElseThrow(() -> new FineAntsException(ExchangeRateErrorCode.NOT_EXIST_BASE));
}

@Transactional
@Secured("ROLE_ADMIN")
public void patchBase(String code) {
// 기존 기준 통화의 base 값을 false로 변경
findBaseExchangeRate().changeBase(false);
// code의 base 값을 true로 변경
findExchangeRateBy(code).changeBase(true);
updateExchangeRates();
}

private ExchangeRate findExchangeRateBy(String code) {
return exchangeRateRepository.findByCode(code)
.orElseThrow(() -> new FineAntsException(ExchangeRateErrorCode.NOT_EXIST_EXCHANGE_RATE));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package codesquad.fineants.domain.gainhistory.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import codesquad.fineants.domain.gainhistory.domain.dto.response.PortfolioGainHistoryCreateResponse;
import codesquad.fineants.domain.gainhistory.service.PortfolioGainHistoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class PortfolioGainHistoryScheduler {

private final PortfolioGainHistoryService service;

@Scheduled(cron = "0 0 16 * * ?") // 매일 16시에 실행
@Transactional
public void scheduledPortfolioGainHistory() {
PortfolioGainHistoryCreateResponse response = service.addPortfolioGainHistory();
log.info("포트폴리오 수익 내역 기록 결과, size = {}", response.getIds().size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.ArrayList;
import java.util.List;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -26,13 +25,6 @@ public class PortfolioGainHistoryService {
private final PortfolioRepository portfolioRepository;
private final CurrentPriceRedisRepository currentPriceRedisRepository;

@Transactional
@Scheduled(cron = "0 0 16 * * ?") // 매일 16시에 실행
public void scheduledPortfolioGainHistory() {
PortfolioGainHistoryCreateResponse response = addPortfolioGainHistory();
log.info("포트폴리오 수익 내역 기록 결과, size = {}", response.getIds().size());
}

@Transactional
public PortfolioGainHistoryCreateResponse addPortfolioGainHistory() {
List<Portfolio> portfolios = portfolioRepository.findAll();
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -80,16 +81,19 @@ public ApiResponse<List<KisClosingPrice>> refreshLastDayClosingPrice(
// 상장된 종목 정보 조회
@GetMapping("/ipo/search-stock-info")
@Secured(value = {"ROLE_MANAGER", "ROLE_ADMIN"})
public ApiResponse<Set<StockDataResponse.StockIntegrationInfo>> fetchStockInfoInRangedIpo() {
Set<StockDataResponse.StockIntegrationInfo> result = service.fetchStockInfoInRangedIpo();
return ApiResponse.success(KisSuccessCode.OK_FETCH_IPO_SEARCh_STOCK_INFO, result);
public Mono<ApiResponse<Set<StockDataResponse.StockIntegrationInfo>>> fetchStockInfoInRangedIpo() {
Mono<Set<StockDataResponse.StockIntegrationInfo>> result = service.fetchStockInfoInRangedIpo()
.collect(Collectors.toUnmodifiableSet());
return result.map(data -> ApiResponse.success(KisSuccessCode.OK_FETCH_IPO_SEARCh_STOCK_INFO, data));
}

// 배당 일정 조회
@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));
public Mono<ApiResponse<List<KisDividend>>> fetchDividend(@PathVariable String tickerSymbol) {
Mono<List<KisDividend>> result = service.fetchDividend(tickerSymbol)
.collect(Collectors.toList());
return result.map(data -> ApiResponse.success(KisSuccessCode.OK_FETCH_DIVIDEND, data));
}

@DeleteMapping("/access-token")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import codesquad.fineants.domain.stock.domain.entity.Stock;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
Expand All @@ -29,6 +30,7 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
@JsonDeserialize(using = KisDividend.KissDividendDeserializer.class)
@EqualsAndHashCode
public class KisDividend implements Comparable<KisDividend> {
private String tickerSymbol;
private Money dividend;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,62 @@
import org.springframework.stereotype.Component;

import codesquad.fineants.domain.common.money.Money;
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 codesquad.fineants.global.errors.exception.kis.CredentialsTypeKisException;
import codesquad.fineants.global.errors.exception.kis.ExpiredAccessTokenKisException;
import codesquad.fineants.global.errors.exception.kis.RequestLimitExceededKisException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@RequiredArgsConstructor
@Component
@RequiredArgsConstructor
@Slf4j
public class ClosingPriceRepository {

private static final String format = "lastDayClosingPrice:%s";
private static final String CLOSING_PRICE_FORMAT = "lastDayClosingPrice:%s";
private final RedisTemplate<String, String> redisTemplate;
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));
addPrice(KisClosingPrice.create(tickerSymbol, price));
}

public void addPrice(KisClosingPrice price) {
redisTemplate.opsForValue()
.set(String.format(format, price.getTickerSymbol()), String.valueOf(price.getPrice()), Duration.ofDays(2));
.set(String.format(CLOSING_PRICE_FORMAT, price.getTickerSymbol()), String.valueOf(price.getPrice()),
Duration.ofDays(2));
}

public Optional<Money> getClosingPrice(String tickerSymbol) {
accessTokenAspect.checkAccessTokenExpiration();

String closingPrice = redisTemplate.opsForValue().get(String.format(format, tickerSymbol));
if (closingPrice == null) {
handleClosingPrice(tickerSymbol);
return getClosingPrice(tickerSymbol);
public Optional<Money> fetchPrice(String tickerSymbol) {
Optional<String> cachedPrice = getCachedPrice(tickerSymbol);
if (cachedPrice.isEmpty()) {
Optional<KisClosingPrice> price = fetchClosingPriceFromKis(tickerSymbol);
price.ifPresent(this::addPrice);
return price
.map(KisClosingPrice::getPrice)
.map(Money::won);
}
return Optional.of(Money.won(closingPrice));
return cachedPrice.map(Money::won);
}

private Optional<String> getCachedPrice(String tickerSymbol) {
return Optional.ofNullable(redisTemplate.opsForValue().get(String.format(CLOSING_PRICE_FORMAT, tickerSymbol)));
}

private void handleClosingPrice(String tickerSymbol) {
kisClient.fetchClosingPrice(tickerSymbol)
.blockOptional(delayManager.timeout())
.ifPresent(this::addPrice);
private Optional<KisClosingPrice> fetchClosingPriceFromKis(String tickerSymbol) {
return kisClient.fetchClosingPrice(tickerSymbol)
.doOnSuccess(price -> log.debug("reload stock current price {}", price))
.onErrorResume(ExpiredAccessTokenKisException.class::isInstance, throwable -> Mono.empty())
.onErrorResume(CredentialsTypeKisException.class::isInstance, throwable -> Mono.empty())
.retryWhen(Retry.fixedDelay(5, delayManager.fixedDelay())
.filter(RequestLimitExceededKisException.class::isInstance))
.onErrorResume(Exceptions::isRetryExhausted, throwable -> Mono.empty())
.blockOptional(delayManager.timeout());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.stereotype.Repository;

import codesquad.fineants.domain.common.money.Money;
import codesquad.fineants.domain.kis.aop.CheckedKisAccessToken;
import codesquad.fineants.domain.kis.client.KisClient;
import codesquad.fineants.domain.kis.client.KisCurrentPrice;
import codesquad.fineants.global.common.delay.DelayManager;
Expand Down Expand Up @@ -55,7 +54,6 @@ private Optional<String> getCachedPrice(String tickerSymbol) {
return Optional.ofNullable(redisTemplate.opsForValue().get(String.format(CURRENT_PRICE_FORMAT, tickerSymbol)));
}

@CheckedKisAccessToken
private Optional<KisCurrentPrice> fetchAndCachePriceFromKis(String tickerSymbol) {
return kisClient.fetchCurrentPrice(tickerSymbol)
.doOnSuccess(kisCurrentPrice -> log.debug("reload stock current price {}", kisCurrentPrice))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package codesquad.fineants.domain.kis.repository;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -44,5 +45,9 @@ public boolean isTokenExpiringSoon(LocalDateTime localDateTime) {
}
return accessToken.betweenSecondFrom(localDateTime).toSeconds() < 3600;
}

public Optional<KisAccessToken> getAccessToken() {
return Optional.ofNullable(accessToken);
}
}

Loading

0 comments on commit cfe38e0

Please sign in to comment.