Spring Rest Docs란?

Spring Rest Docs는 테스트 코드를 기반으로 자동으로 API 문서를 작성할 수 있게 도와주는 프레임워크입니다.

그렇기 때문에 반드시 Test가 통과되어야 문서가 작성 된다는 장점이 있습니다.

Test 통과과 전제조건이기 때문에 API Spec이 변경되거나 추가/삭제 되는 부분에 대해 항상 테스트 코드를 수정해야되고, API 문서가 최신화 될 수 있도록 해줍니다.

처음에는 마크다운이 저에게 익숙하기 때문에 마크다운으로 API 문서를 작성하려고 했지만, 설정을 하는 부분도 번잡하고, 대부분의 큰 서비스 회사에서도 asciidoc을 채택하는 것 같아서 asciidoc을 사용하여 문서를 작성하기로 했습니다. asciidoc은 마크다운과 비슷하게 html를 작성할 수 있는 언어입니다.

1. Spring Rest Docs 설정 방법

일단, 의존성 라이브러리로 JUnit, MockMvc, asciidoc을 선택하여 최소한의 라이브러리로 구현하였습니다.

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.2.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id "org.asciidoctor.convert" version "1.5.9.2" // (1)
}

group 'com.kakaopay'
version '1.0-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // (5)
}

test {
    useJUnitPlatform()
}

// (2)
asciidoctor {
    dependsOn test  
}

// (3)
bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}/html5") {
        into "BOOT-INF/classes/static/docs"
    }
}

// (4)
task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/asciidoc/html5")
    into file("src/main/resources/static/docs")
}

// (6)
build {
    dependsOn copyDocument
}

def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

dependsOn은 종속 테스크를 지정할 때 사용하는 기능입니다.

테스크 순서

  1. (1), (2)는 asciidoc 파일을 컨버팅하고 Build 디렉토리로 복사하기 위한 플러그인 입니다. asciidoctor Task를 통해 html 문서로 processing 되어 build/asciidoc/html5 하위에 html 문서로 생성이 됩니다.
  2. (3)은 gradle build 시에 asciidoc -> bootJar 순으로 수행이 되고, (4)은 실제 배포 시, BOOT-INF/classes가 classpath가 되기 때문에 아래와 같이 파일을 복사해야 합니다.
  3. (5)는 mockMvc를 restdocs에 사용할 수 있게 하는 라이브러리 입니다.
  4. (6)은 build 테스크를 수행하기 전에 소스 코드에 html파일을 복사하는 테스크 작업입니다.

2. RestDocs 예제 코드

ApiDocumentUtils 클래스

import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;

public interface ApiDocumentUtils {
    static OperationRequestPreprocessor getDocumentRequest() {
        return preprocessRequest(
                modifyUris() // (1)
                        .scheme("http")
                        .host("investment.api.com")
                        .port(8080),
                prettyPrint());
    }

    static OperationResponsePreprocessor getDocumentResponse() {
        return preprocessResponse(prettyPrint());
    }
}

위의 ApiDocumentUtils 클래스의 getDocumentRequest() 메서드는 문서상 uri를 기본 값인 http://localhost:8080에서 http://investment.api.com으로 변경하기 위해 사용합니다.

prettyPrint()는 문서의 request를 보기좋게 출력하기 위해 사용합니다.
getDocumentResponse() 메서드 역시 문서의 response를 보기 좋게 출력하는 용도로 사용합니다.

