본문 바로가기
프로젝트 관련

일일 단위로 환율 DB 저장을 위한 스케줄링 구현(with Spring boot)

by khds 2024. 8. 11.

 

들어가기

 

프로젝트 진행 중 예산 관리하는 기능에서 달러, 엔에 따라 변환해야 하는 기능이 있다.

이를 위해 환율을 적용해야 하는데, 매일 변하는 환율에 따라 이를 적용해야 한다.

이를 위해 한국수출입은행 환율 정보 조회 API를 사용하고자 한다.

일일 1000번 호출이 가능하기에 데이터베이스에 저장해 놓았다가 환율 변환 시 저장된 값을 통해 변환을 할 수 있도록 할 것이다. 

이를 위해선 데이터 베이스에 매일 환율이 업데이트가 필요하다. 그렇기에 스프링 부트에 내장되어 있는 @Scheduled 어노테이션을 사용할 것이다. 

스프링 부트 및 JPA를 사용하였고 스프링 부트 버전은 3.2.5 이다.

 

 

본론 

1. 한국수출입은행 환율 정보 조회 API  활용

 

우선 환율 정보를 조회하기 위한 API를 신청해야 한다. 아래의 주소에서 확인 가능하다. 

https://www.koreaexim.go.kr/ir/HPHKIR019M01#tab1

 

 

3개의 정보를 조회할 수 있으며 1번 현재환율 API를 사용할 것이다. 

인증 키를 해당 페이지에서 신청 후 아래와 같은 정보들을 통해 요청할 수 있다.

 

총 23가지 화폐로 응답되며 프로젝트에서 필요한 미국, 일본 정보 예시 정보는 아래와 같이 나온다.

  {
    "result": 1,  // 조회 결과
    "cur_unit": "JPY(100)",  // 통화코드
    "ttb": "921.77",  // 국가/통화명
    "tts": "940.4", // 전신환(송금) 받으실 때
    "deal_bas_r": "931.09",  // 전신환(송금) 보내실 때
    "bkpr": "931",  // 매매 기준율
    "yy_efee_r": "0",  // 장부가격
    "ten_dd_efee_r": "0",  // 년환가료율
    "kftc_bkpr": "931",  // 10일환가료율
    "kftc_deal_bas_r": "931.09",  // 서울외국환중개 매매기준율
    "cur_nm": "일본 옌"  // 서울외국환중개장부가격
  }, 
 {
    "result": 1,
    "cur_unit": "USD",
    "ttb": "1,355.7",
    "tts": "1,383.09",
    "deal_bas_r": "1,369.4",
    "bkpr": "1,369",
    "yy_efee_r": "0",
    "ten_dd_efee_r": "0",
    "kftc_bkpr": "1,369",
    "kftc_deal_bas_r": "1,369.4",
    "cur_nm": "미국 달러"
  }

 

 

2. 엔티티 생성

 

엔티티는 화폐 단위, 이름, 환율, 날짜 4가지로 지정을 하였다.

@Entity
@Getter
@Table(name = "currency")
@NoArgsConstructor
public class Currency {

    @Id
    @Column(name = "id")
    private String cur_unit;

    @Column(nullable = false)
    private String cur_name;

    @Column(nullable = false)
    private Double exchange_rate;

    @Column(nullable = false)
    private String date;

    public Currency(String cur_unit, String cur_name, Double exchange_rate, String date) {
        this.cur_unit = cur_unit;
        this.cur_name = cur_name;
        this.exchange_rate = exchange_rate;
        this.date = date;
    }
}

 

해당 필드로 실제 데이터가 저장되면 아래와 같이 나오게 하였다.

 

 

3. openFeign을 통해 데이터 가져오기

 

이제 실제 요청을 통해 데이터를 가져올 것이다. 

openFeign API를 활용하였고, 이에 대한 기본적인 내용은 아래를 참고하길 바란다.

https://khdscor.tistory.com/128

 

OpenFeign을 통한 외부 API (With Spring Boot)

