글 작성자: 개발섭

안녕하세요. 2021년도 연말에 회사에서 사내 배치 프로그램을 만들일이 있어서 어떤 툴을 쓸지 고민하던 도중 Spring 공부에도 큰 도움이 되지 않을까 싶어서 Spring Batch를 공부하기 시작했었습니다.

실제로 Spring Batch에 대해서 공부하는데 큰 도움을 줄만한 게 뭐가 있을까 고민해보았는데요. 아무래도 가장 큰 도움을 줄만한 건 딱봐도 토이(파일럿) 프로젝트 같았습니다.

그래서 어떤 프로젝트를 하는게 좋을지 큰 고민을 했었는데 Spring Batch 관련한 토이프로젝트를 만들어주셨던 한 블로그에 글에 큰 도움을 받아 형식은 좀 다르지만, 여러 가지 Batch에 대해서 구현을 해보았습니다.

Spring Batch를 공부하시는 분들이라면, 어느 정도의 절차를 바탕으로 진행하면서 점진적으로 내 프로그램의 질을 늘려가는 방향으로 개발을 진행해보는 것을 추천한다.

즉, 이번에 작성하는 포스트는 대부분 내가 개발을 하면서 구현해왔었던 순서대로 진행하려고 한다는 점을 알고 개발을 참고해보는걸 추천한다.

도대체 뭘 만들어볼까?

Spring Batch라는 특성인 배치 작업 즉, 일괄 작업이라는 프로세스상 분명히 많은 데이터가 필요했었다. 그리고 프로젝트 특성상 요구사항을 만족시키기 위한 조건도 존재했었다. 중요 쟁점을 요약해보자면 다음과 같다.

  • 첫째, 데이터를 많이 담을 수 있는 프로젝트
  • 두번째, API를 통해서 Data를 받고 그걸 DB에 삽입

그러던 도중에 아래 블로그에서 영화 API를 통해서 Batch 프로세스를 통해서 데이터를 삽입하는 프로젝트를 진행한 글을 보고 영진위 API를 통해서 data양을 더 늘릴 수 있는 프로젝트를 해보겠다는 생각을 하게된다.

 

 

<Spring Batch> API 호출해서 DB에 저장하기(with JPA)

2021/02/25 - [개발공부/Spring ] - 예제 프로그램 따라하기 예제 프로그램 따라하기" data-og-description="스케줄러 형태로 되어있는 배치는 많이 봤는데 Spring Batch를 사용한 배치 프로그램은 본적이 없는

willbfine.tistory.com

그리고 영진위 API를 가서 이 영진위 API에서 적당히 많은 데이터를 가질만한게 도대체 뭘까라는 생각을 하니..

영진위에서 API key를 신청하는 것은 회원가입후 키 신청을 통해서 이루어진다. 그 키를 바탕으로 위의 API URL에 자신이 받은 키를 입력해서 Query Parameter를 입력해서 작동하는 방식으로 구성되어있었다.

 

영화사 목록은 특히 괜찮았던 점이 있는데, 결과 Row값과 현재 페이지까지 제공을하는 것을 보아 아무래도 정말 많은 양의 Data를 가지고 있을거라고 생각했다. 그리고 실제로 API를 통해서 가져올때 Paging되어있는 데이터를 가지고 올 일 역시 존재했었으며.. ㅋㅋ; 꽤나 중요한 역할을 맡아줄 거라고 생각했다.

그래서 이 API를 기반으로 Spring Batch 프로젝트를 진행했었다.

프로젝트 구성

plugins {
    id 'org.springframework.boot' version '2.5.7'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.ventulus95'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
}

test {
    useJUnitPlatform()
}

일단 java 버전은 가장 최신을 했었는데 새롭게 도입된 여러가지 기능들을 써볼려고 했었던 차원이므로, 최소 버전만 지켜서 진행하면 좋을 것 같다.

그외에 특이점에 대해서 간단하게 이야기 하자면...

  1. JPA로 DB 삽입을 해보려고 했었다. JPA에 대해서 경험쌓기가 중요해서 무조건 JPA 익히는데 프로젝트를 모두 만들 었었다.
  2. Quartz의 경우는 차후에 설명하겠지만. CronJob과 관련된 일정 시간마다의 스케쥴링 되는 배치 프로세스를 구성하기 위해서 삽입했었다.
  3. API를 가져오는 방법은 RestTemplate를 사용하는 방법이 있었지만 사용하지 않고 webFlux를 통해서 삽입했다.
  4. H2 InMemoryDB를 통해서 DB 구현에 드는 비용을 최소화 했다.

