개발/Spring Batch

[Spring Batch] 환율 정보 Open API를 활용한 간단한 배치 만들기 - 예제편

seopport 2023. 11. 23. 00:12
728x90
반응형

들어가기에 앞서

이전 블로그 글에 아래와 같은 글을 남겼습니다.

 

2023.11.15 - [개발/Spring & SpringBoot] - [Spring Boot] 대용량 데이터 처리를 위해 Batch를 알아보자 - 이론 편

 

[Spring Boot] 대용량 데이터 처리를 위해 Batch 를 알아보자 - 이론편

들어가기 앞서, 스프링 부트는 실무에서 사용할 수밖에 없는 기술 중 하나입니다. 계정계 업무에서는 배치를 스케쥴러와 함께 사용하여 지정된 시간에 수행할 수 있습니다. 예를 들어 재무 파트

seodeveloper.tistory.com

 

이론 편에서 실전 편으로 넘어가기 전에 배치에 대해서 알아보는 시간을 가지면 좋을 것 같아서, 한국수출입은행의 환율정보 Open API를 활용해서 간단한 배치 예제를 만들어 보았습니다. 여러 블로그 및 ChatGPT 를 활용하여 제작하였고, 스프링 배치 버전이 변경되면서 달라진 부분들을 공부하면서 만들었기 때문에 틀린 부분이 있다면 언제든지 말씀 부탁드립니다.

 

처음 사용하시는 분들도 이해하기 쉽도록 최대한 자세하게 작성하였습니다. 내용이 길어 천천히 보시면 좋을 것 같습니다.

 

Spring Batch Logo
Spring Batch Logo

 

한국수출입은행 Open API 발급 방법

Open API는 개발된 공공데이터를 누구나 사용할 수 있도록 공개된 API(Application Program Interface)를 말합니다.

 

한국수출입은행 OPEN API

 

한국수출입은행

페이지 정보가 없습니다. 요청하신 페이지를 찾을 수 없거나, 서버에서 삭제되었습니다. URL을 확인해주세요.

www.koreaexim.go.kr

 

Open API 제공목록

한국수출입은행은 국제금리 API, 대출금리 API, 현재환율 API 를 제공하고 있습니다. 이 글에서는 현재환율 API를 사용하여 예제 코드를 작성해 보았습니다.

Open API 제공목록
Open API 제공목록

 

Open API 개발명세

1. 요청 URL (Request URL) + 요청변수

요청 URL (Request URL) + authkey (인증키) + searchdate (검색요청날짜) + data (검색요청 API타입)

 

요청 URL (Request URL) + 요청변수
요청 URL (Request URL) + 요청변수

 

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 (서울외국환중개 장부가격)

 

출력결과 (Response Element)
출력결과 (Response Element)

 

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

 

GitHub - SeoYounSeok/exchangeBatch: Spring Batch Project (Spring Batch, Open API)

Spring Batch Project (Spring Batch, Open API). Contribute to SeoYounSeok/exchangeBatch development by creating an account on GitHub.

github.com

 

추가 개발 예정 사항

스케줄러(Scheduler)를 추가하여 특정한 시간에 등록된 작업을 진행하도록 할 예정입니다.

예) Spring Scehduler, Quartz

 

Logback을 활용하여 Log를 남기고, 예외처리를 추가할 예정입니다. 현재 코드는 System.out.println을 사용하여 출력을 해주고 있습니다. 해당 부분을 수정하여 Log로 남기고, 예외처리를 추가하여 오류를 최소화할 예정입니다. 

 

 

참고 페이지

minnczi 블로그 - [Spring Boot] 한국수출입은행 Open API를 활용하여 환율 정보 가져오기

 

[Spring Boot] 한국수출입은행 Open API를 활용하여 환율 정보 가져오기

import java.text.NumberFormat;import java.text.SimpleDateFormat;import java.util.Date;import java.util.Locale;public class ExchangeRateUtils {}

velog.io

 

What’s New in Spring Batch 5.0

 

What’s New in Spring Batch 5.0

Spring Batch 5.0 has the following major themes: Java 17 Requirement Major dependencies upgrade Batch infrastructure configuration updates Batch testing configuration updates Job parameters handling updates Execution context serialization updates SystemCom

docs.spring.io

 

탁 블로그 - util 클래스의 역할

 

util 클래스의 역할

이번 SNS project에서 controller, domain, repository, util로 패키지를 나누었습니다. 보통 보안, 문자열처리, 날짜 처리 등등 특정 비즈니스 로직과 독립적인 기능들은 util 패키지에 넣고 XXXUtil 클래스로 만

soranta.tistory.com

 

개척 라이프 - Webclient 파라미터 인코딩 하는 법

 

[Spring Boot] WebClient 파라미터 인코딩 하는법

WebClient를 사용해서 그냥 호출하게 되면 인코딩을 하지 않아 API 키가 달라지는 경우가 생길수가 있다. 나같은 경우에 그 문제 때문에 골머리를 앓았는데 아래와 같은 방법으로 해결했다. 일단 Uri

wpioneer.tistory.com

 

 

[WebFlux] WebClient를 사용하여 외부 API를 호출할 땐 인코딩을 주의해야 한다

 

[WebFlux] WebClient를 사용하여 외부 API를 호출할 땐 인코딩을 주의해야 한다

문제의 배경 프로젝트 진행 중에 입력값의 유효성을 검사하기 위해 외부 API를 호출할 일이 있었다. 그런데 포스트맨이나 크롬 개발자 도구에서는 문제 없이 잘 호출되는 API가 막상 WebClient를 사

colabear754.tistory.com

 

728x90
반응형