들어가기  최근 프로젝트를 진행하면서 비즈니스 로직을 수행하는 도중 외부 API를 호출해야 하는 기능을 구현해야 하는 경우가 있었다. RestTemplate, WebClient 등 다양하게 있겠지만, Spring Data Jpa

khdscor.tistory.com

 

메서드와 dto는 아래와 같이 작성하였다. 

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ExchangeUseCase {
    private Integer result; // 조회 결과
    private String cur_unit; // 통화코드
    private String cur_nm; // 국가/통화명
    private String ttb; // 전신환(송금) 받으실 때
    private String tts; // 전신환(송금) 보내실 때
    private String deal_bas_r; // 매매 기준율
    private String bkpr; // 장부가격
    private String yy_efee_r; // 년환가료율
    private String ten_dd_efee_r; // 10일환가료율
    private String kftc_bkpr; // 서울외국환중개 매매기준율
    private String kftc_deal_bas_r; // 서울외국환중개장부가격
}

 

@FeignClient(name = "exchangeRateApi",
    url = "https://www.koreaexim.go.kr/site/program/financial/exchangeJSON?data=AP01",
    fallback = CurrencyServiceFallback.class)
public interface CurrencyService {
    @GetMapping
    List<ExchangeUseCase> findExchange(
        @RequestParam("authkey") String authKey,
        @RequestParam("searchdate") String searchDate
    );
}

 

이렇게 데이터를 가져오는 기본적인 코드를 작성하였다. 이제 본격적으로 스케줄링 코드를 작성해 보자. 

 

 

4. @Scheduled 어노테이션을 통한 특정 시각마다 업데이트

 

자 이제 @Scheduled 어노테이션을 통해 환율 정보를 조회하여 데이터베이스에 저장할 것이다. 

먼저 @EnableScheduling 어노테이션을 메인 어플리케이션, 혹은 Config 파일에 지정한다. 지정하지 않으면  @Scheduled가 동작하지 않는다.

@Configuration
@EnableScheduling
public class SchedulerConfig {
}

 

이제 메인 로직을 봐보자. 

환율은 평일에만 업데이트가 되므로 월 ~ 금에 14시부터 5분 주기로 14시 55분까지 호출을 하는 로직을 구현하고 싶다.

이를 위해 아래와 같이 코드를 작성할 수 있다.

private final CurrencyService currencyService;

private final CurrencyRepository currencyRepository;

@Value("${key.exchange-rate.key}")
private String key;
 
 // 월 ~ 금 14시 00분 부터 5분 주기로 55분까지 12회 진행
@Scheduled(cron = "0 0/5 15 * * MON-FRI", zone = "Asia/Seoul")
public void updateCurrency() {
    try {
    	// 오늘 날짜 조회
        String today = DateUtils.getToday();
        // 환율 조회 API 호출
        List<ExchangeUseCase> exchanges = currencyService.findExchange(key, today);
        List<Currency> currencies = new ArrayList<>();
        // 조회된 값을 통해 저장할 DB에 데이터 생성
        for (ExchangeUseCase exchange : exchanges) {
            processExchange(exchange, currencies, today);
        }
        log.info("Save exchange rate to DB");
        currencyRepository.saveAll(currencies);
    } catch (Exception exception) {
        log.error("Failed to update exchange rate : " + exception.getMessage());
    }
}

 

@Scheduled에서 크론식(cron)은 스케줄링을 할 때 사용하는 방식으로 초, 분, 시, 일, 월, 요일을 설정할 수가 있다. 

서버 설정 시각은 UTC를 따르기 때문에 zone 옵션으로 "Asia/Seoul"(한국은 UTC+9)을 지정해 주었다.

@Scheduled을 설정할 때 시간을 설정하는 방법은 아래를 참고하길 바란다. 

https://velog.io/@developer_khj/Spring-Boot-Scheduler-Scheduled

 

[Spring Boot] @Scheduled를 이용한 스케쥴러 구현하기