InvestmentRestControllerTests 클래스

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class InvestmentRestControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();

    @Autowired
    WebApplicationContext context;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

    @DisplayName("회원이 특정 투자상품에 투자 요청을 정상적으로 처리한다.")
    @Test
    public void 투자결과_리턴_테스트() throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("itemId", 1);
        jsonObject.put("investingAmount", 100000);
        String jsonBody = jsonObject.toString();

        ResultActions result = mockMvc.perform(post("/api/investment")
                .content(jsonBody)
                .header("X-USER-ID", 1)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON));

        result.andExpect(status().isOk())
                .andExpect(jsonPath("$.result", is("success")))
                .andDo(document("investment",
                        getDocumentRequest(),
                        getDocumentResponse(),
                        requestFields(
                                fieldWithPath("itemId").type(JsonFieldType.NUMBER).description("투자상품"),
                                fieldWithPath("investingAmount").type(JsonFieldType.NUMBER).description("투자 금액")
                        ),
                        responseFields(
                                fieldWithPath("result").type(JsonFieldType.STRING).description("투자 결과")
                        )
                ));
    }
}

기본적으로 MockMvc 기반으로 테스트를 하기 전에 setUp()에서 두 가지 방식으로 진행해야 하는데, 저는 webAppContextSetup을 이용하여 WebApplicationContext의 인스턴스로 작동하도록 셋업을 하였습니다. 이렇게 하면 스프링 컨트롤러는 물론 의존성까지 로드되기 때문에 완전한 통합 테스트를 할 수 있습니다.

나머지 하나는 아래 코드처럼 standaloneSetup()을 사용하여 한 컨트롤러에 집중하여 테스트 하는 용도로만 사용한다는 점에서 유닛 테스트와 유사합니다.

this.mockMvc = MockMvcBuilders
            .standaloneSetup
            .apply(documentationConfiguration(restDocumentation))
            .build()

JUnitRestDocumentation는 JUnit 프레임워크로 RestDocs를 실행하기 때문에 필요한 객체 입니다.

requestFields, responseFields 메서드는 사용자 정의 API 스펙에 따라서 요청 필드, 응답 필드를 정의할 수 있습니다.

requestFields(fieldWithPath), responseFields(fieldWithPath())는 request-fields asciidoc, response-fields asciidoc 파일들을 생성 합니다.

위 테스트 코드가 성공하면 build/generated-snippets/investment 디렉토리 하위에 adoc 파일이 생성되는 것을 확인할 수 있습니다.

image

3. Snippet 문서

RestDocs에 대한 명세내역을 코드에 작성한 후 테스트 케이스가 정상적으로 실행이 되었다면 RestDocs의 내용을 기준으로 Snippet 문서가 생성되는데, Gradle 기준으로 build/generated-snippets 디렉토리 하위에 생성이 됩니다.