API를 어떻게 가져올것인가? 💫

Spring에서는 RestTemplate를 통해서도 인터넷에서 API를 가져올 수 있긴하다. 하지만, WebFlux를 왜 썼는가? Webclient를 사용하도록 구현하도록 추천하기도 했어서, 후다닥 적용했었다.

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webclient(){
        return WebClient
                .builder()
                .clientConnector(new ReactorClientHttpConnector(
                        HttpClient.create().secure(t->{
                            try {
                                t.sslContext(SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build());
                            } catch (SSLException e) {
                                e.printStackTrace();
                            }
                        })
                ))
                .baseUrl("https://www.kobis.or.kr/kobisopenapi/webservice/rest/")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

여러가지 모드가 존재하긴 했었으나, https를 사용했었어서 다음과 같은 설정을 세팅했다.

Batch 프로세스

배치 프로세스는 물론 어느정도의 이해가 필요한 부분이긴해서 그 설명을 초입부터 설명하는 것이 좋다. 하지만, 나의 이해도와 나보다 훨씬 더 자세한 설명으로 알 수 있는 블로그는 정말 차고 차고 넘쳤기 때문에 구체적인 설명 하지 않겠지만, 간단하게 프로세스에 대해서 짚고 넘어가보도록 하자.

Spring Batch에서의 한 작업의 단위는 대략 다음과 같은 구성으로 조합된다.

한개의 Job단위로 한개의 작업으로 구성되고 그 한번의 작업은 여러개의 Step으로 구성될수 있다.

쉽게 이야기하자면, DB 추출 -> 월 정산 금액 추출 -> 미납 회원 정지 프로세스 와 같이 여러가지 단계로 작업이 진행되는 배치작업이 있다고 치자.

그러면 이 3개의 각각의 작업은 Step으로 구성되고, 이 Step으로 구성된 여러가지 단계 DB 추출, 월정산 금액 추출과 같은 여러 일을 한개의 잡에서 몇개의 단계로 나눠서 일을 처리할 수 있게 한다.

그리고 또한 Step도 3가지 단계를 거쳐서 구성할 수 있다.

대부분의 배치 작업들은 많은 양의 Data를 받아서, 일정 작업을 하고 -> 우리 서버에서 어디로든(DB, 파일) 삽입하면서,

  • ItemReader -> 일단 특정 DataSet에서 데이터를 읽어오는 역할을 한다.
  • ItemProcessor -> 읽어온 data를 변환한다.
  • ItemWriter -> 변환된 data를 삽입한다.

이렇게 몇몇 조합을 섞어서 내가 원하는 방향으로 배치 프로세스를 구성할 수 있다.

그렇다면 영화 API Batch를 구성해보자.

import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.JpaItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.web.reactive.function.client.WebClient;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import java.util.ArrayList;
import java.util.List;

@Configuration
@RequiredArgsConstructor
public class MovieConfig {

    private static final Logger logger = LoggerFactory.getLogger(MovieConfig.class);
    private final MovieCmmRepository movieCmmRepository;
    private final WebClient client;
    private final EntityManagerFactory managerFactory;

    @Bean //Job
    public Job movieScrappingJob(JobBuilderFactory jobBuilderFactory, Step movieScrappingStep){
        return jobBuilderFactory.get("movieScrappingJob")
                .preventRestart()
                .start(movieScrappingStep)
                .build();
    }

    @Bean //Step
    public Step movieScrappingJobStep(StepBuilderFactory stepBuilderFactory){
        return stepBuilderFactory.get("movieScrapStep").<MovieCompany, MovieCmm>chunk(30)
                .reader(movieScrapper())
                .processor(processor())
                .writer(insert())
                .build();
    }

    @Bean
    @StepScope
    public ListItemReader<MovieCompany> movieScrapper(){
        logger.info("$$$$$$$$$$$ 배치 읽어오기 시작");
        int page = 1;
        List<MovieCompany> list = new ArrayList<>();
        for (;page<4; page++){
            ResponseEntity<kobisResponse> res =client.get().uri("/company/searchCompanyList.json?key=f5eef3421c602c6cb7ea224104795888&itemPerPage=10&curPage="+page)
                    .retrieve().toEntity(kobisResponse.class)
                    .block();
            list.addAll(res.getBody().companyListResult.getCompanyList());
        }
        return new ListItemReader<>(list);
    }

    public ItemProcessor<MovieCompany, MovieCmm> processor(){
        return item -> {
            logger.info("~~~~~~~~~~~~~~~ 배치 프로세스 진행중!!!");
            return item.toEntity();
        };
    }

    public JpaItemWriter<MovieCmm> insert(){
        JpaItemWriter<MovieCmm> writer = new JpaItemWriter<>();
        writer.setEntityManagerFactory(managerFactory);
        return writer;
    }
}

MovieCompany, MovieCmm, kobisResponse와 같은 클래스는 Entity, Dto, Response를 받아오기위한 프로세스를 구성했었다. 아래는 다음 코드들에 대해서 작성해보았다 접은글을 펴서 보면 될 것 같다. 

더보기

MovieCmm -> 실제로 회사명을 받기 위해 필요한 Entity

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;


import javax.persistence.Entity;
import javax.persistence.Id;


@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MovieCmm {


    @Id
    private Long id;


    private String name;


    private String partName;
}

MovieCompany -> DTO 파일

import com.ventulus95.springbatch.MovieCmm;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;


@Getter
@NoArgsConstructor
@ToString
public class MovieCompany {


    private long companyCd;
    private String companyNm;
    private String companyPartNames;


    public MovieCmm toEntity(){
        return MovieCmm.builder()
                .id(companyCd)
                .name(companyNm)
                .partName(companyPartNames)
                .build();
    }
}

kobisResponse  -> 영진위 사이트에서 받은 Json 정보를 받을 때 필요한 DTO

이 당시에는 일단 박스 오피스 정보를 통해서 가져온 것이므로 차후에 변경이 좀 있긴하다

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.List;

@Getter
@NoArgsConstructor
@ToString
public class kobisResponse {

    private BoxOfficeResult boxOfficeResult;
    public CompanyResult companyListResult;

    @Getter
    @ToString
    private static class BoxOfficeResult {
        private String boxofficeType;
        private String showRange;
        private List<BoxOfficeDto> dailyBoxOfficeList;
    }

    @Getter
    @ToString
    public static class CompanyResult {
        private int totCnt;
        private List<MovieCompany> companyList;
    }

}

 

movieScrapper, processor, insert 라는 reader, processor, writer을 구성해서 stepBuilderFactory를 통해서 Step을 구성하고 그 jobBuilderFactory을 통해 Job을 구성하는 방식으로 구성하게된다.

특이한 점을 하나 더 소개하자면, ListItemReader 의 형태가 현재는 상당히 조악한 상태로 구성되어있다. 이상하다고 느낀 사람들이 있을텐데, 본 글은 시리즈로 조악한 상태에서 점점 발전된 형태로 구성하는 글의 형태로 구성할 예정이니 참고 부탁한다.

 

이렇게 코드를 구성하게 되면, 배치가 작동하게 되는데... 이 배치 프로세스를 작동시키기 위해서는 옵션이 필요하다.

spring:
  batch:
    jdbc:
      initialize-schema: embedded

initialize-schema를 통해서 이 배치작업과 관련한 Batch DB를 구성해줘야한다. 즉, 이런 Batch DB를 미리 구성해서 그 Batch단계별 진행정도 이 작업의 실행이 성공했었는지 실패했었는지? 그리고 무슨 이유로 실패했었는지등을 Spring Batch가 자동으로 인지하고, 그것을 바탕으로 실패이력을 남겨준다.

즉, 이런 구성을 해줘야지 정상작동 하니까 세팅해주자!

일단 Quartz와 관련한 포스팅은 추가로 진행할 예정이긴하지만... 일단 이 부분은 제외하고 봐주셨으면 좋겠다. 

아래 링크는 Quartz까지 추가한 Batch 관련 프로세스를 Branch화한 레포지토리 링크를 남겨두겠다. 혹시 의문점이 있다면, 댓글로 남겨 주시면 질의 응답할 수 있을 것이라고 생각한다.

 

 

GitHub - ventulus95/SpringBatch

Contribute to ventulus95/SpringBatch development by creating an account on GitHub.

github.com

 

한 1~2주 간격으로 계속 글을 작성해보려고 하는데, 일정상 변경될 수 있을수도 있고, 이미 종료한 프로젝트라서 이해도가 좀 떨어졌을 수도 있어서 그 점은 양해 부탁드린다. 끝까지 글 읽어주셔서 감사하다!