들어가기에 앞서
이전 블로그 글에 아래와 같은 글을 남겼습니다.
2023.11.15 - [개발/Spring & SpringBoot] - [Spring Boot] 대용량 데이터 처리를 위해 Batch를 알아보자 - 이론 편
이론 편에서 실전 편으로 넘어가기 전에 배치에 대해서 알아보는 시간을 가지면 좋을 것 같아서, 한국수출입은행의 환율정보 Open API를 활용해서 간단한 배치 예제를 만들어 보았습니다. 여러 블로그 및 ChatGPT 를 활용하여 제작하였고, 스프링 배치 버전이 변경되면서 달라진 부분들을 공부하면서 만들었기 때문에 틀린 부분이 있다면 언제든지 말씀 부탁드립니다.
처음 사용하시는 분들도 이해하기 쉽도록 최대한 자세하게 작성하였습니다. 내용이 길어 천천히 보시면 좋을 것 같습니다.
한국수출입은행 Open API 발급 방법
Open API는 개발된 공공데이터를 누구나 사용할 수 있도록 공개된 API(Application Program Interface)를 말합니다.
Open API 제공목록
한국수출입은행은 국제금리 API, 대출금리 API, 현재환율 API 를 제공하고 있습니다. 이 글에서는 현재환율 API를 사용하여 예제 코드를 작성해 보았습니다.
Open API 개발명세
1. 요청 URL (Request URL) + 요청변수
요청 URL (Request URL) + authkey (인증키) + searchdate (검색요청날짜) + data (검색요청 API타입)
2. 출력결과 (Response Element)
- RESULT (조회 결과)
- CUR_UNIT (통화코드)
- CUR_NM (국가/통화명)
- TTB (전신환(송금) 받으실 때)
- TTS (전신환(송금) 보내실 때)
- DEAL_BAS_R (매매 기준율)
- BKPR (장부가격)
- YY_EFEE_R (년환가료율)
- TEN_DD_EFEE_R (10 일환가료율)
- KFTC_DEAL_BAS_R (서울외국환중개 매매기준율)
- KFTC_BKPR (서울외국환중개 장부가격)
3. 나의 인증키 발급내역
인증키 신청을 진행하고 나의 인증키 발급내역 탭을 누르게 되면 인증키를 확인할 수 있습니다.
(인증키 신청 절차는 굉장히 간단하여 생략하였습니다.)
사용하기 위한 준비는 어느 정도 끝났으며, 본격적으로 코드를 짜보도록 하겠습니다.
환경 세팅
기본적인 환경 설정은 다음과 같습니다.
- IntelliJ IDEA 2022.1.2
- Spring Boot version '3.1.5'
- Spring Batch version '5.0.3'
- JAVA 17
- Gradle
Spring Batch 5 이상 버전으로 개발하면서, Spring Batch 4 버전과 달라진 점들이 있습니다. 예제를 보신다면 조금 낯설 수도 있다고 생각합니다. 해당 내용은 다른 글에서 조금 더 깊이 있게 정리해 보는 시간을 가지도록 하겠습니다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
id("com.google.osdetector") version "1.7.1"
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
if (osdetector.classifier == "osx-aarch_64") {
runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.77.Final:${osdetector.classifier}")
}
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
}
tasks.named('bootBuildImage') {
builder = 'paketobuildpacks/builder-jammy-base:latest'
}
tasks.named('test') {
useJUnitPlatform()
}
application.properties
별도로 파일(application-API.properties)을 따로 만들어서 의존성을 주입하는 이유는 나중에 다른 API 호출시에 한 곳에서 KEY 들을 관리하고 싶기 때문입니다. 현재 글에서 사용하고 있는 코드의 경우에는 별도의 파일(application-API.properties) 을 만들 필요는 없습니다. 그러나 차후 해당 예제를 가지고 활용할 수도 있어 아래와 같이 구성한 점은 참고 부탁드립니다.
# OPEN API
spring.profiles.include=API
application-API.properties
# Exchange Rate API
exchange-authkey=사이트에서 발급받은 API-KEY
exchange-data=AP01
ExchangeBatchApplication.java
메인 함수는 간단하여 설명을 생략하겠습니다.
package com.main.exchangeBatch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class ExchangeBatchApplication {
public static void main(String[] args) {
// 스프링 애플리케이션을 실행하고 컨텍스트를 얻어옵니다.
ConfigurableApplicationContext context = SpringApplication.run(ExchangeBatchApplication.class, args);
}
}
ExchangeDto.java
한국수출입은행 환율정보 API 호출 시 출력결과로 나오는 변수들을 객체로 생성하기 위한 파일을 생성하였습니다.
package com.main.exchangeBatch.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@NoArgsConstructor
@ToString
public class ExchangeDto {
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; // 서울외국환중개장부가격
}
ExchangeUtils.java
배치에 사용될 내 로직 내 기능을 독립적으로 구성했습니다.
Util(Utils) 클래스/패키지 란 무엇인가요?
Util(Utils) 클래스/패키지는 프로젝트 내 전역으로 사용되는 문자열 처리, 날짜 및 시간 처리 등 독립적인 기능을 구현해 놓은 클래스/패키지입니다. 단순한 처리만 하는 메서드들은 특히 정적(static) 메서드로 많이 구성을 합니다.
필자는 여러 가지 API 관련 호출 로직들은 Util(Utils) 패키지에 모으려고 하였습니다. 그러나 비즈니스 로직과 관련된 내용은 Util(Utils) 패키지에 넣으면 안 된다는 내용도 블로그 글에서 봐서 Service로 옮기는 것을 고민하고 있습니다. 해당 부분을 고려하시고 폴더 구조를 바꾸어 사용하시면 좋을 것 같습니다.
getSearchdate()
주말(토요일, 일요일) 에는 환율 정보가 들어오지 않습니다. 파라미터에 값을 설정하기 위하여 토요일, 일요일 모두 금요일로 설정하도록 하는 함수입니다. 특정 조건들을 추가하여 특정 일자를 호출하고 싶다면 이 메서드에서 수정하면 됩니다.
private String getSearchdate() {
LocalDate currentDate = LocalDate.now();
DayOfWeek dayOfWeek = currentDate.getDayOfWeek();
// 토요일
if (dayOfWeek.getValue() == 6)
return currentDate.minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// 일요일
if (dayOfWeek.getValue() == 7)
return currentDate.minusDays(2).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return currentDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
}
@Value("${-- KEY --}")
application-API.properties 파일에 작성한 값을 키(명칭)를 통해 가져옵니다. 해당 어노테이션을 통해 작성한 변수에 값을 부여합니다.
@Value("${exchange-authkey}")
private String authkey;
@Value("${exchange-data}")
private String data;
getExchangeDataSync()
- Open API 개발명세의 요청 URL (Request URL) + 요청변수 형식을 구성하여 Get 방식을 사용하였습니다.
- WebClient를 사용하여 외부 API를 호출할 땐 인코딩을 주의해야 합니다.
- DefaultUriBuilderFactory 객체를 생성하여 인코딩 모드를 None으로 변경하고 이를 아래와 같이 WebClient에 적용했습니다.
- queryParam을 사용할 때, API를 WebClient로 호출하기 위해서 인코딩을 하지 않도록 처리하였습니다.
public JsonNode getExchangeDataSync() {
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
// WebClient를 생성합니다.
webClient = WebClient.builder().uriBuilderFactory(factory).build();
// WebClient를 사용하여 동기적으로 데이터를 요청하고, 바로 parseJson 함수를 호출합니다.
String responseBody = webClient.get()
.uri(builder -> builder
.scheme("https")
.host("www.koreaexim.go.kr")
.path("/site/program/financial/exchangeJSON")
.queryParam("authkey", authkey)
.queryParam("searchdate", searchdate)
.queryParam("data", data)
.build())
.retrieve()
.bodyToMono(String.class)
.block(); // 동기적으로 결과를 얻음
return parseJson(responseBody);
}
parseJson(String responseBody)
getExchangeDataSync()에서 가져온 결과 값 (String responseBody)을 Json 형식으로 나타내기 위한 작업입니다.
private JsonNode parseJson(String responseBody) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(responseBody);
} catch (IOException e) {
// 예외 처리 필요
e.printStackTrace();
return null;
}
}
ExchageUtils.java - 전체 코드
package com.main.exchangeBatch.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.main.exchangeBatch.dto.ExchangeDto;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Component
public class ExchangeUtils {
@Value("${exchange-authkey}")
private String authkey;
@Value("${exchange-data}")
private String data;
private final String searchdate = getSearchdate();
WebClient webClient;
public JsonNode getExchangeDataSync() {
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
// WebClient를 생성합니다.
webClient = WebClient.builder().uriBuilderFactory(factory).build();
// WebClient를 사용하여 동기적으로 데이터를 요청하고, 바로 parseJson 함수를 호출합니다.
String responseBody = webClient.get()
.uri(builder -> builder
.scheme("https")
.host("www.koreaexim.go.kr")
.path("/site/program/financial/exchangeJSON")
.queryParam("authkey", authkey)
.queryParam("searchdate", searchdate)
.queryParam("data", data)
.build())
.retrieve()
.bodyToMono(String.class)
.block(); // 동기적으로 결과를 얻음
return parseJson(responseBody);
}
private JsonNode parseJson(String responseBody) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(responseBody);
} catch (IOException e) {
// 예외 처리 필요
e.printStackTrace();
return null;
}
}
public List<ExchangeDto> getExchangeDataAsDtoList() {
JsonNode jsonNode = getExchangeDataSync();
if (jsonNode != null && jsonNode.isArray()) {
List<ExchangeDto> exchangeDtoList = new ArrayList<>();
for (JsonNode node : jsonNode) {
ExchangeDto exchangeDto = convertJsonToExchangeDto(node);
exchangeDtoList.add(exchangeDto);
}
return exchangeDtoList;
}
return Collections.emptyList();
}
private ExchangeDto convertJsonToExchangeDto(JsonNode jsonNode) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.treeToValue(jsonNode, ExchangeDto.class);
} catch (JsonProcessingException e) {
// 예외 처리 필요
e.printStackTrace();
return null;
}
}
private String getSearchdate() {
LocalDate currentDate = LocalDate.now();
DayOfWeek dayOfWeek = currentDate.getDayOfWeek();
// 토요일
if (dayOfWeek.getValue() == 6)
return currentDate.minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// 일요일
if (dayOfWeek.getValue() == 7)
return currentDate.minusDays(2).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return currentDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
}
}
ExchangeBatch.java
배치에 대한 비즈니스 로직을 담은 파일입니다. 간단한 배치 예제이기 때문에, 비교적 Chunk 방식보다 가벼운 Tasklet 방식으로 구성하였습니다.
ExchangeBatch.java - 전체 코드
package com.main.exchangeBatch.batch;
import com.main.exchangeBatch.dto.ExchangeDto;
import com.main.exchangeBatch.utils.ExchangeUtils;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.List;
@Configuration
public class ExchangeBatch {
@Autowired
private ExchangeUtils exchangeUtils;
@Bean
public Job exchangeJob(JobRepository jobRepository, Step step) {
return new JobBuilder("exchangeJob", jobRepository)
.start(step)
.build();
}
@Bean
public Step step(JobRepository jobRepository, Tasklet tasklet, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("step", jobRepository)
.tasklet(tasklet, platformTransactionManager).build();
}
@Bean
public Tasklet tasklet(){
return ((contribution, chunkContext) -> {
List<ExchangeDto> exchangeDtoList = exchangeUtils.getExchangeDataAsDtoList();
for (ExchangeDto exchangeDto : exchangeDtoList) {
System.out.println("통화 : " + exchangeDto.getCur_nm());
System.out.println("환율 : " + exchangeDto.getDeal_bas_r());
// 추가적인 필드가 있다면 출력 또는 활용
}
return RepeatStatus.FINISHED;
});
}
}
실행 결과
- 11월 21일 임의 실행
Github 링크
https://github.com/SeoYounSeok/exchangeBatch.git
추가 개발 예정 사항
스케줄러(Scheduler)를 추가하여 특정한 시간에 등록된 작업을 진행하도록 할 예정입니다.
예) Spring Scehduler, Quartz
Logback을 활용하여 Log를 남기고, 예외처리를 추가할 예정입니다. 현재 코드는 System.out.println을 사용하여 출력을 해주고 있습니다. 해당 부분을 수정하여 Log로 남기고, 예외처리를 추가하여 오류를 최소화할 예정입니다.
참고 페이지
minnczi 블로그 - [Spring Boot] 한국수출입은행 Open API를 활용하여 환율 정보 가져오기
What’s New in Spring Batch 5.0
개척 라이프 - Webclient 파라미터 인코딩 하는 법
[WebFlux] WebClient를 사용하여 외부 API를 호출할 땐 인코딩을 주의해야 한다
'개발 > Spring Batch' 카테고리의 다른 글
[Spring Batch] 환율 정보 SMS API 로 문자 보내기 (2023.10.18 이후 불가) - 예제편 (2) | 2023.12.02 |
---|---|
[Spring Batch] 환율 정보 API 를 간단한 배치 스케줄러 추가 - 예제편 (2) | 2023.11.24 |
[Spring Batch] 대용량 데이터 처리를 위해 Batch 를 알아보자 - 이론편 (0) | 2023.11.15 |