이 Snippet이란 문서의 조각을 의미합니다. Gradle 빌드 스크립트에서 asciidoctor로 직접 작성한 문서와 Spring MVC 테스트를 통해 자동 생성된 스니펫을 결합해서 웹 문서를 만듭니다.`

4. Snippet 종류

  • curl-request.adoc : 호출에 대한 curl 명령을 포함 하는 문서
  • httpie-request.adoc : 호출에 대한 http 명령을 포함 하는 문서
  • http-request.adoc : http 요청 정보 문서
  • http-response.adoc : http 응답 정보 문서
  • request-body.adoc : 전송된 http 요청 본문 문서
  • response-body.adoc : 반환된 http 응답 본문 문서
  • request-parameters.adoc : 호출에 parameter 에 대한 문서
  • path-parameters.adoc : http 요청시 url 에 포함되는 path - parameter 에 대한 문서
  • request-fields.adoc : http 요청 object 에 대한 문서
  • response-fields.adoc : http 응답 object 에 대한 문서

이중에서 RestDocs 문서를 만들기 위해 사용될때 가장 우선순위가 높은 문서는 (curl-request.adoc, request-parameters.adoc, path-parameters.adoc, request-fields.adoc, response-fields.adoc) 가 되는데 이 외의 스니펫 문서는 필요에 따라 선택적으로 포함 시켜 줍니다.

5. asciidoc html 파일 작성

이제 Snippet 문서를 html 문서로 생성하기 위해 src/docs/asciidoc/api-guide.adoc 문서를 작성하면 됩니다.

image

참고로 Gradle에서는 Maven이랑 다르게 src/docs/asciidoc 디렉토리 하위에 adoc 문서를 생성해야 합니다. 그렇지 않으면 에러가 발생합니다.

ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
:doctype: investment
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response

[[resources]]
= Resources

[[resources-investment]]
== Investment

[[resources-investment]]
=== 정보 가져 오기
include::{snippets}/investment/http-request.adoc[]
include::{snippets}/investment/http-response.adoc[]
include::{snippets}/investment/response-fields.adoc[]
include::{snippets}/investment/request-fields.adoc[]

저 같은 경우에는 외부 API를 사용하기 위해서 정말 필요하다고 생각되는 snippet만 adoc 파일에 포함시켰습니다. include::~ 이 부분이 실제 html 태그안에 삽입하는 부분입니다.

이제 Gradle로 build 테스크를 수행하면 위 스크립트에서 작성한 static/docs/api-guide.html 파일이 생성되는 것을 확인할 수 있고, http://localhost:8080/docs/api-guide.html로 접근이 가능하면 아래와 같은 웹 문서가 렌더링 됩니다.

실행 결과

스크린샷 2021-04-12 오후 3 21 27

참조 사이트: https://jogeum.net/16, https://jaehun2841.github.io/2019/08/04/2019-08-04-spring-rest-docs/#userkt, 우아한 형제 기술 블로그

'SpringFramework > Spring Boot' 카테고리의 다른 글

Spring Boot + Spring Data Redis 연동  (0) 2022.07.10
멀티 모듈에서 QueryDSL 설정 방법  (0) 2021.05.11
HandlerMethodArgumentResolver란?  (0) 2019.12.16
인터셉터란?  (0) 2019.12.07
JWT 토큰 기반 인증  (0) 2019.12.06

멀티 모듈에서 QueryDSL 설정 방법

최근에 Spring Boot를 사용하여 멀티 모듈을 구성하였고, 하위 모듈별로 꼭 필요한 의존성만 설정하도록 했습니다.

기술 스펙은 아래와 같습니다.

  • Spring Boot 2.3
  • Gradle 6.3

1. 멀티모듈 구성 화면

image

core 모듈은 여러 하위 모듈에서 공통으로 사용하는 도메인 계층을 관리합니다.

settings.gradle

gradle이 빌드 과정에서 settings.gradle 파일을 참조하기 때문에 반드시 root 모듈과 sub 모듈을 include 해야합니다.

settings.gradle 파일

rootProject.name = 'yolo'
include 'yolo-core'
include 'yolo-api'

2. QueryDSL 설정 방법

저같은 경우에는 core 모듈에서 도메인 영역인 Entity, Repository를 가지고 있기 때문에 core 모듈에서 QueryDSL 플러그인과 의존성을 가지고 있도록 설정했습니다.

core - build.script

plugins {
    // (1) QueryDSL 플러그인 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

bootJar {
    enabled = false
}

jar {
    enabled = true
}

dependencies {
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
    // (2) QueryDSL 의존성 추가
    compile('com.querydsl:querydsl-jpa') 
}

// (3) QClass 생성 위치
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

// (4) QClass 소스 위치를 지정합니다.
sourceSets {
    main.java.srcDir querydslDir
}
// (5) gradle 5.0 설정
configurations {
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

(1) 플러그인을 사용할 수 있도록 설정합니다.
(2) QueryDSL 의존성 라이브러리를 추가합니다.
(3) QueryDSL를 사용하기 위해서는 QClass라는 도메인의 meta model이 필요한데 QClass를 생성하고 빌드 후 generaged/querydsl 디렉토리 하위로 파일이 생성됩니다.

아래 코드는 QueryDSL 플러그인 저장소 url를 repositories에 선언하여 의존성을 받아오도록 설정한 코드입니다.

repositories {
    mavenCentral()
    maven {
        url "https://plugins.gradle.org/m2/"
    }
}

dependency 추가

만약 멀티 모듈을 사용할 경우, 저처럼 core 상위 모듈에서 QueryDSL을 사용할 경우 테스트 코드는 api 하위 모듈에서 작성하게 됩니다. 그러면 테스트 코드에서 JPAQueryFactory를 주입받아서 repository 계층에 대해서 테스트를 진행하게 되는데, 이 경우에 테스트는 문제없이 성공하지만 build를 실행할 경우 JPAQueryFactory does not exist라는 에러 문구가 나오면서 build가 실패합니다.

여기서 많은 삽질을 하였였는데, 실패 사유는 아래와 같이 build.script에서 implementation으로 선언해서 발생한 문제였습니다. implementation으로 의존성이 등록되면, 하위 프로젝트들은 해당 의존성을 가질 수 없습니다.

즉, 아래와 같이 api 하위 모듈이 core 상위 모듈을 의존하고 있다면 api는 implementation으로 선언된 com.querydsl:querydsl-jpa을 사용할 수가 없습니다. 이렇게 함으로써 멀티 모듈에서의 의존관계에 개방/폐쇄를 적용할 수가 있습니다.

core 모듈 build.script 수정 전

implementation('com.querydsl:querydsl-jpa') 

그래서 저는 아래와 같이 api 모듈에서도 사용할 수 있도록 compile로 변경했습니다.

core 모듈 build.script 수정 후

compile('com.querydsl:querydsl-jpa') 

3. QueryDSL 상세 설정

jpa

아래 설정에서 jpa는 QClass를 자동으로 생성할지 결정합니다.
기본값은 false 입니다.

만약, true일 경우 com.querydsl.apt.jpa.JPAAnnotationProcessor가 추가되면서 프로젝트에 사용됩니다.

JPAAnnotationProcessor는 meta model(QClass)를 생성합니다.

false일 경우 build 타임에 QCalss들이 생성되지 않게 됩니다.

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

querydslSourceDir

  • 어디에 QClass를 생성할지 설정합니다.
  • 기본값은 src/querydsl/java 입니다.
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

sourceSet

sourceSets {
    main.java.srcDir querydslDir
}
  • QClass가 생성된 위치를 나타냅니다.
  • 위에서 선언한 querydslSourceDir가 위치가 같아야 합니다.
  • 기본적으로 자바 소스와 리소스 파일의 그룹을 나타냅니다.
  • 기본적으로 java 플러그인은 src/main/javasrc/test/java를 기본 소스 디렉토리로 인식합니다.
  • main.java.srcDirbuild/generated/querydsl이 되게 됩니다.
  • 나중에 User라는 도메인을 생성하면 해당 위치에 QUser라는 QueryDSL용 도메인이 생성되고 사용하게 됩니다.

4. gradle 5.0 설정

configurations {
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

gradle 5+ 버전을 사용할 경우 위와 같은 설정 값을 추가해야 합니다.

configuration

  • QueryDSL의 컴파일된 클래스 패스 경로를 지정합니다.

compileQuerydsl

  • querydsl-apt의 annotation processor 경로를 지정합니다.
  • gradle 5 버전에서는 자체 annotation processor를 사용합니다
  • querydsl-apt의 annotation processor와 충돌

'SpringFramework > Spring Boot' 카테고리의 다른 글

Spring Boot + Spring Data Redis 연동  (0) 2022.07.10
Spring RestDocs  (0) 2021.05.11
HandlerMethodArgumentResolver란?  (0) 2019.12.16
인터셉터란?  (0) 2019.12.07
JWT 토큰 기반 인증  (0) 2019.12.06

스프링 부트 배치 입문을 위한 용어정리

스프링 부트 배치는 대용량 데이터를 처리하는 기술로만 알고 있어서, 이번 기회에 한번 개념만 살펴보았습니다.

스프링 부트 배치를 왜 사용하는지 장점부터 살펴보았습니다.

  • 대용량 데이터 처리에 최적화되어 고성능을 발휘합니다.
  • 효과적인 로깅, 통계 처리, 트랜잭션 관리 등 재사용 가능한 필수 기능을 지원합니다.
  • 수동으로 처리하지 않도록 자동화 되었습니다.
  • 예외 사항과 비정상 동작에 대한 방어 기능이 있습니다.
  • 스프링 부트 배치의 반복되는 작업 프로세스를 이해하면 비즈니스 로직에 집중할 수 있습니다.

일반적으로 스프링 부트 배치의 절차는 읽기 -> 처리 -> 쓰기를 따릅니다.

  1. 읽기(read): 데이터 저장소(일반적으로 데이터 베이스)에서 특정 데이터 레코드를 읽습니다.
  2. 처리(processing): 원하는 방식으로 데이터를 가공/처리합니다.
  3. 쓰기(write): 수정된 데이터를 다시 저장소(데이터베이스)에 저장합니다. 혹은 외부 API를 통해 내보내기도 합니다.

아래 그림은 배치 처리와 관련된 객체의 관계입니다.

Untitled Diagram (1)

Job과 Step은 1:M, Step과 ItemReader, ItemProcessor, ItemWriter는 1:1의 관계를 가집니다. 즉, Job이라는 하나의 큰 일감(Job)에 여러 단계(Step)을 두고, 각 단계를 배치의 기본 흐름대로 구현합니다.

스프링 부트 배치 용어들에 대해서 간단하게 개념정리를 해봤습니다.

1. Job

Job은 배치 처리 과정을 하나의 단위로 만들어 표현한 객체입니다. 전체 배치 처리에 있어 항상 최상단 계층에 있습니다. 스프링 배치에서 Job 객체는 여러 Step 인스턴스를 포함하는 컨테이너 입니다.

@Slf4j
@RequiredArgsConstructor
@Configuration
public class SimpleJobConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        // simpleJob이라는 이름을 가진 Job을 생성할 수 있는 JobBuilder 인스턴스 반환
        return jobBuilderFactory.get("simpleJob")
                // simpleJobBuilder 인스턴스 반환
                .start(simpleStep1())
                // simpleJob이라는 이름을 가진 Job 인스턴스 반환 
                .build(); 
    }
}

보통 배치 Job 객체를 만드는 빌더는 여러 개 있습니다. 여러 빌더를 통합 처리하는 공장인 JobBuiderFactory 객체로 원하는 Job을 손쉽게 만들 수 있습니다. JobBuilderFactory의 get() 메서드를 호출하여 JobBuilder를 생성하고 이를 이용합니다.

JobBuilder

JobBuildr의 메서드들을 까보면 모든 반환 타입이 빌더입니다. 아무래도 비즈니스 환경에 따라서 Job 생성 방법이 모두 다르기 때문에 별도의 구체적인 빌더를 구현하고 이를 통해 Job 생성이 이루어지게 하려는 의도가 아닌가 싶습니다.

JobInstance

JobInstance는 배치에서 Job이 실행될 때 하나의 Job 실행 단위입니다. 만약 하루에 한 번씩 배치의 Job이 실행된다면 어제와 오늘 실행한 각각의 Job을 JobInstance라고 부를 수 있습니다.

만약 JobInstance가 Job 실행이 실패하면 JobInstance가 끝난 것이 아닙니다. 이 경우 JobExecution이 실패 정보를 가지고 있고, 성공하면 JobInstance는 끝난 것으로 간주합니다. 그리고 성공한 JobExecution을 가져서 총 두개를 가지게 됩니다. 여기서 JobInstance와 JobExecution은 부모와 자식관계로 생각하면 됩니다.

JobParameters

JobParameters는 Job이 실행될 때 필요한 파라미터들을 Map 타입으로 저장하는 객체입니다.
JobParameters는 JobInstance를 구분하는 기준이 되기도 합니다. 예를 들어서 Job 하나를 생성할 때 시작 시간 등의 정보를 파리미터로 해서 하나의 JobInstance를 생성합니다. 즉 JobInstance와 JobParameters는 1:1 관계입니다. 파라미터 타입으로는 String, Long, Date, Double를 사용할 수 있습니다.

StepExecution

Job에 JobExecution이라는 Job 실행 정보가 있따면 Step에는 StepExecution이라는 Step 실행 정보를 담는 객체가 있습니다. 각각의 Step이 실행될 때마다 StepExecution이 생성됩니다.

JobRepository

JobRepository는 배치 처리 정보를 담고 있는 메커니즘입니다. 어떤 Job이 실행되었으며 몇 번 실행되었고 언제 끝났는지 등 배치 처리에 대한 메타 데이터를 저장합니다.

예를 들어 Job 하나가 실행되면 JobRepository에서는 배치 실행에 관련된 정보를 담고 있는 도메인인 JobExecution을 생성합니다.

JobRepository는 Step의 실행 정보를 담고 있는 StepExecution도 저장소에 저장하며 전체 메타데이터를 저장/관리하는 역할을 수행합니다.

JobLauncher

JobLauncher는 Job, JobParameters와 함께 배치를 실행하는 인터페이스입니다. 인터페이스의 메서드는 run() 메서드 하나입니다.

public interface JobLauncher {
    public JobExecution run(Job job, JobParameters jobParameters) throw ...
}

run() 메서드는 매개변수로 Job과 JobParameters를 받아 JobExecution을 반환합니다. 만약 매개변수가 이전과 동일하면서 이전에 JobExecution이 중단된 적이 있다면 동일한 JobExecution을 반환합니다.

ItemReader

ItemReader는 Step의 대상이 되는 배치 데이터를 읽어오는 인터페이스입니다. FILE, XML, DB 등 여러 타입의 데이터를 읽어올 수 있습니다.

public interface ItemReader<T> {
    T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;
}

ItemReader에서 read() 메서드의 반환 타입을 제네릭으로 구현했기 때문에 직접 타입을 지정할 수 있습니다.

ItemProcessor

ItemProcessor는 ItemReader로 읽어온 배치 데이터를 변환하는 역할을 수행합니다.

ItemProcessor가 존재하는 이유는 비즈니스 로직을 분리하기 위해서입니다. ItemWriter는 정말 저장만 수행하고, ItemProcessor 로직 처리만 수행해 역할을 명확하게 분리하여 유지보수성을 용이하게 합니다. 또 다른 이유는 읽어온 배치 데이터와 쓰여질 데이터 타입이 다를 경우 대응하기 위해서입니다. 명확한 인풋과 아웃풋을 ItemProcessor로 구현해놓는다면 더 직관적인 코드가 됩니다.

public interface ItemProcessor<I, O> {
    O process(I item) throws Exception;
}

ItemWriter

ItemWriter는 배치 데이터를 저장합니다. 일반적으로 DB나 파일에 저장합니다.

public interface ItemWriter<T> {
    public write<(List<? extends T> items) throws Exception;
}

ItemWriter와 ItemReader와 비슷한 방식으로 구현하면 됩니다. 제네릭으로 원하는 타입을 받습니다. write() 메서드는 List 자료구조를 사용해 지정한 타입이 리스트를 매개변수로 받습니다. 리스트 데이터 수는 설정한 청크 단위로 불러옵니다. write() 메서드의 반환 값은 따로 없고 매개 변수로 받은 데이터를 저장하는 로직을 구현하면 됩니다.

이제 다음편에는 실제로 어떻게 스프링 부트 배치를 사용하는지 예제코드를 소개하겠습니다.

+ Recent posts