Spring Boot`에서 @Scheduled 어노테이션을 이용하여 스케쥴러를 구현하는 방법에 대해 작성하였습니다

velog.io

 

먼저 오늘 날짜를 조회하는 DateUtils.getToday를 확인해 보자. 

public static String getToday() {
    // 현재 날짜
    LocalDate currentDate = LocalDate.now();
    // 날짜 포맷
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
    // 토요일일 경우 금요일로 변경
    if (currentDate.getDayOfWeek() == DayOfWeek.SATURDAY) {
        return currentDate.minusDays(1).format(formatter);
    }
    // 일요일일 경우 금요일로 변경
    if (currentDate.getDayOfWeek() == DayOfWeek.SUNDAY) {
        return currentDate.minusDays(2).format(formatter);
    }
    return currentDate.format(formatter);
}

 

LacalDate.now()를 통해 현재 날짜를 가져온 후 getDayOfweek()를 통해 토, 일일 경우 금요일로 변경하여 formatter의 형태로 반환하도록 하였다.

 

이후 위에서 정의한 currencyService.findExchange()를 호출하여 데이터를 받아온다. 이제 processExchange()를 통해 데이터를 원하는 형태로 가공할 차례이다.

private void processExchange(ExchangeUseCase exchange, List<Currency> currencies, String today) {
    if (cur_unit.equals("JPY(100)") || cur_unit.equals("USD")) {
        String cur_unit = exchange.getCur_unit();
        String cur_name = exchange.getCur_nm();
        double exchangeRate = Double.parseDouble(exchange.getDeal_bas_r().replace(",", ""));
        if (exchange.getCur_unit().equals("JPY(100)")) {
            cur_unit = "JPY";
            cur_name = "일본 엔";
            exchangeRate = Math.round(exchangeRate) / 100.0;
        }
        Currency currency = new Currency(cur_unit, cur_name, exchangeRate, today);
        currencies.add(currency);
    }
}

 

JPY는 100 단위로 받기 때문에 1 단위로 변환을 해주는 과정을 거쳤다.

이후 currencyRepository.saveAll(currencies)를 통해 데이터를 저장할 수 있도록 하였다.

 

전체적인 코드는 아래와 같다.

@Service
@RequiredArgsConstructor
public class SchedulerService {

    private final CurrencyService currencyService;

    private final CurrencyRepository currencyRepository;
    
    @Value("${key.exchange-rate.key}")
    private String key;

    // 월 ~ 금 14시 00분 부터 5분 주기로 55분까지 12회 진행
    @Scheduled(cron = "0 0/5 15 * * MON-FRI", zone = "Asia/Seoul")
    public void updateCurrency() {
        try {
            String today = DateUtils.getToday();
            List<ExchangeUseCase> exchanges = currencyService.findExchange(key, today);
            List<Currency> currencies = new ArrayList<>();
            for (ExchangeUseCase exchange : exchanges) {
                processExchange(exchange, currencies, today);
            }
            LogUtils.writeInfoLog("updateCurrency", "Save exchange rate to DB");
            currencyRepository.saveAll(currencies);
        } catch (Exception exception) {
            LogUtils.writeErrorLog("updateCurrency", "Failed to update exchange rate : " + exception.getMessage());
        }
    }

    private void processExchange(ExchangeUseCase exchange, List<Currency> currencies, String today) {
        if (isTargetCurrency(exchange.getCur_unit())) {
            String cur_unit = exchange.getCur_unit();
            String cur_name = exchange.getCur_nm();
            double exchangeRate = Double.parseDouble(exchange.getDeal_bas_r().replace(",", ""));
            if (exchange.getCur_unit().equals("JPY(100)")) {
                cur_unit = "JPY";
                cur_name = "일본 엔";
                exchangeRate = Math.round(exchangeRate) / 100.0;
            }
            Currency currency = new Currency(cur_unit, cur_name, exchangeRate, today);
            currencies.add(currency);
        }
    }

    // 엔과 달러만 저장
    private boolean isTargetCurrency(String cur_unit) {
        return cur_unit.equals("JPY(100)") || cur_unit.equals("USD");
    }