6. ItemReaders and ItemWriters

모든 배치 처리는 대량의 데이터를 읽고, 어떤 타입의 계산 또는 변환을 수행하고, 그 결과를 기록하는 것이라 설명할 수 있다. 스프링 배치는 대량 읽기 및 쓰기를 수행하는 데 도움이 되는 세 가지 주요 인터페이스를 제공한다: 아이템리더(ItemReader), 아이템프로세서(ItemProcessor), 그리고 아이템라이터(ItemWriter).

6.1. ItemReader

간단한 개념이지만, 아이템리더(ItemReader)는 다양한 입력 타입의 데이터를 제공하는 수단이다. 가장 일반적인 예는 다음과 같다:

  • 플랫 파일(Flat File): 플랫 파일 아이템리더는 일반적으로 파일의 고정된 위치에 정의되거나 일부 특수 문자(예: 쉼표)로 구분된 데이터 필드가 있는 레코드를 설명하는 플랫 파일에서 데이터 라인을 읽는다.
  • XML: XML 아이템리더(ItemReader)는 객체 파싱, 매핑 및 유효성 검사에 사용되는 기술과는 독립적으로 XML을 처리한다. 입력 데이터는 XSD 스키마에 대한 XML 파일의 유효성 검사를 허용한다.
  • 데이터베이스(Database): 처리를 위해 객체에 매핑할 수 있는 리절트셋(resultset)을 반환하기 위해 데이터베이스 리소스에 접근한다. 기본 SQL 아이템리더 구현체는 로우매퍼(RowMapper)를 호출하여 객체를 반환하고, 재시작이 필요한 경우 현재 로우을 추적하고, 기본 통계를 저장하고, 나중에 설명할 몇 가지 트랜잭션 향상 기능을 제공한다.

더 많은 상황이 있지만, 이 장에서는 기본적인 상황에 중점을 둔다. 사용 가능한 모든 아이템리더(ItemReader) 구현체의 전체 목록은 부록 A에서 찾을 수 있다.

아이템리더(ItemReader)는 다음 인터페이스에 표시된 것처럼, 일반 입력 작업을 위한 기본 인터페이스이다:

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

read 메서드는 아이템리더(ItemReader)의 가장 필수적인 기능이다. 호출하면 하나의 아이템이 반환되거나 아이템이 더 이상 남아 있지 않으면 null이 반환된다. 아이템은 파일의 라인, 데이터베이스의 로우 또는 XML의 엘리먼트를 나타낼 수 있다. 일반적으로 사용 가능한 도메인 객체(예: Trade, Foo 또는 기타)에 매핑되는 것으로 예상되지만, 이 기능은 그럴 필요없다.

아이템리더(ItemReader) 인터페이스의 구현체는 다음 데이터를 읽는 기능만 가능할 것으로 예상된다. 그러나 리소스가 트랜잭션(예: JMS 대기열)인 경우 read를 호출하면 롤백 시나리오의 호출에서는 동일한 아이템이 반환될 수 있다. 아이템리더(ItemReader)가 처리할 아이템이 부족해도 예외가 발생하지 않는다는 점도 주목할만하다. 예를 들어, 0개의 결과를 반환하는 쿼리로 구성된 데이터베이스 아이템리더(ItemReader)는 read의 첫 번째 호출에서 null을 반환한다.

6.2. ItemWriter

아이템라이터(ItemWriter)는 기능면에서 아이템리더(ItemReader)와 유사하지만 반대 작업이다. 리소스는 여전히 찾고(located), 열고, 닫아야 하지만 아이템라이터(ItemWriter)가 읽는 것이 아니라 써낸다는 점에서 다르다. 출력의 직렬화(serialization) 포맷은 각 배치 잡에 따라 다르다.

아이템리더(ItemReader)와 마찬가지로 아이텝라이터(ItemWriter)는 다음 인터페이스 정의와 같이 상당히 일반적인 인터페이스이다:

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

아이템리더(ItemReader)에서 read와 마찬가지로 write는 아이템라이터(ItemWriter)의 기본 기능을 제공한다. 열려 있는 동안 전달된 아이템 리스트을 작성하려고 시도한다. 일반적으로 아이템이 청크 단위로 ‘배치’처리된 다음 출력되는 것으로 예상되기 때문에, 인터페이스는 아이템 단건이 아닌 아이템 리스트를 받는다. 리스트를 작성한 후, write 메서드가 반환하기 전에 필요할 경우 flush를 수행할 수 있다. 예를 들어, 하이버네이트 DAO에 작성하는 경우 각 아이템에 대해 하나씩 write가 여러번 호출될 수 있다. 그런 다음 라이터는 반환하기 전에 하이버네이트 세션에서 flush를 호출할 수 있다.

6.3. ItemStream

아이템리더(ItemReader)아이템라이터(ItemWriter)는 둘 다 개별 목적을 잘 수행하지만, 다른 인터페이스가 필요한 두 가지 상황이 있다. 일반적인 배치 작업의 일부로, 리더와 라이터를 열고 닫아야, 하며 상태를 유지하기 위한 메커니즘이 필요하다. 아이템스트림(ItemStream) 인터페이스는 다음 예제와 같이 해당 용도로 사용된다:

  public interface ItemStream {
    void open(ExecutionContext executionContext) throws ItemStreamException;
    void update(ExecutionContext executionContext) throws ItemStreamException;
    void close() throws ItemStreamException;
  }

각 메서드를 설명하기 전에, 익스큐션컨텍스트(ExecutionContext)를 언급해야 한다. 아이템스트림(ItemStream)을 구현하는 아이템리더(ItemReader)의 클라이언트는 파일과 같은 리소스를 열거나, read를 호출하기 전에 open을 호출해야 한다. 유사한 제한이 아이템스트림(ItemStream)을 구현하는 아이템라이터(ItemWriter)에도 적용된다. 2장에서 언급했듯이, 익스큐션컨텍스트(ExecutionContext)에서 기대했던 데이터가 발견되면, 초기 상태가 아닌 아이템리더(ItemReader) 또는 아이템라이터(ItemWriter)를 시작하는 데 사용될 수 있다. 반대로, open 중에 할당된 리소스가 안전하게 해제되도록 close를 호출한다. update는 주로 현재 보유 중인 모든 상태를 익스큐션컨텍스트(ExecutionContext)에 저장하기 위해 호출한다. 이 메서드는 커밋하기 전에, 현재 상태가 데이터베이스에 유지되도록 하기 위해 커밋하기 전에 호출된다.

아이템스트림(ItemStream)의 클라이언트가 (스프링 배치 코어에서) 스텝인 특수한 경우, 동일 잡인스턴스(JobInstance)가 다시 시작되면, 익스큐션컨텍스트(ExecutionContext)는 각 스텝익스큐션(StepExecution)에 대해 특정 실행 상태를 저장하며, 그것이 반환된다. 쿼츠(Quartz)에 익숙한 사용자의 경우 잡데이터맵(JobDataMap)과 매우 유사하다.

6.4. The Delegate Pattern and Registering with the Step

컴포지트아이템라이터(CompositeItemWriter)는 스프링 배치에서 일반적으로 사용되는 델리게이트(delegate) 패턴의 예시이다. 델리게이트 자체는 스텝리스너(StepListener)와 같은 콜백 인터페이스를 구현할 수 있다. 스텝이 스프링 배치 코어와 함께 사용되는 경우, 스텝에 수동 등록해야 한다. 스텝에 직접 연결된 리더(reader), 라이터(writer) 또는 프로세서(processor)는 아이템스트림(ItemStream) 또는 스텝리스너(StepListener) 인터페이스를 구현하는 경우 자동으로 등록된다. 그러나 델리게이트는 스텝에 알려지지 않았기 때문에 리스너 또는 스트림(또는 적절한 경우 둘 다)으로 주입되어야 한다.

다음 예제는 XML에서 델리게이트를 스트림으로 주입하는 방법을 보여준다: XML 구성

  <job id="ioSampleJob">
    <step name="step1">
      <tasklet>
        <chunk reader="fooReader" processor="fooProcessor" writer="compositeItemWriter" commit-interval="2">
          <streams>
            <stream ref="barWriter" />
          </streams>
        </chunk>
      </tasklet>
    </step>
  </job>

  <bean id="compositeItemWriter" class="...CustomCompositeItemWriter">
    <property name="delegate" ref="barWriter" />
  </bean>

  <bean id="barWriter" class="...BarWriter" />

다음 예제는 자바에서 델리게이트를 주입하는 방법을 보여준다: 자바 구성

  @Bean
  public Job ioSampleJob(JobRepository jobRepository) {
    return new JobBuilder("ioSampleJob", jobRepository)
            .start(step1())
            .build();
  }

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilder("step1", jobRepository)
                .<String, String>chunk(2, transactionManager)
                .reader(fooReader())
                .processor(fooProcessor())
                .writer(compositeItemWriter())
                .stream(barWriter())
                .build();
  }
  @Bean
  public CustomCompositeItemWriter compositeItemWriter() {
      CustomCompositeItemWriter writer = new CustomCompositeItemWriter();
      writer.setDelegate(barWriter());
      return writer;
  }

  @Bean
  public BarWriter barWriter() {
    return new BarWriter();
  }

6.5. Flat Files

대량 데이터를 교환하는 가장 일반적인 메커니즘 중 하나는 항상 플랫 파일이었다. 구조(XSD)을 정의하기 위한 합의된 표준이 있는 XML과 달리, 플랫 파일을 읽는 사람은 파일 구조를 정확히 미리 이해해야 한다. 일반적으로, 모든 플랫 파일은 구분된(delimited) 길이와 고정(fixed) 길이의 두 가지 타입으로 나뉜다. 구분된(Delimited) 파일은 필드가 쉼표와 같은 구분 기호로 구분된 파일이다. 고정(Fixed) 길이 파일에는 길이가 설정된 필드가 있다.

6.5.1. The FieldSet

스프링 배치에서 플랫 파일로 작업할 때, 입력용이든 출력용이든 상관없이 가장 중요한 클래스 중 하나는 필드셋(FieldSet)이다. 많은 아키텍처와 라이브러리에는 파일에서 읽는 데 도움이 되는 추상화가 포함되어 있지만, 일반적으로 스트링(String) 또는 스트링(String) 배열을 반환한다. 이제 절반 정도 알게 됐다. 필드셋(FieldSet)은 파일 리소스에서 필드 바인딩을 활성화하기 위한 스프링 배치의 추상화이다. 개발자가 데이터베이스 입력으로 작업하는 것과 거의 동일한 방식으로 파일 입력을 작업할 수 있다. 필드셋(FieldSet)은 개념적으로 JDBC 리절트셋(ResultSet)와 유사하다. 필드셋(FieldSet)에는 하나의 아규먼트(토큰의 문자열 배열)만 필요하다. 선택적으로, 예제와 같이 필드셋(FieldSet) 다음에 패턴화된 대로 인덱스 또는 이름으로 필드에 액세스할 수 있도록 필드 이름을 구성할 수도 있다.

  String[] tokens = new String[]{"foo", "1", "true"};
  FieldSet fs = new DefaultFieldSet(tokens);
  String name = fs.readString(0);
  int value = fs.readInt(1);
  boolean booleanValue = fs.readBoolean(2);

필드셋(FieldSet) 인터페이스에는 Date, long, BigDecimal 등과 같은 많은 옵션이 있다. 필드셋(FieldSet)의 가장 큰 장점은 플랫 파일 입력에 대한 일관된 파싱 방식을 제공한다는 것이다. 각 배치 잡이 잠재적으로 예상치 못한 방식으로 다르게 파싱하는 대신, 형식(format) 예외로 인한 오류를 처리하거나, 간단한 데이터 변환을 수행할 때 모두 일관성을 유지할 수 있다.

6.5.2. FlatFileItemReader

플랫 파일은 최대 2차원(표 형식) 데이터를 가진 모든 파일이다. 스프링 배치 프레임워크에서 플랫 파일 읽기는, 플랫 파일을 읽기 및 파싱 기능을 제공하는 플랫파일아이템리더(FlatFileItemReader) 클래스에 의해 작동한다. 플랫파일아이템리더(FlatFileItemReader)의 가장 중요한 두 가지 필수 의존성은 리소스(Resource)라인매퍼(LineMapper)이다. 라인매퍼(LineMapper) 인터페이스는 다음 장에서 자세히 살펴보자. 리소스 프로퍼티는 스프링 코어 리소스를 나타낸다. 이 타입의 빈을 생성하는 방법을 설명하는 문서는 스프링 프레임워크, 5장에서 찾을 수 있다. 따라서, 이 가이드에서는 다음과 같은 간단한 예제를 보여주는 것 이상으로 리소스(Resource) 객체 생성에 대해 자세히 다루지 않는다:

  Resource resource = new FileSystemResource("resources/trades.csv");

복잡한 배치 환경에서, 디렉토리 구성은, FTP에서 배치 처리로 또는 그 반대로 파일을 이동하기 위해 외부 인터페이스용 드롭 영역(drop zones)이 설정되는, EAI(Enterprise Application Integration) 인프라에 의해 관리되는 경우가 많다. 파일 이동 유틸리티는 스프링 배치 아키텍처의 범위를 벗어나지만, 배치 잡 스트림(job stream)이 파일 이동 유틸리티를 스텝으로 포함하는 것은 자주 있는 일이다. 배치 아키텍처는 처리할 파일을 찾는 방법만 알면 된다. 스프링 배치는 이 시작점에서 파이프로 데이터를 공급하는 프로세스를 시작한다. 그러나, 스프링 인테그레이션(Integration)은 이러한 타입의 서비스를 많이 제공한다.

플랫파일아이템리더(FlatFileItemReader)의 다른 프로퍼티스를 사용하면, 다음 테이블에 설명된 대로 데이터 해석 방법을 추가로 지정할 수 있다:

테이블 15. 플랫파일아이템리더(FlatFileItemReader) 프로퍼티스

PropertyTypeDescription
commentsString[]코멘트 로우를 나타내는 라인의 접두사를 지정한다.
encodingString사용할 텍스트 인코딩을 지정한다. 기본값은 UTF-8이다.
lineMapperLineMapper문자열을 아이템을 나타내는 객체로 변환한다.
linesToSkipint파일 상단부터 무시할 줄 수이다.
recordSeparatorPolicyRecordSeparatorPolicy줄 끝이 어딘지 확인하고 인용된 문자열 내부에 있는 경우 줄 끝에서 진행하는 것과 같은 작업을 수행한다.
resourceResource읽을 리소스이다.
skippedLinesCallbackLineCallbackHandler파일에서 건너뛸 라인의 로우(raw) 내용을 전달하는 인터페이스이다. linesToSkip이 2로 설정되면 이 인터페이스가 두 번 호출된다.
strictboolean스트릭트 모드(strict mode)에서, 리더(reader)는 입력 리소스가 존재하지 않는 경우 익스큐션컨텍스트(ExecutionContext)에서 예외를 발생시킨다. 그렇지 않으면, 문제를 기록하고 진행한다.

LineMapper

리절트셋(ResultSet)과 같은, 로우 레벨 구성을 사용하고 객체를 반환하는, 로우매퍼(RowMapper)와 마찬가지로 플랫 파일 처리에는 다음 인터페이스 정의에 표시된 것처럼, 스트링(String) 라인을 객체로 변환하는 구성이 필요하다:

 public interface LineMapper<T> {
    T mapLine(String line, int lineNumber) throws Exception;
  }

기본 기능은, 현재 라인과 연결된 라인 번호가 주어지면, 매퍼가 결과 도메인 객체(resulting domain object)를 반환해야 한다는 것이다. 이것은 리절트셋(ResultSet)의 각 라인이 라인 번호에 연결되어 있는 것처럼 각 라인이 해당 라인 번호와 연결된다는 점에서 로우매퍼(RowMapper)와 유사하다. 이를 통해 동일성(identity) 비교 또는 유익한 로깅을 위해 라인 번호를 결과 도메인 객체에 연결할 수 있다. 그러나, 위에서 설명한 것처럼, 로우매퍼(RowMapper)와 달리, 라인매퍼(LineMapper)에는 절반만 기능을 하는 원시 라인으로 제공된다. 라인은 이 문서의 뒷부분에 설명된 대로, 객체에 매핑될 수 있는 필드셋(FieldSet)으로 토큰화되어야 한다.

LineTokenizer

필드셋(FieldSet)으로 변환해야 하는 플랫 파일 데이터 형식이 많을 수 있으므로 입력 라인을 필드셋(FieldSet)으로 전환하기 위한 추상화가 필요하다. 스프링 배치에서 이 인터페이스를 라인토크나이저(LineTokenizer)라 한다:

  public interface LineTokenizer {
    FieldSet tokenize(String line);
  }

라인토크나이저(LineTokenizer)의 기능은, 입력 라인이 주어지면(이론적으로 스트링(String)은 둘 이상의 라인을 포함할 수 있음) 해당 라인을 나타내는 필드셋(FieldSet)이 반환되는 것과 같다. 이 필드셋(FieldSet)필드셋매퍼(FieldSetMapper)에 전달될 수 있다. 스프링 배치는 다음 라인토크나이저(LineTokenizer) 구현체를 가지고 있다:

  • 디리미티드라인토크나이저(DelimitedLineTokenizer): 레코드의 필드가 구분 기호(delimiter)로 구분되는 파일에 사용된다. 가장 일반적인 구분 기호는 쉼표(,)이지만, 파이프()나 세미콜론(;)도 자주 사용된다.
  • 픽스드렝스토크나이저(FixedLengthTokenizer): 레코드의 필드가 각각 “고정 너비”인 파일에 사용된다. 각 레코드 유형에 대해 각 필드의 너비를 정의해야 한다.
  • 패턴매칭컴포지트라인토크나이저(PatternMatchingCompositeLineTokenizer): 패턴을 확인하여 토크나이저 목록 중 특정 라인에서 사용해야 하는 라인토크나이저(LineTokenizer)를 결정한다.

FieldSetMapper

필드셋매퍼(FieldSetMapper) 인터페이스는 필드셋(FieldSet) 객체를 가져오고 내용을 객체에 매핑하는 메서드인 mapFieldSet를 정의한다. 이 객체는 잡의 필요에 따라, 커스텀 DTO, 도메인 객체 또는 배열일 수 있다. 필드셋매퍼(FieldSetMapper)라인토크나이저(LineTokenizer)와 함께 사용되어, 다음 인터페이스 정의된 것처럼, 리소스의 데이터 라인을 원하는 타입의 객체로 변환한다:

 public interface FieldSetMapper<T> {
    T mapFieldSet(FieldSet fieldSet) throws BindException;
  }

사용 패턴은 JdbcTemplate에서 사용하는 로우매퍼(RowMapper)와 동일하다.

DefaultLineMapper

이제 플랫 파일을 읽기 위한 기본 인터페이스가 정의되었으므로, 세 가지 단계가 필요하다는 것이 분명해졌다:

  1. 파일에서 한 줄 읽는다.
  2. 스트링(String) 라인을 LineTokenizer#tokenize() 메서드에 전달하여 필드셋(FieldSet)을 검색한다.
  3. 토큰화 후 반환된 필드셋(FieldSet)필드셋매퍼(FieldSetMapper)에 전달하고, ItemReader#read() 메서드의 결과를 반환한다.

위에서 설명한 두 인터페이스는 라인을 필드셋(FieldSet)으로 변환하고 필드셋(FieldSet)을 도메인 객체에 매핑하는 두 가지 개별 작업을 나타낸다. 라인토크나이저(LineTokenizer)의 입력은 라인매퍼(LineMapper)(라인)의 입력과 일치하고, 필드셋매퍼(FieldSetMapper)의 출력은 라인매퍼(LineMapper)의 출력과 일치하므로 라인토크나이저(LineTokenizer)필드셋매퍼(FieldSetMapper)를 모두 사용하는 구현체가 제공된다. 다음 클래스 정의에 표시된, 디폴트라인매퍼(DefaultLineMapper)는 대부분의 사용자에게 필요한 동작들이 만들어져 있다:

  public class DefaultLineMapper<T> implements LineMapper<>, InitializingBean {
    private LineTokenizer tokenizer;
    private FieldSetMapper<T> fieldSetMapper;

    public T mapLine(String line, int lineNumber) throws Exception {
      return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line));
    }

    public void setLineTokenizer(LineTokenizer tokenizer) {
      this.tokenizer = tokenizer;
    }

    public void setFieldSetMapper(FieldSetMapper<T> fieldSetMapper) {
      this.fieldSetMapper = fieldSetMapper;
    } 
  }

위의 기능은 특히 원시 라인에 대한 접근이 필요한 경우, 사용자가 파싱 프로세스 제어 시 유연성 주기위해 리더(reader) 자체에 내장되지 않고(이전 버전의 프레임워크에서 수행된 것처럼) 기본 구현체로 제공된다.

Simple Delimited File Reading Example

다음 예는 실제 도메인 시나리오로 플랫 파일을 읽는 방법을 보여준다. 이 특정 배치 잡은 다음 파일에서 축구 선수를 읽는다:

  ID,lastName,firstName,position,birthYear,debutYear
  "AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996",
  "AbduRa00,Abdullah,Rabih,rb,1975,1999",
  "AberWa00,Abercrombie,Walter,rb,1959,1982",
  "AbraDa00,Abramowicz,Danny,wr,1945,1967",
  "AdamBo00,Adams,Bob,te,1946,1969",
  "AdamCh00,Adams,Charlie,wr,1979,2003"

이 파일의 내용은 다음 플레이어(Player) 도메인 객체에 매핑된다:

  public class Player implements Serializable {
    private String ID;
    private String lastName;
    private String firstName;
    private String position;
    private int birthYear;
    private int debutYear;

    public String toString() {
      return "PLAYER:ID=" + ID + ",Last Name=" + lastName +
          ",First Name=" + firstName + ",Position=" + position +
          ",Birth Year=" + birthYear + ",DebutYear=" +
          debutYear;
    }

    // setters and getters...
  }

필드셋(FieldSet)플레이어(Player) 객체에 매핑하려면, 다음 예제와 같이 플레이어를 반환하는 필드셋매퍼(FieldSetMapper)를 정의해야 한다:

 protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> {
    public Player mapFieldSet(FieldSet fieldSet) {
      Player player = new Player();
      
      player.setID(fieldSet.readString(0));
      player.setLastName(fieldSet.readString(1));
      player.setFirstName(fieldSet.readString(2));
      player.setPosition(fieldSet.readString(3));
      player.setBirthYear(fieldSet.readInt(4));
      player.setDebutYear(fieldSet.readInt(5));

      return player;
    }
  }

그러면 다음 예제와 같이 플랫파일아이템리더(FlatFileItemReader)를 올바르게 구성하고 read를 호출하여 파일을 읽을 수 있다;

  FlatFileItemReader<Player> itemReader = new FlatFileItemReader<>();
  itemReader.setResource(new FileSystemResource("resources/players.csv"));
  DefaultLineMapper<Player> lineMapper = new DefaultLineMapper<>();
  //디리미티드라인토크나이저(DelimitedLineTokenizer)는 기본적으로 구분 기호(delimiter)로 쉼표(,)를 사용한다.
  lineMapper.setLineTokenizer(new DelimitedLineTokenizer());
  lineMapper.setFieldSetMapper(new PlayerFieldSetMapper());
  itemReader.setLineMapper(lineMapper);
  itemReader.open(new ExecutionContext());

  Player player = itemReader.read();

read를 호출할 때마다 파일의 각 줄에서 새 플레이어(Player) 개체를 반환한다. 파일 끝에 도달하면 null이 반환된다.

Mapping Fields by Name

디리미티드라인토크나이저(DelimitedLineTokenizer)픽스드렝스토크나이저(FixedLengthTokenizer) 모두 허용되고 기능이 JDBC 리절트셋(ResultSet)과 유사한 추가 기능이 하나 있다. 필드명은 매핑 함수의 가독성을 높이기 위해 라인토크나이저(LineTokenizer) 구현체 중 하나에 주입될 수 있다. 먼저, 다음 예제와 같이 플랫 파일에 있는 모든 필드의 컬럼명이 토크나이저에 주입된다:

  tokenizer.setNames(new String[] {"ID", "lastName", "firstName", "position", "birthYear", "debutYear"});

필드셋매퍼(FieldSetMapper)는 이 정보를 다음과 같이 사용할 수 있다:

  public class PlayerMapper implements FieldSetMapper<Player> {
    public Player mapFieldSet(FieldSet fs) {
      if (fs == null) {
        return null;
      }

      Player player = new Player();

      player.setID(fs.readString("ID"));
      player.setLastName(fs.readString("lastName"));
      player.setFirstName(fs.readString("firstName"));
      player.setPosition(fs.readString("position"));
      player.setDebutYear(fs.readInt("debutYear"));
      player.setBirthYear(fs.readInt("birthYear"));

      return player;
    }
  }

Automapping FieldSets to Domain Objects

많은 사람들에게, 특정 필드셋매퍼(FieldSetMapper)를 작성해야 하는 것은 JdbcTemplate에 대한 특정 로우매퍼(RowMapper)를 작성하는 것만큼 번거롭다. 스프링 배치는 자바빈(JavaBean) 사양을 사용하여 객체의 setter와 필드명을 일치시켜 필드를 자동으로 매핑하는 필드셋매퍼(FieldSetMapper)를 제공하여 이를 더 쉽게 만든다.

축구 예제를 ​​다시 사용하면, 빈래퍼파일셋매퍼(BeanWrapperFieldSetMapper) 구성은 XML에서 다음 스니펫으로 보여준다:

XML 구성

  <bean id="fieldSetMapper" class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
    <property name="prototypeBeanName" value="player" />
  </bean>
  <bean id="player"
        class="org.springframework.batch.sample.domain.Player"
        scope="prototype" />

축구 예제를 ​​다시 사용하면, 빈래퍼파일셋매퍼(BeanWrapperFieldSetMapper) 구성은 자바에서 다음 스니펫으로 보여준다: 자바 구성

  @Bean
  public FieldSetMapper fieldSetMapper() {
    BeanWrapperFieldSetMapper fieldSetMapper = new BeanWrapperFieldSetMapper();
    fieldSetMapper.setPrototypeBeanName("player");
    return fieldSetMapper;
  }
  
  @Bean
  @Scope("prototype")
  public Player player() {
    return new Player();
  }

필드셋(FieldSet)의 각 아이템에 대해 매퍼는 스프링 컨테이너가 프로퍼티명과 일치하는 setter를 찾는 것과 같은 방식으로 플레이어(Player) 객체의 새 인스턴스에서 해당 setter를 찾는다(이러한 이유로 프로토타입 스코프가 필요함). 필드셋(FieldSet)에서 사용 가능한 각 필드가 매핑되고, 코드를 작성하지 않고도, 플레이어(Player) 객체가 반환된다.

Fixed Length File Formats

지금까지, 구분된(delimited) 파일에 대해서만 자세히 설명했다. 그러나 이것은 파일 읽기의 절반만 보여준 것이다. 플랫 파일을 사용하는 많은 조직에서는 고정 길이 형식(fixed length file)을 사용한다. 고정 길이 파일의 예는 다음과 같다.

  UK21341EAH4121131.11customer1
  UK21341EAH4221232.11customer2
  UK21341EAH4321333.11customer3
  UK21341EAH4421434.11customer4
  UK21341EAH4521535.11customer5

이것은 하나의 큰 필드처럼 보이지만 실제로는 4개의 개별 필드를 나타낸다:

  1. ISIN: 주문 항목의 고유 식별자 - 12자 길이
  2. Quantity: 주문할 항목의 번호 - 3자 길이.
  3. Price: 아이템 가격 - 5자 길이.
  4. Customer: 아이템을 주문하는 고객 ID - 9자 길이. 픽스드렝스라인토크나이저(FixedLengthLineTokenizer)를 구성할 때, 이러한 각 길이는 범위 형식으로 제공된다.

다음 예는 XML에서 픽스드렝스라인토크나이저(FixedLengthLineTokenizer)의 범위를 정의하는 방법을 보여준다: XML 구성

  <bean id="fixedLengthLineTokenizer" class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
    <property name="names" value="ISIN,Quantity,Price,Customer" />
    <property name="columns" value="1-12, 13-15, 16-20, 21-29" />
  </bean>

픽스드렝스라인토크나이저(FixedLengthLineTokenizer)는 앞에서 설명한 것과 동일한 라인토크나이저(LineTokenizer) 인터페이스를 사용하므로, 구분 기호(delimiter)가 사용된 것처럼 동일한 필드셋(FieldSet)을 반환한다. 이를 통해 빈래퍼필드셋매퍼(BeanWrapperFieldSetMapper)를 사용하는 것과 같은 출력 처리와 같은 접근 방식을 사용할 수 있다.

범위에 대한 앞의 구문을 지원하려면 특수 프로퍼티 에디터인 레인지어레이프로퍼티에디터(RangeArrayPropertyEditor)어플리케이션컨텍스트(ApplicationContext)에 구성되어 있어야 한다. 그러나, 이 빈은 배치(batch) 네임스페이스가 사용되는 어플리케이션컨텍스트(ApplicationContext)에 자동으로 선언된다

다음 예는 자바에서 픽스드렝스라인토크나이저(FixedLengthLineTokenizer)의 범위를 정의하는 방법을 보여준다: 자바 구성

  @Bean
  public FixedLengthTokenizer fixedLengthTokenizer() {
    FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
    tokenizer.setNames("ISIN", "Quantity", "Price", "Customer");
    tokenizer.setColumns(new Range(1, 12),
                        new Range(13, 15),
                        new Range(16, 20),
                        new Range(21, 29));

    return tokenizer;
  }

픽스드렝스라인토크나이저(FixedLengthLineTokenizer)는 위에서 설명한 것과 동일한 라인토크나이저(LineTokenizer) 인터페이스를 사용하므로, 구분 기호(delimiter)가 사용된 것처럼 동일한 필드셋(FieldSet)을 반환한다. 를 통해 빈래퍼필드셋매퍼(BeanWrapperFieldSetMapper)를 사용하는 것과 같은 출력 처리와 같은 접근 방식을 사용할 수 있다.

Multiple Record Types within a Single File

지금까지 파일 읽기 예제 단순화를 위해 모든 레코드는 동일한 형식을 가진다는 가정을 했다. 그러나, 항상 그런 것은 아니다. 일반적으로 파일에 다르게 토큰화하고 다른 객체에 매핑해야 하는 형식의 레코드가 있을 수 있따. 다음은 파일의 내용이다:

  USER;Smith;Peter;;T;20014539;F
  LINEA;1044391041ABC037.49G201XX1383.12H
  LINEB;2134776319DEF422.99M005LI

이 파일에는 “USER”, “LINEA” 및 “LINEB”의 세 가지 유형의 레코드가 있다. “USER” 라인은 유저(User) 객체에 해당한다. “LINEA”와 “LINEB”는 둘 다 라인(Line) 객체에 해당하지만, “LINEA”는 “LINEB”보다 더 많은 정보를 가지고 있다.

아이템리더(ItemReader)는 각 라인을 개별적으로 읽지만, 아이템라이터(ItemWriter)가 올바른 아이템을 수신하도록 서로 다른 라인토크나이저(LineTokenizer)필드셋매퍼(FieldSetMapper) 객체를 지정해야 한다. 패턴와칭컴포짓라인매퍼(PatternMatchingCompositeLineMapper)를 사용하면 라인토크나이저(LineTokenizer)에 대한 패턴 맵(map)과 필드셋매퍼(FieldSetMapper)에 대한 패턴을 구성할 수 있으므로 이를 쉽게 수행할 수 있다.

다음 예에서는 XML에서 픽스드렝스라인토크나이저(FixedLengthLineTokenizer)의 범위를 정의하는 방법을 보여준다: XML 구성

  <bean id="orderFileLineMapper" class="org.spr...PatternMatchingCompositeLineMapper">
    <property name="tokenizers">
      <map>
        <entry key="USER*" value-ref="userTokenizer" />
        <entry key="LINEA*" value-ref="lineATokenizer" />
        <entry key="LINEB*" value-ref="lineBTokenizer" />
      </map>
    </property>
    <property name="fieldSetMappers">
      <map>
        <entry key="USER*" value-ref="userFieldSetMapper" />
        <entry key="LINE*" value-ref="lineFieldSetMapper" />
      </map>
    </property>
  </bean>

자바 구성

  @Bean
  public PatternMatchingCompositeLineMapper orderFileLineMapper() {
      PatternMatchingCompositeLineMapper lineMapper = new PatternMatchingCompositeLineMapper();

      Map<String, LineTokenizer> tokenizers = new HashMap<>(3);
      tokenizers.put("USER*", userTokenizer());
      tokenizers.put("LINEA*", lineATokenizer());
      tokenizers.put("LINEB*", lineBTokenizer());

      lineMapper.setTokenizers(tokenizers);

      Map<String, FieldSetMapper> mappers = new HashMap<>(2);
      mappers.put("USER*", userFieldSetMapper());
      mappers.put("LINE*", lineFieldSetMapper());

      lineMapper.setFieldSetMappers(mappers);

      return lineMapper;
  }

이 예에서 “LINEA” 및 “LINEB”에는 별도의 라인토크나이저(LineTokenizer) 인스턴스가 있지만, 둘 다 동일한 필드셋매퍼(FieldSetMapper)를 사용한다.

패턴매칭컴포짓라인매퍼(PatternMatchingCompositeLineMapper)PatternMatcher#match 메서드를 사용하여 각 라인에 대한 올바른 델리게이트(delegate)를 선택한다. 패턴매처(PatternMatcher)는 특별한 의미를 가진 두 개의 와일드카드 문자를 사용한다. 물음표(question mark: ?)는 정확히 한 문자와 일치하고, 별표(asterisk: *)는 0개 이상의 문자와 일치한다. 이전 구성에서, 모든 패턴은 별표로 끝나므로, 기능적으로 라인의 접두사가 된다. 패턴매처(PatternMatcher)는 구성 순서에 관계없이 항상 구체적인 패턴과 일치시킨다. 따라서 “LINE*” 및 “LINEA*“가 모두 패턴으로 나열된 경우 “LINEA”는 패턴 “LINEA*“와 일치하고 “LINEB”는 패턴 “LINE*“과 일치한다. 또한, 단일 별표(“*“)는 다른 패턴과 일치하지 않는 라인을 일치시키는 기본값으로 사용할 수 있다.

다음 예에서는 XML의 다른 패턴과 일치하지 않는 라인을 일치시키는 방법을 보여준다: XML 구성

  <entry key="*" value-ref="defaultLineTokenizer" />

다음 예에서는 자바의 다른 패턴과 일치하지 않는 라인을 일치시키는 방법을 보여준다: 자바 구성

  ...
  tokenizers.put("*", defaultLineTokenizer());
  ...

토큰화에만 사용할 수 있는 패턴매칭컴포짓라인토크나이저(PatternMatchingCompositeLineTokenizer)도 있다.

플랫 파일에 각각 여러 라인에 걸쳐 있는 레코드가 포함되는 것도 일반적이다. 이 상황을 처리하려면 더 복잡한 전략이 필요하다. 이 일반적인 패턴의 데모는 multiLineRecords 샘플에서 찾을 수 있다.

Exception Handling in Flat Files

라인을 토큰화하면 예외가 발생할 수 있는 여러 상황이 있다. 플랫 파일이 불완전하고 형식이 잘못된 레코드를 포함할 수 있다. 많은 사용자가 문제, 라인 및 라인 번호를 기록하는 동안 이러한 잘못된 라인을 건너뛰도록 선택한다. 이러한 로그는 나중에 수동으로 또는 다른 배치 잡으로 검사할 수 있다. 이러한 이유로 스프링 배치는 파링 예외를 처리하기 위한 예외 계층(플랫파일파스익셉션(FlatFileParseException)플랫파일포맷익셉션(FlatFileFormatException))을 제공한다. 파일을 읽으려고 시도하는 동안 오류가 발생하면 플랫파일파스익셉션(FlatFileParseException)플랫파일아이템리더(FlatFileItemReader)에 의해 발생한다. 플랫파일포맷익셉션(FlatFileFormatException)라인토크나이저(LineTokenizer) 인터페이스의 구현체에서 발생하며 토큰화하는 동안 보다 구체적인 오류를 나타냅니다.

IncorrectTokenCountException

디리미티드라인토크나이저(DelimitedLineTokenizer)픽스트렝스라인토크나이저(FixedLengthLineTokenizer) 모두 필드셋(FieldSet) 생성에 사용할 수 있는 컬럼 이름을 지정하는 기능이 있다. 그러나 컬럼명의 수가 라인을 토큰화하는 동안 발견된 컬럼의 수와 일치하지 않으면, 필드셋(FieldSet)을 생성할 수 없으며, 다음 예시처럼, 발생한 토큰 수와 예상되는 수를 포함하는 인코렉트토큰카운트익셉션(IncorrectTokenCountException)이 발생한다:

  tokenizer.setNames(new String[] {"A", "B", "C", "D"});

  try {
    tokenizer.tokenize("a,b,c");
  } catch (IncorrectTokenCountException e) {
    assertEquals(4, e.getExpectedCount());
    assertEquals(3, e.getActualCount());
  }

토크나이저가 4개의 컬럼명으로 구성되었지만, 파일에서 3개의 토큰만 발견되었기 때문에 인코렉트토큰카운트익셉션(IncorrectTokenCountException)이 발생했다.

IncorrectLineLengthException

고정 길이 형식으로 형식이 지정된 파일은 구분(delimited) 형식과 달리 각 컬럼이 미리 정의된 너비를 엄격하게 준수해야 하므로, 파싱에 추가 요구 사항이 있다. 라인의 총 길이가 이 컬럼의 가장 넓은 값과 같지 않으면 다음 예시처럼 예외가 발생한다:

  tokenizer.setColumns(new Range[] { new Range(1, 5),
                                     new Range(6, 10),
                                     new Range(11, 15) });
  try {
      tokenizer.tokenize("12345");
      fail("Expected IncorrectLineLengthException");
  } catch (IncorrectLineLengthException ex) {
      assertEquals(15, ex.getExpectedLength());
      assertEquals(5, ex.getActualLength());
  }

위의 토크나이저에 구성된 범위는 1-5, 6-10 및 11-15이다. 따라서 선의 총 길이는 15여야 한다. 그러나, 앞의 예에서 길이가 5인 라인이 전달되어 인코렉트라인렝스익셉션(IncorrectLineLengthException)이 발생했다. 첫 번째 컬럼만 매핑하는 대신 예외를 발생하면 필드셋매퍼(FieldSetMapper)의 컬럼 2를 읽기를 실패한 경우보다 더 많은 정보와 함께 라인 처리가 더 일찍 실패할 수 있다. 그러나 라인의 길이가 항상 일정하지 않은 경우가 있다. 이러한 이유로 다음 예와 같이 ‘strict’ 프로퍼티을 통해 라인 길이 유효성 검사를 끌 수 있다:

  tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) });
  tokenizer.setStrict(false);
  FieldSet tokens = tokenizer.tokenize("12345");
  assertEquals("12345", tokens.readString(0));
  assertEquals("", tokens.readString(1));

앞의 예제는 tokenizer.setStrict(false)가 호출되었다는 점을 제외하면 이전 예제와 거의 동일하다. 이제 필드셋(FieldSet)이 올바르게 생성되고 반환된다. 그러나 나머지 토큰 값은 비어있다.

6.5.3. FlatFileItemWriter

플랫 파일에 쓰기는 파일에서 읽기와 동일한 문제가 있다. 스텝은 트랜잭션 방식으로 구분(delimited) 또는 고정 길이 형식(fixed length format)을 작성할 수 있어야 한다.

LineAggregator

라인토크나이저(LineTokenizer) 인터페이스가 아이템을 가져와 스트링(String)으로 변환하는 게 필요한 것처럼, 파일에 쓰기 위해 여러 필드를 단일 스트링(string)으로 집계하는 방법이 있어야 한다. 스프링 배치에서, 이것은 다음 인터페이스에 정의된 라인애그리게이터(LineAggregator)이다:

  public interface LineAggregator<T> {
      public String aggregate(T item);
  }

라인애그리게이터(LineAggregator)라인토크나이저(LineTokenizer)와 논리적으로 반대이다. 라인토크나이저(LineTokenizer)스트링(String)을 가져와 필드셋(FieldSet)을 반환하는 반면 라인애그리게이터(LineAggregator)아이템을 가져와 스트링(String)을 반환한다.

PassThroughLineAggregator

라인애그리게이터(LineAggregator) 인터페이스의 기본적인 구현체는 패스스루라인애그리게이터(PassThroughLineAggregator)로, 다음 코드와 같이 객체가 이미 스트링(string)이며 쓰기에 적합하다고 가정한다:

  public class PassThroughLineAggregator<T> implements LineAggregator<T> {
    public String aggregate(T item) {
      return item.toString();
    } 
  }

위의 구현체는 스트링(string) 생성에 대한 직접적인 제어가 필요하지만, 트랜잭션 및 재시작 지원과 같은 플랫파일아이템라이터(FlatFileItemWriter)의 장점이 필요한 경우에는 유용하다.

Simplified File Writing Example

이제 라인애그리게이터(LineAggregator) 인터페이스와 가장 기본적인 구현체인 패스스루라인애그리게이터(PassThroughLineAggregator)가 정의되었으므로, 기본 쓰기의 흐름을 설명할 수 있다.

  1. 작성할 객체는 스트링(String)을 얻기 위해 라인애그리게이터(LineAggregator)에 전달된다.
  2. 반환된 스트링은 구성된 파일에 기록된다.

플랫파일아이템라이터(FlatFileItemWriter)에 정의된 다음 코드는 위 내용을 코드로 나타낸다:

  public void write(T item) throws Exception {
    write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
  }

XML으로 구성된, 간단한 예는 다음과 같다: XML 구성

  <bean id="itemWriter" class="org.spr...FlatFileItemWriter">
    <property name="resource" value="file:target/test-outputs/output.txt" />
    <property name="lineAggregator">
        <bean class="org.spr...PassThroughLineAggregator"/>
    </property>
  </bean>

자바로 구성된, 간단한 예는 다음과 같다: 자바 구성

  @Bean
  public FlatFileItemWriter itemWriter() {
      return new FlatFileItemWriterBuilder<Foo>()
              .name("itemWriter")
              .resource(new FileSystemResource("target/test-outputs/output.txt"))
              .lineAggregator(new PassThroughLineAggregator<>())
              .build();
  }

FieldExtractor

앞의 예제는 파일 쓰기의 가장 기본적인 사용에 유용할 수 있다. 그러나 대부분의 플랫파일아이템라이터(FlatFileItemWriter) 사용자는 작성해야 하는 도메인 객체가 있으므로, 라인으로 변환해야 한다. 파일 읽기에서 다음이 필요했었다:

  1. 파일에서 한 줄 읽기.
  2. 필드셋(FieldSet)을 검색하기 위해 라인을 LineTokenizer#tokenize() 메서드에 전달한다.
  3. 토큰화에서 반환된 필드셋(FieldSet)필드셋매퍼(FieldSetMapper)에 전달하고 ItemReader#read() 메서드의 결과를 반환한다.

파일 쓰기와 유사하지만 반대의 단계가 있다:

  1. 작성할 아이템을 라이터에게 전달.
  2. 아이템의 필드를 배열로 변환.
  3. 결과 배열을 한 줄로 집계.

프레임워크가 객체의 어떤 필드를 작성해야 하는지 알 방법이 없기 때문에, 필드익스트랙터(FieldExtractor)는 다음 인터페이스에 정의된 것처럼 아이템을 배열로 바꾸는 작업을 수행해야 한다:

  public interface FieldExtractor<T> {
    Object[] extract(T item);
  }

필드익스트랙터(FieldExtractor) 인터페이스의 구현체는 엘리먼트 사이에 구분 기호(delimiter)를 사용하거나 고정 너비 라인의 일부로 작성할 수 있는 객체의 필드에서 배열을 생성한다.

PassThroughFieldExtractor

배열, 컬렉션 또는 필드셋(FieldSet)과 같은 객체을 작성해야 하는 경우가 많다. 이러한 컬렉션 타입 중 하나에서 배열을 “추출”하는 것은 매우 간단하다. 이렇게 하려면 컬렉션을 배열로 변환한다. 따라서 이런 경우에서 패스스루필드익스트렉터(PassThroughFieldExtractor)를 사용해야 한다. 전달된 객체가 컬렉션 타입이 아닌 경우 패스스루필드익스트랙터(PassThroughFieldExtractor)는 추출할 아이템만 포함하는 배열을 반환한다는 점에 유의해야 한다.

BeanWrapperFieldExtractor

파일 읽기 절에서 설명한 빈래퍼필드셋매퍼(BeanWrapperFieldSetMapper)와 마찬가지로 변환을 직접 작성하는 것보다, 도메인 객체를 객체 배열로 변환하는 방법을 선호하는 경우가 많다. 빈래퍼필드익스트랙터(BeanWrapperFieldExtractor)는 다음 예제와 같이 이런 기능을 제공한다:

  BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<>();
  extractor.setNames(new String[] { "first", "last", "born" });

  String first = "Alan";
  String last = "Turing";
  int born = 1912;

  Name n = new Name(first, last, born);
  Object[] values = extractor.extract(n);

  assertEquals(first, values[0]);
  assertEquals(last, values[1]);
  assertEquals(born, values[2]);

이 익스트랙터(extractor) 구현체에는 하나의 필수 프로퍼티(매핑할 필드명)만 있다. 빈래퍼필드셋매퍼(BeanWrapperFieldSetMapper)가 제공된 객체의 필드셋(FieldSet)의 필드를 setter에 매핑하기 위해 필드명이 필요한 것처럼, 빈래퍼필드익스트랙터(BeanWrapperFieldExtractor)는 객체 배열을 생성을 위해 getter에 매핑용 이름이 필요하다. 이름의 순서가 배열 내 필드 순서를 결정한다는 점은 주목할만 하다.

Delimited File Writing Example

가장 기본적인 플랫 파일은 모든 필드가 구분 기호(delimiter)로 구분되는 형식이다. 이는 디리미티드라인애그리게이터(DelimitedLineAggregator)를 사용하여 수행할 수 있다. 다음 예에서 고객 계정에 대한 크레딧을 나타내는 간단한 도메인 객체를 작성한다:

  public class CustomerCredit {
    private int id;
    private String name;
    private BigDecimal credit;
    
    //getters and setters는 명확하게 하기 위해 삭제한다.
  }

도메인 객체를 사용 중이므로, 사용할 구분 기호(delimiter)와 함께 필드익스트랙터(FieldExtractor) 인터페이스의 구현체를 제공해야 한다.

다음 예는 XML에서 구분 기호와 함께 필드익스트랙터(FieldExtractor)를 사용하는 방법을 보여준다: XML 구성

  <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
    <property name="resource" ref="outputResource" />
    <property name="lineAggregator">
      <bean class="org.spr...DelimitedLineAggregator">
        <property name="delimiter" value=","/>
        <property name="fieldExtractor">
          <bean class="org.spr...BeanWrapperFieldExtractor">
            <property name="names" value="name,credit"/>
          </bean>
        </property>
      </bean>
    </property>
  </bean>

다음 예는 자바에서 구분 기호와 함께 필드익스트랙터(FieldExtractor)를 사용하는 방법을 보여준다: 자바 구성

  @Bean
  public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    BeanWrapperFieldExtractor<CustomerCredit> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(new String[] {"name", "credit"});
    fieldExtractor.afterPropertiesSet();

    DelimitedLineAggregator<CustomerCredit> lineAggregator = new DelimitedLineAggregator<>();
    lineAggregator.setDelimiter(",");
    lineAggregator.setFieldExtractor(fieldExtractor);

    return new FlatFileItemWriterBuilder<CustomerCredit>()
              .name("customerCreditWriter")
              .resource(outputResource)
              .lineAggregator(lineAggregator)
              .build();
  }

이 장의 앞부분에서, 설명한 빈래퍼필드익스트랙터(BeanWrapperFieldExtractor)커스터머크레딧(CustomerCredit) 내의 name 및 credit 필드를 객체 배열로 변환하는 데 사용되며 각 필드 사이에 쉼표로 작성된다. 다음 예제와 같이 FlatFileItemWriterBuilder.DelimitedBuilder를 사용하여 빈래퍼필드익스트랙터(BeanWrapperFieldExtractor)디리미티드라인애그리게이터(DelimitedLineAggregator)를 자동으로 생성하는 것도 가능하다:

자바 구성

  @Bean
  public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
      return new FlatFileItemWriterBuilder<CustomerCredit>()
                  .name("customerCreditWriter")
                  .resource(outputResource)
                  .delimited()
                  .delimiter("|")
                  .names(new String[] {"name", "credit"})
                  .build();
  }

Fixed Width File Writing Example

구분 기호가 플랫 파일 형식의 유일한 타입은 아니다. 일반적으로 ‘고정 너비’라고 하는 필드 사이를 구분하기 위해 각 컬럼에 설정된 너비를 사용하는 것을 선호한다. 스프링 배치는 포맷터라인애그리게이터(FormatterLineAggregator)를 사용한 파일 작성에서 이를 지원한다.

위에서 설명한 것과 동일한 커스터머크레딧(CustomerCredit) 도메인 객체를 사용하여, XML에서 다음과 같이 구성할 수 있다: XML 구성

  <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
    <property name="resource" ref="outputResource" />
    <property name="lineAggregator">
      <bean class="org.spr...FormatterLineAggregator">
        <property name="fieldExtractor">
          <bean class="org.spr...BeanWrapperFieldExtractor">
            <property name="names" value="name,credit" />
          </bean>
        </property>
        <property name="format" value="%-9s%-2.0f" />
      </bean>
    </property>
  </bean>

위에서 설명한 것과 동일한 커스터머크레딧(CustomerCredit) 도메인 객체를 사용하여, 자바에서 다음과 같이 구성할 수 있다: 자바 구성

  @Bean
  public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    BeanWrapperFieldExtractor<CustomerCredit> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(new String[] {"name", "credit"});
    fieldExtractor.afterPropertiesSet();

    FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
    lineAggregator.setFormat("%-9s%-2.0f");
    lineAggregator.setFieldExtractor(fieldExtractor);

    return new FlatFileItemWriterBuilder<CustomerCredit>()
                .name("customerCreditWriter")
                .resource(outputResource)
                .lineAggregator(lineAggregator)
                .build();
  }

위 예제는 대부분 익숙할 것이다. 그러나 format 프로퍼티는 새로울 것이다.

다음 예는 XML format 프로퍼티를 보여준다:

  <property name="format" value="%-9s%-2.0f" />

다음 예는 자바 format 프로퍼티를 보여준다:

  ...
  FormatterLineAggregator<CustomerCredit> lineAggregator = new FormatterLineAggregator<>();
  lineAggregator.setFormat("%-9s%-2.0f");
  ...

기본 구현은 자바 5에 추가된 동일한 포맷터(Formatter)를 사용하여 빌드된다. 자바 포맷터(Formatter)는 C 프로그래밍 언어의 printf 기능을 기반으로 한다. 포맷터를 구성하는 방법에 대한 자세한 내용은 자바독(Javadoc)포맷터(Formatter)에서 찾을 수 있다.

다음 예제와 같이 FlatFileItemWriterBuilder.FormattedBuilder를 사용하여 빈래퍼필드익스트랙터(BeanWrapperFieldExtractor)포맷터라인애그리게이터(FormatterLineAggregator)를 자동으로 생성하는 것도 가능하다:

자바 구성

  @Bean
  public FlatFileItemWriter<CustomerCredit> itemWriter(Resource outputResource) throws Exception {
    return new FlatFileItemWriterBuilder<CustomerCredit>()
            .name("customerCreditWriter")
            .resource(outputResource)
            .formatted()
            .format("%-9s%-2.0f")
            .names(new String[] {"name", "credit"})
            .build();
  }

Handling File Creation

플랫파일아이템리더(FlatFileItemReader)는 파일 리소스와 매우 단순한 관계를 가진다. 리더(reader)가 초기화되면 파일이 있는 경우 열고 파일이 없으면 예외를 발생시킨다. 파일 쓰기는 그렇게 간단하지 않다. 언뜻 보기에 플랫파일아이템라이터(FlatFileItemWriter)는 간단한 기능인 것처럼 보인다. 파일이 이미 있으면 예외를 발생, 그렇지 않으면 파일을 만들고 작성을 시작한다. 그러나 잡을 다시 시작하면 문제가 발생할 수 있다. 일반적인 재시작 상황에서는 기능이 반전된다. 파일이 있으면 마지막으로 알려진 위치에서 파일 쓰기를 시작하고 그렇지 않으면 예외를 발생시킨다. 그러나 이 의 파일 이름이 항상 동일하면 어떻게 될까? 이 경우, 재시작할 경우 파일이 있으면 삭제하고 싶을 것이다. 이러한 가능성 때문에 플랫파일아이템라이터(FlatFileItemWriter)에는 shouldDeleteIfExists 프로퍼티가 포함되어 있다. 이 프로퍼티를 true로 설정하면 라이터가 열릴 때 같은 이름의 기존 파일이 삭제된다.

6.6. XML Item Readers and Writers

스프링 배치는 XML 레코드를 읽고 자바 객체에 매핑하고 자바 객체를 XML 레코드로 작성하기 위한 트랜잭션 인프라스트럭처를 제공한다.

스트리밍 XML에 대한 제약: 표준 XML 파싱 API는 배치를 처리하기 위한 요구 사항(DOM은 전체 입력을 한 번에 메모리에 로드하고 SAX는 사용자가 콜백만 제공하도록 허용하여 파싱 프로세스를 제어함)에 맞지 않기 때문에, StAX API는 I/O에 사용된다.

스프링 배치에서 XML 입력 및 출력이 작동하는 방식을 고려해야 한다. 첫째, 파일 읽기 및 쓰기와는 다르지만 스프링 배치 XML 처리에서 공통되는 몇 가지 개념이 있다. XML 처리를 사용하면 토큰화해야 하는 레코드의 라인(필드셋(FieldSet) 인스턴스) 대신, XML 리소스가 다음 이미지와 같이 개별 레코드에 해당하는 ‘조각(fragments)’이라 가정한다:

XML Input 이미지 18. XML 입력

위의 상황에서 ‘trade’ 태그는 ‘root 엘리먼트’로 정의된다. ‘'와 '’ 사이의 모든 것은 하나의 ‘조각(fragments)’으로 간주한다. 스프링 배치는 OXM(Object/XML Mapping)을 사용하여 조각을 객체에 바인딩한다. 그러나, 스프링 배치는 특정 XML 바인딩 기술에 묶여 있지 않다. 일반적으로 가장 인기 있는 OXM 기술에 대한, 균일한 추상화를 제공하는 스프링 OXM에 위임한다. 스프링 OXM에 대한 의존성은 선택 사항이며 원하는 경우 스프링 배치의 특정 인터페이스를 구현하도록 선택할 수 있다.

OXM이 지원하는 기술과의 관계는 다음 이미지에 나와 있다:

OXM Binding 이미지 19. OXM 바인딩

OXM에 대한 소개와 XML 조각(fragment)을 사용하여 레코드를 나타내는 방법을 통해, 이제 리더(reader)와 라이터(writer)를 더 면밀히 조사할 수 있다.

6.6.1. StaxEventItemReader

스택스이벤트아이템리더(StaxEventItemReader) 구성은 XML 입력 스트림의 레코드 처리를 위한 일반적인 설정을 제공한다. 먼저, 스택스이벤트아이템리더(StaxEventItemReader)가 처리할 수 있는 다음 XML 레코드 세트를 생각해보자:

 <?xml version="1.0" encoding="UTF-8"?>
  <records>
    <trade xmlns="https://springframework.org/batch/sample/io/oxm/domain">
      <isin>XYZ0001</isin>
      <quantity>5</quantity>
      <price>11.39</price>
      <customer>Customer1</customer>
    </trade>
    <trade xmlns="https://springframework.org/batch/sample/io/oxm/domain">
      <isin>XYZ0002</isin>
      <quantity>2</quantity>
      <price>72.99</price>
      <customer>Customer2c</customer>
    </trade>
    <trade xmlns="https://springframework.org/batch/sample/io/oxm/domain">
      <isin>XYZ0003</isin>
      <quantity>9</quantity>
      <price>99.99</price>
      <customer>Customer3</customer>
    </trade>
  </records>

XML 레코드를 처리하려면 다음 내용이 필요하다:

  • 루트 엘리먼트명: 매핑할 객체를 구성하는 프래그먼트(fragment)의 루트 엘리먼트명이다. 예제에서는 trade의 값이 루트 엘리먼트 명이다.
  • 리소스(Resource): 읽을 파일을 나타내는 스프링 리소스.
  • 언마샬러(Unmarshaller): XML 프래그먼트(fragment)를 객체에 매핑하기 위해 스프링 OXM에서 제공하는 역마샬링 기능.

다음 예제는 XML에서 trade라는 루트 엘리먼트, data/iosample/input/input.xml의 리소스 및 tradeMarshaller라는 언마샬러와 함께 작동하는 스택스이벤트아이템리더(StaxEventItemReader)를 정의하는 방법을 보여준다:

XML 구성

  <bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader">
    <property name="fragmentRootElementName" value="trade" />
    <property name="resource" value="org/springframework/batch/item/xml/domain/trades.xml" />
    <property name="unmarshaller" ref="tradeMarshaller" />
  </bean>

다음 예제는 자바에서 trade라는 루트 엘리먼트, data/iosample/input/input.xml의 리소스 및 tradeMarshaller라는 언마샬러와 함께 작동하는 스택스이벤트아이템리더(StaxEventItemReader)를 정의하는 방법을 보여준다:

자바 구성

  @Bean
  public StaxEventItemReader itemReader() {
      return new StaxEventItemReaderBuilder<Trade>()
              .name("itemReader")
              .resource(new FileSystemResource("org/springframework/batch/item/xml/domain/trades.xml"))
              .addFragmentRootElements("trade")
              .unmarshaller(tradeMarshaller())
              .build();
  }

이 예에서, 우리는, 키(key)가 프래그먼트(fragment)의 이름(즉, 루트 엘리먼트)이고 값(value)이 바인딩할 객체의 타입인 맵으로 만들어진 앨리어스(alias)을 받는, 엑스스트림마샬러(XStreamMarshaller)를 사용하기로 선택했다.

그런 다음, 필드셋(FieldSet)과 유사하게, 객체 타입 내의 필드에 매핑되는 엘리먼트명이 맵에서 키/값 쌍으로 나타난다. 구성 파일에서, 스프링 컨피규레이션 유틸리티(Spring configuration utility)를 사용하여 필요한 앨리어스(alias)을 설명할 수 있다.

다음 예는 XML에서 앨리어스(alias)을 설명하는 방법을 보여준다: XML 구성

  <bean id="tradeMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
    <property name="aliases">
      <util:map id="aliases">
        <entry key="trade" value="org.springframework.batch.sample.domain.trade.Trade" />
        <entry key="price" value="java.math.BigDecimal" />
        <entry key="isin" value="java.lang.String" />
        <entry key="customer" value="java.lang.String" />
        <entry key="quantity" value="java.lang.Long" />
      </util:map>
    </property>
  </bean>

다음 예는 자바에서 앨리어스(alias)을 설명하는 방법을 보여준다: 자바 구성

  @Bean
  public XStreamMarshaller tradeMarshaller() {
    Map<String, Class> aliases = new HashMap<>();
    
    aliases.put("trade", Trade.class);
    aliases.put("price", BigDecimal.class);
    aliases.put("isin", String.class);
    aliases.put("customer", String.class);
    aliases.put("quantity", Long.class);
    
    XStreamMarshaller marshaller = new XStreamMarshaller();
    
    marshaller.setAliases(aliases);
    
    return marshaller;
  } 

입력 시, 리더(reader)는 새 프래그먼트(fragment)가 시작된다는 것을 인식할 때까지 XML 리소스를 읽는다. 기본적으로, 리더(reader)는 엘리먼트명과 일치하여 새 프래그먼트가 곧 시작된다는 것을 인식한다. 리더는 프래그먼트(fragment)에서 독립형(standalone) XML 문서를 생성하고 문서를 디시리얼라이저(deserializer)(일반적으로 스프링 OXM 언마샬러(Unmarshaller) 주변의 래퍼)에 전달하여 XML을 자바 객체에 매핑한다.

요약하면 이 절차는 스프링에서 제공하는 인젝션(injection)을 사용하는 자바 코드와 유사하다:

  StaxEventItemReader<Trade> xmlStaxEventItemReader = new StaxEventItemReader<>();
  Resource resource = new ByteArrayResource(xmlResource.getBytes());

  Map aliases = new HashMap();
  aliases.put("trade","org.springframework.batch.sample.domain.trade.Trade");
  aliases.put("price","java.math.BigDecimal");
  aliases.put("customer","java.lang.String");
  aliases.put("isin","java.lang.String");
  aliases.put("quantity","java.lang.Long");
  XStreamMarshaller unmarshaller = new XStreamMarshaller();
  unmarshaller.setAliases(aliases);
  xmlStaxEventItemReader.setUnmarshaller(unmarshaller);
  xmlStaxEventItemReader.setResource(resource);
  xmlStaxEventItemReader.setFragmentRootElementName("trade");
  xmlStaxEventItemReader.open(new ExecutionContext());

  boolean hasNext = true;

  Trade trade = null;

  while (hasNext) {
    trade = xmlStaxEventItemReader.read();
    if (trade == null) {
      hasNext = false;
    } else {
      System.out.println(trade);
    } 
  }

6.6.2. StaxEventItemWriter

출력은 입력과 대칭적으로 작동한다. 스택스이벤트아이템라이터(StaxEventItemWriter)에는 리소스(Resource), 마샬러(marshaller) 및 rootTagName이 필요하다. 자바 객체는 OXM 도구에 의해 각 프래그먼트(fragment)에 대해 생성된 스타트다큐먼트(StartDocument)엔드다큐먼트(EndDocument) 이벤트를 필터링하는 커스텀 이벤트 라이터(writer)를 사용하여 리소스에 쓰는 마샬러(일반적으로 표준 스프링 OXM 마샬러)에 전달된다.

다음 XML 예제에서는 마샬링이벤트라이터시리얼라이저(MarshallingEventWriterSerializer)를 사용한다: XML 구성

  <bean id="itemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter">
    <property name="resource" ref="outputResource" />
    <property name="marshaller" ref="tradeMarshaller" />
    <property name="rootTagName" value="trade" />
    <property name="overwriteOutput" value="true" />
  </bean>

다음 자바 예제에서는 마샬링이벤트라이터시리얼라이저(MarshallingEventWriterSerializer)를 사용한다: 자바 구성

  @Bean
  public StaxEventItemWriter itemWriter(Resource outputResource) {
      return new StaxEventItemWriterBuilder<Trade>()
              .name("tradesWriter")
              .marshaller(tradeMarshaller())
              .resource(outputResource)
              .rootTagName("trade")
              .overwriteOutput(true)
              .build();
  }

앞의 구성은 이 장의 앞부분에서 언급했던, 세 가지 필수 프로퍼티스(propertie)와 기존 파일을 덮어쓸지 여부를 지정하기 위해 옵셔널한 overwriteOutput=true 애트리뷰트 설정한다.

다음 XML 예제는 이 장 앞부분에 표시된 읽기 예제에서 사용된 것과 동일한 마샬러를 사용한다: XML 구성

  <bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
    <property name="aliases">
      <util:map id="aliases">
        <entry key="customer" value="org.springframework.batch.sample.domain.trade.Trade" />
        <entry key="price" value="java.math.BigDecimal" />
        <entry key="isin" value="java.lang.String" />
        <entry key="customer" value="java.lang.String" />
        <entry key="quantity" value="java.lang.Long" />
      </util:map>
    </property>
  </bean>

다음 자바 예제는 이 장 앞부분에 표시된 읽기 예제에서 사용된 것과 동일한 마샬러를 사용한다: 자바 구성

  @Bean
  public XStreamMarshaller customerCreditMarshaller() {
    XStreamMarshaller marshaller = new XStreamMarshaller();

    Map<String, Class> aliases = new HashMap<>();
    aliases.put("trade", Trade.class);
    aliases.put("price", BigDecimal.class);
    aliases.put("isin", String.class);
    aliases.put("customer", String.class);
    aliases.put("quantity", Long.class);

    marshaller.setAliases(aliases);

    return marshaller;
}

자바 예제로 요약하면, 다음 코드는 논의된 모든 사항을 설명하고, 필요한 프로퍼티의 프로그래밍의 설정을 보여준다:

  FileSystemResource resource = new FileSystemResource("data/outputFile.xml")

  Map aliases = new HashMap();
  aliases.put("trade","org.springframework.batch.sample.domain.trade.Trade");
  aliases.put("price","java.math.BigDecimal");
  aliases.put("customer","java.lang.String");
  aliases.put("isin","java.lang.String");
  aliases.put("quantity","java.lang.Long");
  
  Marshaller marshaller = new XStreamMarshaller();
  marshaller.setAliases(aliases);

  StaxEventItemWriter staxItemWriter = new StaxEventItemWriterBuilder<Trade>()
                                        .name("tradesWriter")
                                        .marshaller(marshaller)
                                        .resource(resource)
                                        .rootTagName("trade")
                                        .overwriteOutput(true)
                                        .build();

  staxItemWriter.afterPropertiesSet();

  ExecutionContext executionContext = new ExecutionContext();
  staxItemWriter.open(executionContext);
  Trade trade = new Trade();
  trade.setPrice(11.39);
  trade.setIsin("XYZ0001");
  trade.setQuantity(5L);
  trade.setCustomer("Customer1");
  staxItemWriter.write(trade);

6.7. JSON Item Readers And Writers

스프링 배치는 다음 형식으로 JSON 리소스 읽기 및 쓰기를 지원한다:

  [
    {
      "isin": "123",
      "quantity": 1,
      "price": 1.2,
      "customer": "foo"
    }, {
      "isin": "456",
      "quantity": 2,
      "price": 1.4,
      "customer": "bar"
    } 
  ]

JSON 리소스는 개별 아이템을 가진 JSON 객체 배열이라고 가정한다. 스프링 배치는 특정 JSON 라이브러리에 연결되지 않는다.

6.7.1. JsonItemReader

제이선아이템리더(JsonItemReader)는 JSON 파싱 및 바인딩을 org.springframework.batch.item.json.JsonObjectReader 인터페이스 구현체에 위임한다. 이 인터페이스는 청크에서 JSON 객체를 읽기 위해 스트리밍 API를 사용하여 구현하기 위한 것이다. 현재 두 가지 구현체가 제공된다.

  • org.springframework.batch.item.json.JacksonJsonObjectReader를 통한 잭슨(Jackson)
  • the org.springframework.batch.item.json.GsonJsonObjectReader를 통한 지선(Gson)

JSON 레코드를 처리하려면 다음내용이 필요하다:

  • 리소스(Resource): 읽을 JSON 파일을 나타내는 스프링 리소스
  • 제이선오브젝트리더(JsonObjectReader): JSON 객체를 파싱하고 아이템에 바인딩하는 JSON 객체 리더(reader)

다음 예제는 이전 JSON 리소스 org/springframework/batch/item/json/trades.json잭슨(Jackson) 기반 제이선오브젝트리더(JsonObjectReader)와 함께 작동하는 제이선아이템리더(JsonItemReader)를 정의하는 방법을 보여준다:

  @Bean
  public JsonItemReader<Trade> jsonItemReader() {
     return new JsonItemReaderBuilder<Trade>()
              .jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
              .resource(new ClassPathResource("trades.json"))
              .name("tradeJsonItemReader")
              .build();
  }

6.7.2. JsonFileItemWriter

제이선파일아이템라이터(JsonFileItemWriter)는 아이템에 마샬링(marshalling)을 org.springframework.batch.item.json.JsonObjectMarshaller 인터페이스에 위임한다. 이 인터페이스의 기능은 객체를 가져와 JSON 스트링(String)으로 마샬링하는 것이다. 현재 두 가지 구현체가 제공된다:

  • org.springframework.batch.item.json.JacksonJsonObjectMarshaller를 통한 잭슨(Jackson)
  • org.springframework.batch.item.json.GsonJsonObjectMarshaller를 통한 지선(Gson)

JSON 레코드를 작성하려면, 다음 내용이 필요하다:

  • 리소스(Resource): 작성할 JSON 파일을 나타내는 스프링 리소스.
  • 제이선오브젝트마살러(JsonObjectMarshaller): 객체를 JSON 형식으로 마샬링하는 JSON 객체 마샬러(marshaller).

다음 예제는 제이선파일아이템라이터(JsonFileItemWriter)를 정의하는 방법을 보여준다:

  @Bean
  public JsonFileItemWriter<Trade> jsonFileItemWriter() {
     return new JsonFileItemWriterBuilder<Trade>()
            .jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
            .resource(new ClassPathResource("trades.json"))
            .name("tradeJsonFileItemWriter")
            .build();
  }

6.8. Multi-File Input

일반적으로 단일 스텝 내에서 여러 파일을 처리한다. 파일 포맷이 모두 동일하다고 가정하면 멀티리소스아이템리더(MultiResourceItemReader)는 XML 및 플랫 파일 처리 모두에 대해 이러한 타입의 입력을 지원한다. 디렉토리에 있는 다음 파일을 생각해보자:

file-1.txt  file-2.txt  ignored.txt

file-1.txt 및 file-2.txt는 동일 포맷이며, 비즈니스상의 이유로 함께 처리되어야 한다. 멀티리소스아이템리더(MultiResourceItemReader)는 와일드카드(*)를 사용하여 두 파일을 읽는 데 사용할 수 있다.

다음 예는 XML에서 와일드카드를 사용하여 파일을 읽는 방법을 보여준다: XML 구성

  <bean id="multiResourceReader" class="org.spr...MultiResourceItemReader">
    <property name="resources" value="classpath:data/input/file-*.txt" />
    <property name="delegate" ref="flatFileItemReader" />
  </bean>

다음 예제는 자바에서 와일드카드를 사용하여 파일을 읽는 방법을 보여준다: 자바 구성

  @Bean
  public MultiResourceItemReader multiResourceReader() {
      return new MultiResourceItemReaderBuilder<Foo>()
              .delegate(flatFileItemReader())
              .resources(resources())
              .build();
  }

참조된 델리게이트(delegate)는 간단한 플랫파일아이템리더(FlatFileItemReader)이다. 위의 구성은 롤백 및 재시작 상황를 처리하면서, 두 파일 모두에서 입력을 읽는다. 아이템리더(ItemReader)와 마찬가지로, 추가 입력(파일)을 추가하면 재시작 시 잠재적으로 문제가 발생할 수 있다. 배치 잡이 성공적으로 완료될 때까지 개별 디렉토리에서 작업하는 것이 좋다.

재시작 상황에서 잡 실행 간에 리소스 순서가 유지되도록 MultiResourceItemReader#setComparator(Comparator)를 사용하여 입력 리소스를 정렬한다.

6.9. Database

대부분의 엔터프라이즈 애플리케이션 스타일과 마찬가지로, 데이터베이스는 배치를 위한 중앙 저장 메커니즘이다. 그러나, 배치는 시스템이 작동해야 하는 데이터 세트의 크기 때문에 다른 애플리케이션 스타일과 다르다. SQL 문이 100만(1 million) 로우를 반환하는 경우, 결과 집합은 모든 로우을 읽을 때까지 반환된 모든 결과를 메모리에 보유해야 한다. 스프링 배치는 이런 문제에 대해 두 가지 타입의 솔루션을 제공한다.

  • 커서 기반 아이템리더(ItemReader) 구현체
  • 페이징 아이템리더(ItemReader) 구현

6.9.1. Cursor-based ItemReader Implementations

데이터베이스 커서를 사용하는 것은 관계형 데이터 ‘스트리밍’ 문제에 대한 데이터베이스 솔루션이기 때문에, 일반적으로 대부분의 배치 개발의 기본적인 접근 방식이다. 자바 리절트셋(ResultSet) 클래스는 기본적으로 커서를 조작하기 위한 객체 지향 메커니즘이다. 리절트셋(ResultSet)은 현재 데이터 로우에 대한 커서를 유지한다. 리절트셋(ResultSet)에서 next를 호출하면 이 커서가 다음 로우로 이동한다. 스프링 배치의 커서 기반 아이템리더(ItemReader)의 구현은 초기화 시 커서를 열고, read를 호출마다 커서를 한 로우씩 이동하여 처리할 수 있는 매핑된 객체를 반환한다. 그런 다음 모든 리소스가 해제되었는지 확인하기 위해 close 메서드가 호출된다. 스프링 코어 JdbcTemplate은 콜백 패턴을 사용하여 리절트셋(ResultSet)의 모든 로우을 매핑하고 메서드 호출자에게 제어를 다시 반환하기 전에 닫음으로써 이 문제를 해결한다. 그러나 배치에서는 스텝이 완료될 때까지 기다려야 한다. 다음 이미지는 커서 기반 아이템리더(ItemReader)의 작동 방식에 대한 일반적인 다이어그램을 보여준다. 예제에서는 SQL을 사용하지만(SQL은 널리 알려져 있기 때문에) 모든 기술이 기본 접근 방식을 구현할 수 있다.

Cursor Example

이미지 20. 커서 예제

이 예는 기본 패턴을 보여준다. ID, NAMEBAR 세 컬럼이 있는 ‘FOO’ 테이블이 주어지면 ID가 1보다 크고 7보다 작은 모든 로우을 선택한다. 이렇게 하면 커서(로우 1)의 시작 부분이 ID 2에 위치한다. 이 로우의 결과는 매핑된 Foo 객체이다. read()를 다시 호출하면, 커서가 다음 로우(ID가 3인 Foo)로 이동한다. 이러한 결과는 각 read 호출 후에 기록되므로 객체가 가비지 컬렉터에 의해 제거(인스턴스 변수가 객체 참조를 유지하지 않는다고 가정) 될 수 있다.

JdbcCursorItemReader

JdbcCursorItemReader는 커서 기반 기술의 JDBC 구현체이다. 리절트셋(ResultSet)과 직접 작동하며 데이터소스(DataSource)에서 얻은 커넥션(connection)에서 실행하려면 SQL 문이 필요하다. 다음 데이터베이스 스키마가 예로 사용된다:

  CREATE TABLE CUSTOMER (
    ID BIGINT IDENTITY PRIMARY KEY,
    NAME VARCHAR(45),
    CREDIT FLOAT
  );

많은 사람들이 각 로우에 대해 도메인 객체를 사용하는 것을 선호하므로, 다음 예제에서는 로우매퍼(RowMapper) 인터페이스 구현체를 사용하여 커스터머크레딧(CustomerCredit) 객체를 매핑한다:

  public class CustomerCreditRowMapper implements RowMapper<CustomerCredit> {

    public static final String ID_COLUMN = "id";
    public static final String NAME_COLUMN = "name";
    public static final String CREDIT_COLUMN = "credit";
    
    public CustomerCredit mapRow(ResultSet rs, int rowNum) throws SQLException {
      CustomerCredit customerCredit = new CustomerCredit();

      customerCredit.setId(rs.getInt(ID_COLUMN));
      customerCredit.setName(rs.getString(NAME_COLUMN));
      customerCredit.setCredit(rs.getBigDecimal(CREDIT_COLUMN));

      return customerCredit;
    }
  }

제이디비씨커서아이템리더(JdbcCursorItemReader)JdbcTemplate과 키 인터페이스를 공유하므로, 아이템리더(ItemReader)와 대조하기 위해 JdbcTemplate을 사용하여 데이터를 읽는 방법의 예시를 보는 것이 유용하다. 이 예제에서는 CUSTOMER 데이터베이스에 1,000개의 로우가 있다고 가정한다. 첫 번째 예는 JdbcTemplate을 사용한다:

  //단순화를 위해, dataSource가 이미 확보되었다고 가정한다.
  JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  List customerCredits = jdbcTemplate.query("SELECT ID, NAME, CREDIT from CUSTOMER", new CustomerCreditRowMapper());

앞의 코드 스니펫을 실행한 후, customerCredits 리스트에는 1,000개의 CustomerCredit 객체가 포함되어 있다. 쿼리 메서드에서는, 데이터소스(DataSource)에서 커넥션(connection)을 얻고, 제공된 SQL이 실행되며, mapRow 메서드는 리절트셋(ResultSet)의 각 로우에 대하여 호출된다. 다음 예제에 표시된, JdbcCursorItemReader 접근 방식과 이를 대조해보자:

  JdbcCursorItemReader itemReader = new JdbcCursorItemReader();
  itemReader.setDataSource(dataSource);
  itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER");
  itemReader.setRowMapper(new CustomerCreditRowMapper());
  int counter = 0;
  ExecutionContext executionContext = new ExecutionContext();
  itemReader.open(executionContext);
  Object customerCredit = new Object();
  while(customerCredit != null) {
    customerCredit = itemReader.read();
    counter++; 
  }
  itemReader.close();

위 코드 스니펫을 실행한 후, 카운터는 1,000이 된다. 위의 코드가 반환한 customerCredit을 리스트에 넣었다면, 결과는 JdbcTemplate 예제와 정확히 동일했을 것이다. 그러나, 아이템리더(ItemReader)의 가장 큰 장점은 아이템을 ‘스트리밍’할 수 있다는 것이다. read 메서드를 한 번 호출하면, 아이템라이터(ItemWriter)에서 아이템을 작성한 후 read를 사용하여 다음 아이템을 가져올 수 있다. 이를 통해 아이템 읽기 및 쓰기를 ‘청크’단위로 수행하고 주기적으로 커밋할 수 있으며, 이는 고성능 배치 처리의 핵심이다. 또한 스프링 배치 스텝에 주입할 수 있도록 쉽게 구성할 수 있다.

다음 예제는 XML에서 아이템리더(ItemReader)를 스텝에 삽입하는 방법을 보여준다: The following example shows how to inject an ItemReader into a Step in XML: XML 구성

  <bean id="itemReader" class="org.spr...JdbcCursorItemReader">
    <property name="dataSource" ref="dataSource"/>
    <property name="sql" value="select ID, NAME, CREDIT from CUSTOMER"/>
    <property name="rowMapper">
      <bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
    </property>
  </bean>

다음 예제는 자바에서 아이템리더(ItemReader)를 스텝에 삽입하는 방법을 보여준다: 자바 구성

  @Bean
  public JdbcCursorItemReader<CustomerCredit> itemReader() {
    return new JdbcCursorItemReaderBuilder<CustomerCredit>()
            .dataSource(this.dataSource)
            .name("creditReader")
            .sql("select ID, NAME, CREDIT from CUSTOMER")
            .rowMapper(new CustomerCreditRowMapper())
            .build();
  }

Additional Properties

자바에는 커서를 여는 다양한 옵션이 있기 때문에, 다음 표에 설명된 대로 JdbcCursorItemReader에 설정할 수 있는 많은 프로퍼티가 있다:

테이블 16. JdbcCursorItemReader 프로퍼티

ignoreWarningsSQLWarning이 기록되는지 또는 예외가 발생하는지 여부를 결정한다. 기본값은 true(경고가 기록됨을 의미)이다.
fetchSize아이템리더(ItemReader)가 사용하는 리절트셋(ResultSet) 객체에 더 많은 로우가 필요할 때 데이터베이스에서 가져와야 하는 로우 수에 대한 힌트를 JDBC 드라이버에 제공한다. 기본적으로 힌트가 제공되지 않는다.
maxRows기본 리절트셋(ResultSet) 이 한 번에 보유할 수 있는 최대 로우 수에 대한 제한을 설정한다.
queryTimeout드라이버가 스테이트먼트(Statement) 객체가 실행되기를 기다리는 시간(초)을 설정한다. 제한을 초과하면 데이터엑세스익셉션(DataAccessException)이 발생한다. (자세한 내용은 드라이버 공급업체 설명서(driver vendor documentation)를 참조하자)
verifyCursorPosition아이템리더(ItemReader)가 보유한 동일한 리절트셋(ResultSet)로우매퍼(RowMapper)로 전달되기 때문에, 사용자가 ResultSet.next()를 직접 호출할 수 있으며, 이로 인해 리더(reader)의 내부 카운트에 문제가 발생할 수 있다. 이 값을 true로 설정하면 로우매퍼(RowMapper) 호출 후 커서 위치가 이전과 동일하지 않으면 예외가 발생한다.
saveStateItemStream#update(ExecutionContext)에서 제공하는 익스큐션컨텍스트(ExecutionContext)에 리더(reader)의 상태를 저장해야 하는지 여부를 나타낸다. 기본값은 true이다.
driverSupportsAbsoluteJDBC 드라이버가 리절트셋(ResultSet)에서 absolute row 설정을 지원하는지 여부를 나타낸다. ResultSet.absolute()를 지원하는 JDBC 드라이버에 대해 true로 설정하는 것이 좋다. 특히 대규모 데이터로 작업하는 동안 스템이 실패하는 경우, 성능이 향상될 수 있기 때문이다. 기본값은 false이다.
setUseSharedExtendedConnection커서에 사용된 커넥션(connection)이 다른 모든 처리에서 사용되어야 하는지 여부를 나타내므로, 동일한 트랜잭션을 공유한다. 이 플래그를 true로 설정하면 각 커밋 후에 커넥션(connection)이 닫히고 해제되는 것을 방지하기 위해 익스텐드커넥션데이터소스프록시(ExtendedConnectionDataSourceProxy)에서 데이터소스(DataSource)를 래핑해야 한다. 이 옵션을 true로 설정하면, ‘READ_ONLY’ 및 ‘HOLD_CURSORS_OVER_COMMIT’ 옵션으로 커서를 여는 스테이트먼트(statement)가 생성된다. 이렇게 하면 스텝 처리에서 수행된 트랜잭션 시작 및 커밋에 대해 커서를 열어 둘 수 있다. 이 기능을 사용하려면, 이를 지원하는 데이터베이스와 JDBC 3.0 이상을 지원하는 JDBC 드라이버가 필요하다. 기본값은 false이다.

HibernateCursorItemReader

일반 스프링 사용자가 Jdbc템플릿(JdbcTemplate) 또는 하이버네이트템플릿(HibernateTemplate)의 ORM 솔루션 사용 여부에 대한 고민을 하는것 것처럼, 스프링 배치 사용자도 동일한 고민을 한다. 하이버네이트커서아이템리더(HibernateCursorItemReader)는 커서 기술의 하이버네이트(Hibernate) 구현체이다. 배치에서 하이버네이트의 사용은 상당히 논란이 되어 왔다. 이는 하이버네이트가 원래 온라인 애플리케이션 스타일을 지원하기 위해 개발되었기 때문이다. 그러나 이것이 배치에 사용할 수 없다는 의미는 아니다. 이 문제를 해결하는 가장 쉬운 방법은 표준 세션이 아닌 스테이트리스세션(StatelessSession)을 사용하는 것이다. 이렇게 하면 하이버네이트를 사용하는 배치 시나리오에서 문제를 일으킬 수 있는 모든 캐싱(caching) 및 더티 체크(dirty checking)가 제거된다. 스테이트리스(stateless) 세션과 노멀 하이버네이트(normal hibernate) 세션 간의 차이점에 대한 자세한 내용은 하이버네이트 릴리스 설명서를 참조하자. 하이버네이트커서아이템리더(HibernateCursorItemReader)를 사용하면 HQL 문을 선언하고, Jdbc커서아이템리더(JdbcCursorItemReader)와 동일한 방식으로 읽기 위해 호출당 하나의 아이템을 다시 전달하는, 세션팩토리(SessionFactory)를 전달한다. 다음 예제는은 JDBC 리더와 동일한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다:

  HibernateCursorItemReader itemReader = new HibernateCursorItemReader();
  itemReader.setQueryString("from CustomerCredit");
  //단순화를 위해, sessionFactory를 이미 획득했다고 가정한다.
  itemReader.setSessionFactory(sessionFactory);
  itemReader.setUseStatelessSession(true);
  int counter = 0;
  ExecutionContext executionContext = new ExecutionContext();
  itemReader.open(executionContext);
  Object customerCredit = new Object();
  while(customerCredit != null){
      customerCredit = itemReader.read();
      counter++; 
  }
  itemReader.close();

이 구성된 아이템리더(ItemReader)는 하이버네이트 매핑 파일이 커스터머(Customer) 테이블에 대해 올바르게 생성됐다고 가정할 때, Jdbc커서아이템리더(JdbcCursorItemReader)에서 설명한 것과 똑같은 방식으로 커스터머크레딧(CustomerCredit) 객체를 반환한다. ‘useStatelessSession’ 프로퍼티의 기본값은 true이지만 켜거나 끄는 기능을 확인해보기 위해 추가됐다. setFetchSize 프로퍼티를 사용하여 기본 커서의 크기를 설정할 수 있다는 점도 주목해 볼만 하다. Jdbc커서아이템리더(JdbcCursorItemReader)와 마찬가지로, 구성은 간단하다.

다음 예제는 XML에 하이버네이트 아이템리더(ItemReader)를 주입하는 방법을 보여준다: XML 구성

  <bean id="itemReader" class="org.springframework.batch.item.database.HibernateCursorItemReader">
    <property name="sessionFactory" ref="sessionFactory" />
    <property name="queryString" value="from CustomerCredit" />
  </bean>

다음 예제는 자바에 하이버네이트 아이템리더(ItemReader)를 주입하는 방법을 보여준다: 자바 구성

  @Bean
  public HibernateCursorItemReader itemReader(SessionFactory sessionFactory) {
    return new HibernateCursorItemReaderBuilder<CustomerCredit>()
            .name("creditReader")
            .sessionFactory(sessionFactory)
            .queryString("from CustomerCredit")
            .build();
  }

StoredProcedureItemReader

저장 프로시저를 사용하여 커서 데이터를 가져와야 하는 경우가 있다. 스토어드프로시저아이템리더(StoredProcedureItemReader)는 커서를 얻기 위해 쿼리를 실행하는 대신 커서를 반환하는 저장 프로시저를 실행한다는 점을 제외하면 Jdbc커서아이템리더(JdbcCursorItemReader)와 유사하다. 저장 프로시저는 세 가지 방법으로 커서를 반환할 수 있다:

  • 반환된 리절트셋(ResultSet)(SQL Server, Sybase, DB2, Derby 및 MySQL에서 사용).
  • out 파라미터로 반환된 ref-cursor로(Oracle 및 PostgreSQL에서 사용)
  • 저장된 함수 호출의 반환 값으로.

다음 XML 예제는 이전 예제와 동일한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다: XML 구성

  <bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
    <property name="dataSource" ref="dataSource"/>
    <property name="procedureName" value="sp_customer_credit"/>
    <property name="rowMapper">
        <bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
    </property>
  </bean>

다음 자바 예제는 이전 예제와 동일한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다: 자바 구성

  @Bean
  public StoredProcedureItemReader reader(DataSource dataSource) {
    StoredProcedureItemReader reader = new StoredProcedureItemReader();
    reader.setDataSource(dataSource);
    reader.setProcedureName("sp_customer_credit");
    reader.setRowMapper(new CustomerCreditRowMapper());
    return reader;
  }

앞의 예는 반환된 결과로 리절트셋(ResultSet)를 제공(이전의 옵션 1번)하기 위해 저장 프로시저에 의존한다.

저장 프로시저가 ref-cursor(옵션 2번)를 반환한 경우, 반환된 ref-cursor인 out 파라미터의 위치를 ​​제공해야 한다.

다음 예는 XML에서 ref-cursor가 되는 첫 번째 파라미터로 작업하는 방법을 보여준다: XML 구성

  <bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
    <property name="dataSource" ref="dataSource"/>
    <property name="procedureName" value="sp_customer_credit"/>
    <property name="refCursorPosition" value="1"/>
    <property name="rowMapper">
      <bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
    </property>
  </bean>

다음 예는 자바에서 ref-cursor가 되는 첫 번째 파라미터로 작업하는 방법을 보여준다: 자바 구성

  @Bean
  public StoredProcedureItemReader reader(DataSource dataSource) {
    StoredProcedureItemReader reader = new StoredProcedureItemReader();
    reader.setDataSource(dataSource);
    reader.setProcedureName("sp_customer_credit");
    reader.setRowMapper(new CustomerCreditRowMapper());
    reader.setFunction(true);
    return reader;
  }

모든 경우에, 로우매퍼(RowMapper)데이터소스(DataSource) 및 실제 프로시저 이름을 정의해야 한다.

저장 프로시저 또는 함수가 파라미터를 받는 경우, 파라미터 프로퍼티를 사용하여 선언하고 설정해야 한다. 다음 예제는 오라클의 경우 세 개의 파라미터를 선언한다. 첫 번째는 ref-cursor를 반환하는 out 파라미터고 두 번째와 세 번째는 INTEGER 타입의 값을 갖는 in 파라미터이다.

다음 예는 XML에서 파라미터로 작업하는 방법을 보여준다: XML 구성

  <bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
    <property name="dataSource" ref="dataSource"/>
    <property name="procedureName" value="spring.cursor_func"/>
    <property name="parameters">
      <list>
        <bean class="org.springframework.jdbc.core.SqlOutParameter">
          <constructor-arg index="0" value="newid"/>
          <constructor-arg index="1">
              <util:constant static-field="oracle.jdbc.OracleTypes.CURSOR"/>
          </constructor-arg>
        </bean>
        <bean class="org.springframework.jdbc.core.SqlParameter">
          <constructor-arg index="0" value="amount"/>
          <constructor-arg index="1">
              <util:constant static-field="java.sql.Types.INTEGER"/>
          </constructor-arg>
        </bean>
        <bean class="org.springframework.jdbc.core.SqlParameter">
          <constructor-arg index="0" value="custid"/>
          <constructor-arg index="1">
              <util:constant static-field="java.sql.Types.INTEGER"/>
          </constructor-arg>
        </bean>
      </list>
    </property>
    <property name="refCursorPosition" value="1"/>
    <property name="rowMapper" ref="rowMapper"/>
    <property name="preparedStatementSetter" ref="parameterSetter"/>
  </bean>

다음 예는 자바에서 파라미터로 작업하는 방법을 보여준다: 자바 구성

  @Bean
  public StoredProcedureItemReader reader(DataSource dataSource) {
    List<SqlParameter> parameters = new ArrayList<>();
    parameters.add(new SqlOutParameter("newId", OracleTypes.CURSOR));
    parameters.add(new SqlParameter("amount", Types.INTEGER);
    parameters.add(new SqlParameter("custId", Types.INTEGER);

    StoredProcedureItemReader reader = new StoredProcedureItemReader();

    reader.setDataSource(dataSource);
    reader.setProcedureName("spring.cursor_func");
    reader.setParameters(parameters);
    reader.setRefCursorPosition(1);
    reader.setRowMapper(rowMapper());
    reader.setPreparedStatementSetter(parameterSetter());

    return reader;
  }

파라미터 선언 외에도, 호출에 대한 파라미터 값을 설정하는 프리페어드스테이트먼트세터(PreparedStatementSetter) 구현체를 지정해야 한다. 위의 Jdbc커서아이템리더(JdbcCursorItemReader)와 동일하게 작동한다. 나열된 모든 추가 프로퍼티는 스토어드프로시저아이템리더(StoredProcedureItemReader)에도 적용된다.

6.9.2. Paging ItemReader Implementations

데이터베이스 커서를 사용하는 대신 쿼리를 실행하여 결과의 일부를 가져오는 방법 있다. 이때 가져오는 일부를 ‘페이지(page)’라고 한다. 각 쿼리는 페이지에 반환할 시작 로우 번호와 페이지에 반환할 로우 수를 지정해야 한다.

JdbcPagingItemReader

페이징 아이템리더(paging ItemReader)의 구현체중 하나는 Jdbc페이징아이템리더(JdbcPagingItemReader)이다. Jdbc페이징아이템리더(JdbcPagingItemReader)는 페이지(page)를 구성하는 로우를 검색하는 데 필요한 SQL 쿼리를 제공하는 페이징쿼리프로바이더(PagingQueryProvider)가 필요하다. 각 데이터베이스마다 페이징(paging)을 제공하기 위한 전략이 있기 때문에, 지원되는 데이터베이스 타입마다 다른 페이징쿼리프로바이더(PagingQueryProvider)를 사용해야 한다. 또한 Sql페이징쿼리프로바이더팩토리빈(SqlPagingQueryProviderFactoryBean)을 사용하면 현재 사용 중인 데이터베이스를 자동으로 감지하고 적절한 페이징쿼리프로바이더(PagingQueryProvider) 구현체를 결정한다. 이렇게 하면 설정이 간소화되며, 권장 사항이다.

Sql페이징쿼리프로바이더팩토리빈(SqlPagingQueryProviderFactoryBean)을 사용하려면 셀렉트(select)문과 프롬(from)문을 지정해야 한다. 선택적으로 웨어(where) 절을 제공할 수도 있다. 이러한 절과 필수 sortKey는 SQL문을 작성하는 데 사용된다.

실행 중에 데이터가 손실되지 않도록 하려면 sortKey에 대한 유니크키 제약 조건을 갖는 것이 중요하다.

리더(reader)가 열린 후에, 다른 아이템리더(ItemReader)와 동일한 방식으로 읽기(read) 호출당 하나의 아이템을 다시 전달한다. 페이징은 추가 로우가 필요할 때 발생한다.

다음 XML 예제 구성은 이전에 표시된 커서 기반 아이템리더(ItemReaders)와 유사한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다: XML 구성

  <bean id="itemReader" class="org.spr...JdbcPagingItemReader">
    <property name="dataSource" ref="dataSource"/>
    <property name="queryProvider">
      <bean class="org.spr...SqlPagingQueryProviderFactoryBean">
        <property name="selectClause" value="select id, name, credit"/>
        <property name="fromClause" value="from customer"/>
        <property name="whereClause" value="where status=:status"/>
        <property name="sortKey" value="id"/>
      </bean>
    </property>
    <property name="parameterValues">
      <map>
        <entry key="status" value="NEW"/>
      </map>
    </property>
    <property name="pageSize" value="1000"/>
    <property name="rowMapper" ref="customerMapper"/>
  </bean>

다음 자바 예제 구성은 이전에 표시된 커서 기반 아이템리더(ItemReaders)와 유사한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다: 자바 구성

  @Bean
  public JdbcPagingItemReader itemReader(DataSource dataSource, PagingQueryProvider queryProvider) {
    Map<String, Object> parameterValues = new HashMap<>();
    parameterValues.put("status", "NEW");

    return new JdbcPagingItemReaderBuilder<CustomerCredit>()
              .name("creditReader")
              .dataSource(dataSource)
              .queryProvider(queryProvider)
              .parameterValues(parameterValues)
              .rowMapper(customerCreditMapper())
              .pageSize(1000)
              .build();
  }

  @Bean
  public SqlPagingQueryProviderFactoryBean queryProvider() {
    SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean();
    provider.setSelectClause("select id, name, credit");
    provider.setFromClause("from customer");
    provider.setWhereClause("where status=:status");
    provider.setSortKey("id");
    return provider;
  }

아이템리더(ItemReader)는 지정한 로우매퍼(RowMapper)를 사용하여, 커스터머크레딧(CustomerCredit) 객체를 반환한다. ‘pageSize’ 프로퍼티는 각 쿼리 실행에 대해 데이터베이스에서 읽은 엔터티 수를 결정한다.

‘parameterValues’ 프로퍼티를 사용하여 쿼리에 대한 파라미터 값의 맵(Map)을 지정할 수 있다. 웨어(where) 절에 파라미터를 사용하는 경우 각 아이템의 키는 파라미터명과 일치해야 한다. ‘?’ 자리 표시자(placeholder)를 사용하는 경우 각 아이템의 키는 1부터 시작하는 자리 표시자(placeholder)의 번호여야 한다.

JpaPagingItemReader

페이징 아이템리더(paging ItemReader)의 또 다른 구현체는 Jpa페이징아이템리더(JpaPagingItemReader)이다. JPA에는 하이버네이트 스테이트리스세션(Hibernate StatelessSession)과 유사한 개념이 없으므로, JPA 에서 제공하는 다른 기능을 사용해야 한다. JPA는 페이징을 지원하므로, 배치에 JPA를 사용하는 것은 이상하지 않다. 각 페이지를 읽은 후, 엔터티가 분리되고 연속성 컨텍스트(persistence context)가 지워져 페이지가 처리된 후 엔터티가 가비지 컬렉터(garbage collected)될 수 있다. Jpa페이징아이템리더(JpaPagingItemReader)를 사용하면 JPQL 문을 선언하고 엔터티매니저팩토리(EntityManagerFactory)를 전달할 수 있다. 그런 다음 다른 아이템리더(ItemReader)와 동일한 기본 방식으로 읽기(read) 호출당 하나의 아이템을 다시 전달한다. 페이징은 추가 엔터티가 필요할 때 발생한다.

다음 XML 예제 구성은 이전에 JDBC 리더(reader) 아이템리더(ItemReaders)와 유사한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다: XML 구성

  <bean id="itemReader" class="org.spr...JpaPagingItemReader">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
    <property name="queryString" value="select c from CustomerCredit c"/>
    <property name="pageSize" value="1000"/>
  </bean>

다음 자바 예제 구성은 이전에 JDBC 리더(reader) 아이템리더(ItemReaders)와 유사한 ‘커스터머 크레딧(customer credit)’ 예제를 사용한다: 자바 구성

  @Bean
  public JpaPagingItemReader itemReader() {
    return new JpaPagingItemReaderBuilder<CustomerCredit>()
              .name("creditReader")
              .entityManagerFactory(entityManagerFactory())
              .queryString("select c from CustomerCredit c")
              .pageSize(1000)
              .build();
  }

아이템리더(ItemReader)커스터머크레딧(CustomerCredit) 객체에 올바른 JPA 어노테이션 또는 ORM 매핑 파일이 있다고 가정하고, 위의 Jdbc페이징아이템리더(JdbcPagingItemReader)에 대해 설명한 것과 똑같은 방식으로 커스터머크레딧(CustomerCredit) 객체를 반환한다. ‘pageSize’ 프로퍼티는 각 쿼리에 대해 데이터베이스에서 읽을 엔터티 수를 결정한다.

6.9.3. Database ItemWriters

플랫 파일과 XML 파일에는 특정 아이템라이터(ItemWriter) 인스턴스가 있지만, 데이터베이스 세계에는 정확히 일치하는 것이 없다. 이는 트랜잭션이 필요한 모든 기능을 제공하기 때문이다. 아이템라이터(ItemWriter) 구현체는 파일이 트랜잭션인 것처럼 작동해야 하므로, 기록된 아이템을 추적하고 적절한 시간에 플러시(flushing) 또는 클리어(clearing)해야하기 때문에 파일에 필요하다. 쓰기(write)가 이미 트랜잭션에 포함되어 있으므로, 데이터베이스에는 이 기능이 필요하지 않다. 사용자는 아이템라이터(ItemWriter) 인터페이스를 구현하는 자체 DAO를 만들거나 일반 처리를 위해 커스텀 아이템라이터(ItemWriter)를 사용할 수 있다. 어느 쪽이든, 문제 없이 작동해야 한다. 주의해야 할 것은 배치 처리의 출력에서 성능 및 오류 처리 기능이다. 이것은 하이버네이트를 아이템라이터(ItemWriter)로 사용할 때 JDBC 배치 모드를 사용하면 동일한 문제가 있을 수 있다. 데이터베이스 출력의 배치 처리는 플러시(flush)에 주의를 기울이고 데이터에 오류가 없다고 가정할 때, 결함은 없다. 그러나, 다음 이미지와 같이 어떤 개별 아이템이 예외를 발생시켰는지 또는 개별 아이템에 책임이 있는지 여부를 알 수 있는 방법이 없기 때문에 작성하는 동안 오류가 발생하면 혼동이 발생할 수 있다:

Error On Flush

이미지 21. 플러시(Flush) 시 오류

아이템이 쓰기 전에 버퍼링되면, 커밋 직전에 버퍼가 플러시될 때까지 오류가 발생하지 않는다. 예를 들어 청크당 20개 아이템이 작성되고, 15번째 항목에서 데이터인테그리티바이얼레이션익셉션(DataIntegrityViolationException)이 발생한다고 가정하자. 스텝의 경우 20개 아이템이 모두 성공적으로 작성됐다. Session#flush()가 호출되면 버퍼가 비워지고 예외가 발생한다. 이 시점에서, 스텝이 할 수 있는 일은 없다. 트랜잭션을 롤백해야 한다. 일반적으로, 이 예외로 인해 아이템을 건너뛸 수 있으며(건너뛰기/재시도 정책에 따라), 다시 작성되지 않는다. 그러나 배치 상황에서는 어떤 아이템이 문제를 일으켰는지 알 수 있는 방법이 없다. 오류가 발생했을 때 전체 버퍼를 쓰고 있었다. 이 문제를 해결하는 유일한 방법은 다음 이미지와 같이 각 아이템 처리 후에 플러시하는 것이다:

Error On Write

이미지 22. 작성 시 오류

이것은 특히 하이버네이트를 사용할 때, 일반적인 사용 사례이며 아이템라이터(ItemWriter) 구현체에 대한 가이드라인은 write()에 대한 각 호출에서 플러시하는 것이다. 이렇게 하면 아이템을 안정적으로 건너뛸 수 있으며, 스프링 배치는 오류 후 아이템라이터(ItemWriter)에 대한 호출을 세부적으로 처리한다.

6.10. Reusing Existing Services

배치 시스템은 종종 다른 스타일의 애플리케이션과 함께 사용된다. 가장 일반적인 것은 온라인 시스템이지만, 각 애플리케이션에서 사용하는 필요한 대량(bulk) 데이터를 이동하여 통합 또는 씩 클라이언트 애플리케이션(thick client application)을 지원할 수도 있다. 이러한 이유로, 많은 사용자가 배치 잡 내에서 기존 DAO 또는 기타 서비스를 재사용하기를 원하는 경우가 많다. 스프링 컨테이너 자체는 필요한 클래스를 주입(injected)할 수 있도록 하여 이를 매우 쉽게 만든다. 그러나, 기존 서비스가 다른 스프링 배치 클래스의 의존성을 가지거나 실제로 스텝의 메인은 아이템리더(ItemReader)이기 때문에 아이템리더(ItemReader) 또는 아이템라이터(ItemWriter)로 작동해야 하는 경우가 있을 수 있다. 기존의 서비스가 다른 스프링 배치 클래스의 의존성을 충족시키기 위해 아이템리더(ItemReader)나 아이템라이터(ItemWriter)로 작동해야 하는 경우가 있을 수 있으며, 이 서비스가 실제로 스텝의 주요 아이템리더(ItemReader)인 경우일 수도 있다. 래핑(wrapping)이 필요한 각 서비스에 대해 어댑터 클래스를 작성하는 것은 매우 간단하지만, 일반적인 관심사이기 때문에 스프링 배치는 아이템리더어댑터(ItemReaderAdapter)아이템라이터어댑터(ItemWriterAdapter) 구현체를 제공한다. 두 클래스 모두 델리게이트(delegate) 패턴을 호출하여 표준 스프링 메서드를 구현므로 설정이 매우 간단하다.

다음 XML 예제는 아이템리더어댑터(ItemReaderAdapter)를 사용한다: XML 구성

  <bean id="itemReader" class="org.springframework.batch.item.adapter.ItemReaderAdapter">
    <property name="targetObject" ref="fooService" />
    <property name="targetMethod" value="generateFoo" />
  </bean>

  <bean id="fooService" class="org.springframework.batch.item.sample.FooService" />

다음 자바 예제는 아이템리더어댑터(ItemReaderAdapter)를 사용한다: 자바 구성

  @Bean
  public ItemReaderAdapter itemReader() {
    ItemReaderAdapter reader = new ItemReaderAdapter();
    reader.setTargetObject(fooService());
    reader.setTargetMethod("generateFoo");
    return reader;
  }

  @Bean
  public FooService fooService() {
    return new FooService();
  }

한 가지 중요한 점은, targetMethod의 기능이 읽기 기능과 동일해야 한다는 것이다. 읽기가 소진되면: null을 반환한다. 그렇지 않으면, 객체(Object)를 반환한다.

다른 것은 아이템라이터(ItemWriter)의 구현에 따라 무한 루프(infinite loop) 또는 잘못된 실패(incorrect failure)를 유발하여, 프레임워크 처리가 언제 종료되어야 하는지 알지 못할 수도 있다.

다음 XML 예제에서는 아이템라이터어댑터(ItemWriterAdapter)를 사용한다: XML 구성

  <bean id="itemWriter" class="org.springframework.batch.item.adapter.ItemWriterAdapter">
    <property name="targetObject" ref="fooService" />
    <property name="targetMethod" value="processFoo" />
  </bean>

  <bean id="fooService" class="org.springframework.batch.item.sample.FooService" />

다음 자바 예제에서는 아이템라이터어댑터(ItemWriterAdapter)를 사용한다: 자바 구성

  @Bean
  public ItemWriterAdapter itemWriter() {
      ItemWriterAdapter writer = new ItemWriterAdapter();
      writer.setTargetObject(fooService());
      writer.setTargetMethod("processFoo");
      return writer;
  }

  @Bean
  public FooService fooService() {
      return new FooService();
  }

6.11. Preventing State Persistence

기본적으로, 모든 아이템리더(ItemReader)아이템라이터(ItemWriter) 구현체는 커밋되기 전에 익스큐션컨텍스트(ExecutionContext)에 현재 상태를 저장한다. 그러나, 이것이 항상 원하는 동작이 아닐 수도 있다. 예를 들어, 많은 개발자는 프로세스 표시기(process indicator)를 사용하여 데이터베이스 리더(reader)를 ‘재실행 가능(rerunnable)’하게 만들기로 한다. 추가 컬럼(extra column)이 입력 데이터에 추가되어 처리 여부를 나타낸다. 특정 레코드를 읽거나 (쓸 때) 처리 플래그가 false에서 true로 바뀐다. 그런 다음 SQL 문은 where PROCESSED_IND = false와 같은 추가 문을 where 절에 포함할 수 있으므로 재시작하는 경우 처리되지 않은 레코드만 반환되도록 할 수 있다. 이 상황에서는, 현재 로우 번호와 같은 상태는, 재시작할 때 관련이 없으므로, 저장하지 않는 것이 좋다. 이러한 이유로, 모든 리더(reader)라이터(writer)는 ‘saveState’ 프로퍼티를 포함한다.

XML에서 다음 빈 정의는 상태 지속성(state persistence)을 유지하지 않는 방법을 보여준다: XML 구성

  <bean id="playerSummarizationSource" class="org.spr...JdbcCursorItemReader">
    <property name="dataSource" ref="dataSource" />
    <property name="rowMapper">
      <bean class="org.springframework.batch.sample.PlayerSummaryMapper" />
    </property>
    <property name="saveState" value="false" />
    <property name="sql">
      <value>
        SELECT games.player_id, games.year_no, SUM(COMPLETES),
        SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD),
        SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS),
        SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)
        from games, players where players.player_id =
        games.player_id group by games.player_id, games.year_no
      </value>
    </property>
  </bean>

자바에서 다음 빈 정의는 상태 지속성(state persistence)을 유지하지 않는 방법을 보여준다: 자바 구성

  @Bean
  public JdbcCursorItemReader playerSummarizationSource(DataSource dataSource) {
      return new JdbcCursorItemReaderBuilder<PlayerSummary>()
              .dataSource(dataSource)
              .rowMapper(new PlayerSummaryMapper())
              .saveState(false)
              .sql("SELECT games.player_id, games.year_no, SUM(COMPLETES),"
                + "SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD),"
                + "SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS),"
                + "SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)"
                + "from games, players where players.player_id ="
                + "games.player_id group by games.player_id, games.year_no")
            .build();
  }

위에서 구성한 아이템리더(ItemReader)는 참여하는 모든 실행(execution)에 대해 익스큐션컨텍스트(ExecutionContext)에 항목을 만들지 않는다.

6.12. Creating Custom ItemReaders and ItemWriters

지금까지, 이 장에서는 스프링 배치에서 읽기 및 쓰기의 기본 기능과 이를 위한 몇 가지 일반적인 구현체에 대해 얘기했다. 그러나, 이들은 모두 매우 일반적이며 기본 구현체로 다루지 못하는 많은 상황이 있다. 이 장에서는, 간단한 예제를 사용하여, 커스텀 아이템리더(ItemReader)아이템라이터(ItemWriter)구현체를 만들고 기능을 올바르게 구현하는 방법을 알려준다. 아이템리더(ItemReader)는 또한 리더(reader) 또는 라이터(writer)를 재시작 가능하게 만드는 방법을 설명하기 위해 아이템스트림(ItemStream)을 구현한다.

6.12.1. Custom ItemReader Example

이 예제는 제공된 목록을 읽는 간단한 아이템리더(ItemReader)를 구현한다. 다음 코드와 같이 아이템리더(ItemReader)의 가장 기본적인 기능인 read 메서드를 구현하는 것으로 시작한다:

  public class CustomItemReader<T> implements ItemReader<T> {
    List<T> items;
    
    public CustomItemReader(List<T> items) {
        this.items = items;
    }

    public T read() throws Exception, UnexpectedInputException, NonTransientResourceException, ParseException {
      if (!items.isEmpty()) {
        return items.remove(0);
      }

      return null;
    }
  }

앞의 클래스는 아이템 목록을 가져와 한 번에 하나씩 반환하고 목록에서 각 아이템을 제거한다. 목록이 비어 있으면, null을 반환하므로, 다음 테스트 코드와 같이 아이템리더(ItemReader)의 가장 기본적인 요구 사항을 충족한다:

  List<String> items = new ArrayList<>();
  items.add("1");
  items.add("2");
  items.add("3");

  ItemReader itemReader = new CustomItemReader<>(items);
  assertEquals("1", itemReader.read());
  assertEquals("2", itemReader.read());
  assertEquals("3", itemReader.read());
  assertNull(itemReader.read());

Making the ItemReader Restartable

마지막 과제는 아이템리더(ItemReader)를 재시작 가능하게 만드는 것이다. 현재 처리가 중단됐고, 재시작되면 아이템리더(ItemReader)가 처음부터 시작해야 한다. 이것은 실제로 많은 상황에서 유효하지만 배치 잡이 중단된 지점에서 재시작하는 것이 더 나은 경우도 있다. 판별할 수 있는 요소는 리더(reader)가 상태를 저장하고 있는지 저장하고 있지 않은지 여부이다. 상태 비저장 리더(reader)는 재시작 가능성을 걱정할 필요가 없지만, 상태 저장 리더(reader)는 재시작 시 마지막으로 처리한 상태를 재구성해야 한다. 이러한 이유로 가능하면 커스텀 리더(reader)를 상태 비저장 상태로 유지하여 재시작 가능성에 대해 걱정할 필요가 없도록 하는 것이 좋다.

상태를 저장해야 하는 경우, 아이템스트림(ItemStream) 인터페이스를 사용해야 한다:

  public class CustomItemReader<T> implements ItemReader<T>, ItemStream {
    List<T> items;
    int currentIndex = 0;
    private static final String CURRENT_INDEX = "current.index";
    
    public CustomItemReader(List<T> items) {
        this.items = items;
    }

    public T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        if (currentIndex < items.size()) {
            return items.get(currentIndex++);
        }
        return null;
      }

    public void open(ExecutionContext executionContext) throws ItemStreamException {
      if (executionContext.containsKey(CURRENT_INDEX)) {
        currentIndex = new Long(executionContext.getLong(CURRENT_INDEX)).intValue();
      } else {
        currentIndex = 0;
      } 
    }

    public void update(ExecutionContext executionContext) throws ItemStreamException {
        executionContext.putLong(CURRENT_INDEX, new Long(currentIndex).longValue());
    }

    public void close() throws ItemStreamException {}
  }

아이템스트림(ItemStream)의 update 메서드를 호출할 때마다, 아이템리더(ItemReader)의 현재 인덱스는 ‘current.index’ 키와 함께 제공된 익스큐션컨텍스트(ExecutionContext)에 저장된다. 아이템스트림(ItemStream)의 open 메서드가 호출되면, 해당 키를 가진 아이템이 포함되어 있는지 확인하기 위해 익스큐션컨텍스트(ExecutionContext)를 검사한다. 키가 발견되면 현재 색인이 해당 위치로 이동된다. 이것은 매우 간단한 예이지만 여전히 많이 쓰인다.

  ExecutionContext executionContext = new ExecutionContext();
  ((ItemStream)itemReader).open(executionContext);
  assertEquals("1", itemReader.read());
  ((ItemStream)itemReader).update(executionContext);

  List<String> items = new ArrayList<>();
  items.add("1");
  items.add("2");
  items.add("3");
  itemReader = new CustomItemReader<>(items);

  ((ItemStream)itemReader).open(executionContext);
  assertEquals("2", itemReader.read());

대부분의 아이템리더(ItemReader)에는 훨씬 더 정교한 재시작 로직이 있다. 예를 들어 Jdbc커서아이템리더(JdbcCursorItemReader)는 마지막으로 처리된 로우의 ID를 커서에 저장한다.

익스큐션컨텍스트(ExecutionContext) 내에서 사용되는 키를 충분히 고려해야 한다는 점도 주목할만 하다. 스텝 내의 모든 아이템스트림(ItemStream)에 동일한 익스큐션컨텍스트(ExecutionContext)가 사용되기 때문이다. 대부분의 경우 키 앞에 클래스 이름을 추가하는 것만으로도 고유성을 보장하기에 충분하다. 그러나, 동일한 타입의 아이템스트림(ItemStream) 두 개가 동일한 스텝에서 사용되는 드문 경우(출력에 두 개의 파일이 필요한 경우 발생할 수 있음)에는 더 고유한 이름이 필요한다. 이러한 이유로 많은 스프링 배치 아이템리더(ItemReader)아이템라이터(ItemWriter) 구현체에는 이 키 이름을 오버라이드(overridden) 할 수 있는 setName() 프로퍼티가 있다.

6.12.2. Custom ItemWriter Example

커스텀 아이템라이터(ItemWriter)를 구현하는 것은 위의 아이템리더(ItemReader) 예제와 많이 유사하지만 예제를 새로 작성할 정도로 충분히 다르다. 그러나 재시작에 대한 내용을 추가하는 것은 기본적으로 동일하므로 이 예제에서는 다루지 않는다. 아이템리더(ItemReader) 예제와 마찬가지로, 예제를 최대한 단순하게 유지하기 위해 List가 사용된다.

  public class CustomItemWriter<T> implements ItemWriter<T> {
    List<T> output = TransactionAwareProxyFactory.createTransactionalList();
    
    public void write(Chunk<? extends T> items) throws Exception {
      output.addAll(items);
    }

    public List<T> getOutput() {
      return output;
    } 
  }

Making the ItemWriter Restartable

아이템라이터(ItemWriter)를 재시작 가능하게 만들려면, 아이템리더(ItemReader)와 동일한 프로세스를 따라, 아이템스트림(ItemStream) 인터페이스를 추가 및 구현하여 익스큐션 컨텍스트(execution context)를 동기화한다. 예제에서는 처리된 아이템 수를 계산하고 이를 마지막(footer) 레코드로 추가해야 할 수 있다. 필요한 경우 스트림이 다시 열리면 익스큐션 컨텍스트(execution context)에서 카운터가 재구성되도록 아이템라이터(ItemWriter)에서 아이템스트림(ItemStream)을 구현할 수 있다. 많은 사례에서 커스텀 아이템라이터(ItemWriter)는 재시작할 수 있는 다른 라이터(writer)(예: 파일에 쓸 때)에 위임하거나 트랜잭션 리소스에 쓰며 상태 비저장이기 때문에 재시작할 필요가 없다.

상태 저장 라이터(writer)가 있는 경우 아이템라이터(ItemWriter)뿐 아니라 아이템스트림(ItemStream)도 구현해야 한다. 라이터(writer)의 클라이언트는 아이템스트림(ItemStream)을 인식해야 하므로 구성에 스트림을 등록해야 할 수도 있다.

6.13. Item Reader and Writer Implementations

이 장에서는 이전 장에서 아직 논의되지 않은 리더(reader)와 작가(writer)를 소개합한다.

6.13.1. Decorators

경우에 따라 사용자는 기존 아이템리더(ItemReader)에 특수한 동작을 추가해야 한다. 스프링 배치는 아이템리더(ItemReader)아이템라이터(ItemWriter) 구현에 추가 동작을 추가할 수 있는 기본 데코레이터를 제공한다.

Spring Batch에는 다음 데코레이터들이 포함되어있다.:

  • SynchronizedItemStreamReader
  • SingleItemPeekableItemReader
  • SynchronizedItemStreamWriter
  • MultiResourceItemWriter
  • ClassifierCompositeItemWriter
  • ClassifierCompositeItemProcessor

SynchronizedItemStreamReader

스레드로부터 안전하지 않은 아이템리더(ItemReader)를 사용할 때, 스프링 배치는 아이템리더(ItemReader)를 스레드로부터 안전하게 사용할 수 있는 싱크로나이즈드아이템스트림리더(SynchronizedItemStreamReader) 데코레이터를 제공한다. 스프링 배치는 싱크로나이즈드아이템스트림리더빌더(SynchronizedItemStreamReaderBuilder)를 제공하여 싱크로나이즈드아이템스트림리더(SynchronizedItemStreamReader)의 인스턴스를 구성한다.

예를 들어, 플랫파일아이템리더(FlatFileItemReader)는 스레드로부터 안전하지 않으며 멀티스레드 스텝에서 사용할 수 없다. 이 리더(reader)는 멀티스레드 스텝에서 안전하게 사용하기 위해 싱크로나이즈드아이템스트림리더(SynchronizedItemStreamReader)로 데코레이트할 수 있다.

다음은 이러한 리더(reader)를 데코레이터하는 방법의 예이다.:

  @Bean
  public SynchronizedItemStreamReader<Person> itemReader() {
    FlatFileItemReader<Person> flatFileItemReader = new FlatFileItemReaderBuilder<Person>()
                                                      .build();

    // 리더 프로퍼티 설정
    return new SynchronizedItemStreamReaderBuilder<Person>()
              .delegate(flatFileItemReader)
              .build();
  }

SingleItemPeekableItemReader

스프링 배치에는 아이템리더(ItemReader)peek 메서드를 추가하는 데코레이터가 있다. 이 peek 메서드를 사용하면 사용자가 아이템을 미리 볼 수 있다. peek에 대한 반복 호출은 동일한 아이템을 반환하며 이것은 read 메서드에서 반환된 다음 아이템이다. 스프링 배치는 싱글아이템피커블아이템리더(SingleItemPeekableItemReader)의 인스턴스를 생성하기 위해 싱글아이템피커블아이템리더빌더(SingleItemPeekableItemReaderBuilder)를 제공한다.

싱글아이템피커블아이템리더(SingleItemPeekableItemReader)peek 메서드는 멀티스레드 상황에서 반환 값을 확신할 수 없기 때문에 스레드로부터 안전하지 않다. peek이 스레드 중 하나만 다음 read 호출에서 해당 아이템을 가져온다.

SynchronizedItemStreamWriter

스레드로부터 안전하지 않은 아이템라이터(ItemWriter)를 사용할 때, 스프링 배치는 아이템라이터(ItemWriter)를 스레드로부터 안전하게 만드는 데 사용할 수 있는 싱크로나이즈드아이템스트림라이터(SynchronizedItemStreamWriter) 데코레이터를 제공한다. 스프링 배치는 싱크로나이즈드아이템스트림라이터빌더(SynchronizedItemStreamWriterBuilder)를 제공하여 싱크로나이즈드아이템스트림라이터(SynchronizedItemStreamWriter)의 인스턴스를 구성한다.

예를 들어, 플랫파일아이템라이터(FlatFileItemWriter)는 스레드로부터 안전하지 않으며, 멀티스레드 스텝에서 사용할 수 없다. 이 라이터(writer)는 멀티스레드 스텝에서 안전하게 사용하기 위해 싱크로나이즈드아이템스트림라이터(SynchronizedItemStreamWriter)로 데코레이트될 수 있다. 다음은 그러한 라이터(writer)를 데코레이트하는 방법의 예이다:

  @Bean
  public SynchronizedItemStreamWriter<Person> itemWriter() {
    FlatFileItemWriter<Person> flatFileItemWriter = new FlatFileItemWriterBuilder<Person>()
            .build();
  }

  // 라이터 프로퍼티스 설정
  return new SynchronizedItemStreamWriterBuilder<Person>()
          .delegate(flatFileItemWriter)
          .build();

MultiResourceItemWriter

멀티리소스아이템라이터(MultiResourceItemWriter)리소스어웨어아이템라이터아이템스트림(ResourceAwareItemWriterItemStream)을 래핑하고 현재 리소스에 기록된 아이템 수가 itemCountLimitPerResource를 초과할 때 새 출력 리소스를 만든다. 스프링 배치는 멀티리소스아이템라이터(MultiResourceItemWriter)의 인스턴스를 생성하기 위해 멀티리소스아이템라이터빌더(MultiResourceItemWriterBuilder) 제공한다.

ClassifierCompositeItemWriter

클래시파이어컴포짓아이템라이터(ClassifierCompositeItemWriter)는 제공된 클래시파이어(Classifier)를 통해 구현된 라우터(router) 패턴을 기반으로 각 아이템에 대한 아이템라이터(ItemWriter) 구현 컬렉션 중 하나를 호출한다. 모든 델리게이트가 스레드로부터 안전한 경우 구현체 또한 스레드로부터 안전하다. 스프링 배치는 클래시파이어컴포짓아이템라이터빌더(ClassifierCompositeItemWriterBuilder) 제공하여 클래시파이어컴포짓아이템라이터(ClassifierCompositeItemWriter)의 인스턴스를 구성한다.

ClassifierCompositeItemProcessor

클래시파이어컴포짓아이템프로세서(ClassifierCompositeItemProcessor)는 제공된 클래시파이어(Classifier)를 통해 구현된 라우터(router) 패턴을 기반으로 아이템프로세서(ItemProcessor) 구현체 중 하나를 호출하는 아이템프로세서(ItemProcessor)이다. 스프링 배치는 클래시파이어컴포짓아이템프로세서(ClassifierCompositeItemProcessor)의 인스턴스를 생성하기 위해 클래시파이어컴포짓아이템프로세서빌더(ClassifierCompositeItemProcessorBuilder)를 제공한다.

6.13.2. Messaging Readers And Writers

스프링 배치는 일반적으로 사용되는 메시징 시스템에 대해 다음과 같은 리더(reader) 및 라이터(writer)를 제공한다:

  • AmqpItemReader
  • AmqpItemWriter
  • JmsItemReader
  • JmsItemWriter
  • KafkaItemReader
  • KafkaItemWriter

AmqpItemReader

Amqp아이템리더(AmqpItemReader)는 교환에서 메시지를 수신하거나 변환하기 위해 Amqp템플릿(AmqpTemplate)을 사용하는 아이템리더(ItemReader)이다. 스프링 배치는 Amqp아이템리더빌더(AmqpItemReaderBuilder)를 제공하여 Amqp아티템리더(AmqpItemReader)의 인스턴스를 구성한다.

AmqpItemWriter

Amqp아이템라이터(AmqpItemWriter)Amqp템플릿(AmqpTemplate)을 사용하여 AMQP 교환에 메시지를 보내는 아이템라이터(ItemWriter)이다. 제공된 Amqp템플릿(AmqpTemplate)에 이름이 지정되지 않은 경우 이름 없는 교환(exchange)로 메시지가 전송된다. 스프링 배치는 Amqp아이템라이터빌더(AmqpItemWriterBuilder)를 제공하여 Amqp아이템라이터(AmqpItemWriter)의 인스턴스를 구성한다.

JmsItemReader

Jms아이템리더(JmsItemReader)Jms템플릿(JmsTemplate)을 사용하는 JMS용 아이템리더(ItemReader)이다. 템플릿에는 read() 메서드에 대한 아이템을 제공하는 데, 사용되는 기본 대상이 있어야 한다. 스프링배치는 Jms아이템리더(JmsItemReader)의 인스턴스를 구성하기 위해 Jms아이템리더빌더(JmsItemReaderBuilder)를 제공한다.

JmsItemWriter

Jms아이템라이터(JmsItemWriter)Jms템플릿(JmsTemplate)을 사용하는 JMS용 아이템라이터(ItemWriter)이다. 템플릿에는 write(List)에서 아이템을 보내는 데, 사용되는 기본 대상이 있어야 한다 스프링 배치는 Jsm아이템라이터(JmsItemWriter)의 인스턴스를 생성하기 위해 Jms아이템라이터빌더(JmsItemWriterBuilder)를 제공한다. The JmsItemWriter is an ItemWriter for JMS that uses a JmsTemplate. The template should have a default destination, which is used to send items in write(List). Spring Batch provides a JmsItemWriterBuilder to construct an instance of the JmsItemWriter.

KafkaItemReader

카프카아이템리더(KafkaItemReader)는 아파치 카프카 토픽(topic)에 대한 아이템리더(ItemReader)이다. 동일한 토픽의 여러 파티션에서 메시지를 읽도록 구성할 수 있다. 재시작 기능을 지원하기 위해 익스큐션 컨텍스트에 메시지 오프셋을 저장한다. 스프링 배치는 카프카아이템리더(KafkaItemReader)의 인스턴스를 구성하기 위해 카프카아이템리더빌더(KafkaItemReaderBuilder)를 제공한다.

KafkaItemWriter

카프카아이템라이터(KafkaItemWriter)는 카프카템플릿(KafkaTemplate)을 사용하여 이벤트를 기본 토픽으로 보내는 아파치 카프카용 아이템라이터(ItemWriter)이다. 스프링 배치는 카프카아이템라이터빌더(KafkaItemWriterBuider)를 제공하여 카프카아이템라이터(KafkaItemWriter)의 인스턴스를 구성한다.

6.13.3. Database Readers

스프링 배치는 다음과 같은 데이터베이스 리더를 제공한다:

  • Neo4jItemReader
  • MongoItemReader
  • HibernateCursorItemReader
  • HibernatePagingItemReader
  • RepositoryItemReader

Neo4jItemReader

Neo4j아이템리더(Neo4jItemReader)는 페이징 기술을 사용하여 그래프 데이터베이스 Neo4j에서 객체를 읽는 아이템리더(ItemReader)이다. 스프링 배치는 Neo4j아이템리더(Neo4jItemReader)의 인스턴스를 생성하기 위해 Neo4j아이템리더빌더(Neo4jItemReaderBuilder)를 제공한다.

MongoItemReader

몽고아이템리더(MongoItemReader)는 페이징 기술을 사용하여 몽고DB(MongoDB)에서 문서를 읽는 아이템리더(ItemReader)이다. Spring Batch는 몽고아이템리더(MongoItemReader)의 인스턴스를 생성하기 위해 몽고아이템리더빌더(MongoItemReaderBuilder)를 제공한다.

HibernateCursorItemReader

하이버네이트커서아이템리더(HibernateCursorItemReader)하이버네이트(Hibernate)로 데이터베이스 레코드를 읽기 위한 아이템스트림리더(ItemStreamReader)이다. HQL 쿼리를 실행한 다음 초기화되면 read() 메서드가 호출될 때 결과를 반복하여 현재 로우에 해당하는 객체를 반환한다. 스프링 배치는 하이버네이트커서아이템리더(HibernateCursorItemReader)의 인스턴스를 구성하기 위해 하이버네이트커서아이템리더빌더(HibernateCursorItemReaderBuilder)를 제공한다.

HibernatePagingItemReader

하이버네이트페이징아이템리더(HibernatePagingItemReader)하이버네이트(Hibernate)로 데이터베이스 레코드를 읽기 위해 한 번에 고정된 수의 아이템까지 읽기 위한 아이템리더(ItemReader)이다. 스프링 배치는 하이버네이트페이징아이템리더(HibernatePagingItemReader)의 인스턴스를 구성하기 위해 하이버네이트페이징아이템리더빌더(HibernatePagingItemReaderBuilder)를 제공한다.

RepositoryItemReader

리포지터리아이템리더(RepositoryItemReader)페이징엔소팅리포지터리(PagingAndSortingRepository)를 사용하여 레코드를 읽는 아이템리더(ItemReader)이다. 스프링 배치는 리포지터리아이템리더(RepositoryItemReader)의 인스턴스를 생성하기 위해 리포지터리아이템리더빌더(RepositoryItemReaderBuilder)를 제공한다.

6.13.4. Database Writers

스프링 배치는 다음과 같은 데이터베이스 라이터(writer)를 제공한다:

  • Neo4jItemWriter
  • MongoItemWriter
  • RepositoryItemWriter
  • HibernateItemWriter
  • JdbcBatchItemWriter
  • JpaItemWriter

Neo4jItemWriter

Neo4j아이템라이터(Neo4jItemWriter)는 Neo4j 데이터베이스에 쓰는 아이템라이터(ItemWriter)의 구현체이다. 스프링 배치는 Neo4j아이템라이터(Neo4jItemWriter)의 인스턴스를 생성하기 위해 Neo4j아이템라이터빌더(Neo4jItemWriterBuilder)를 제공한다.

MongoItemWriter

몽고아이템라이터(MongoItemWriter)는 스프링 데이터의 몽고오퍼레이션(MongoOperations) 구현체를 사용하여 몽고DB(MongoDB) 저장소에 쓰는 아이템라이터(ItemWriter) 구현체이다. 스프링 배치는 몽고아이템라이터(MongoItemWriter)의 인스턴스를 생성하기 위해 몽고아이템라이터빌더(MongoItemWriterBuilder)를 제공한다.

RepositoryItemWriter

리포지터리아이템라이터(RepositoryItemWriter)는 스프링 데이터의 Crud리포지터리(CrudRepository)에 대한 아이템라이터(ItemWriter) 래퍼이다. 스프링 배치는 리포지터리아이템라이터(RepositoryItemWriter)의 인스턴스를 구성하기 위해 리포지터리아이템라이터빌더(RepositoryItemWriterBuilder)를 제공한다.

HibernateItemWriter

하이버네이트아이템라이터(HibernateItemWriter)는 하이버네이트(Hibernate) 세션을 사용하여 현재 하이버네이트 세션의 일부가 아닌 엔터티를 저장하거나 업데이트하는 아이템라이터(ItemWriter)이다. 스프링 배치는 하이버네이트아이템라이터(HibernateItemWriter)의 인스턴스를 구성하기 위해 하이버네이트아이템라이터빌더(HibernateItemWriterBuilder)를 제공한다.

JdbcBatchItemWriter

Jdbc배치아이템라이터(JdbcBatchItemWriter)네임드파라미터Jdbc템플릿(NamedParameterJdbcTemplate)의 배치 기능을 사용하여 제공된 모든 아이템에 대한 배치 명령문(statement)을 실행하는 아이템라이터(ItemWriter)이다. 스프링 배치는 Jdbc배치아이템라이터(JdbcBatchItemWriter)의 인스턴스를 생성하기 위해 Jdbc배치아이템라이터빌더(JdbcBatchItemWriterBuilder)를 제공한다.

JpaItemWriter

Jpa아이템라이터(JpaItemWriter)는 JPA 엔터티매니저팩토리(EntityManagerFactory)를 사용하여 영속성 컨텍스트(persistence context)의 일부가 아닌 엔터티를 병합하는 아이템라이터(ItemWriter)이다. 스프링 배치는 Jpa아이템라이터(JpaItemWriter)의 인스턴스를 생성하기 위해 Jpa아이템라이터빌더(JpaItemWriterBuilder)를 제공한다.

6.13.5. Specialized Readers

스프링 배치는 다음과 같은 특별한 리더를 제공한다:

  • LdifReader
  • MappingLdifReader
  • AvroItemReader

LdifReader

Ldif리더(LdifReader)리소스에서 LDIF(LDAP 데이터 교환 포맷) 레코드를 읽고, 파싱한 다음 실행된 각 read에 대해 Ldap애트리뷰트(LdapAttribute) 객체를 반환한다. 스프링 배치는 Ldif리더(LdifReader)의 인스턴스를 구성하기 위해 Ldif리더빌더(LdifReaderBuilder)를 제공한다.

MappingLdifReader

매핑Ldif리더(MappingLdifReader)리소스에서 LDIF(LDAP 데이터 교환 포맷) 레코드를 읽고, 파싱한 다음 각 LDIF 레코드를 POJO(Plain Old Java Object)에 매핑한다. 각 read는 POJO를 반환한다. 스프링 배치는 매핑Ldif리더빌더(MappingLdifReaderBuilder)를 제공하여 매핑Ldif리더(MappingLdifReader)의 인스턴스를 구성한다.

AvroItemReader

Avro아이템리더(AvroItemReader)는 리소스에서 직렬화된 Avro 데이터를 읽는다. 각 read는 자바 클래스 또는 Avro 스키마(Schema)에서 지정한 타입의 인스턴스를 반환한다. 리더(reader)는 Avro 스키마를 포함하거나 포함하지 않는 입력에 대해 선택적으로 구성할 수 있다. 스프링 배치는 Avro아이템리더(AvroItemReader)의 인스턴스를 생성하기 위해 Avro아이템리더빌더(AvroItemReaderBuilder) 제공한다.

6.13.6. Specialized Writers

스프링 배치는 다음과 같은 특별한 라이터(writer)를 제공한다:

  • SimpleMailMessageItemWriter
  • AvroItemWriter

SimpleMailMessageItemWriter

심플메일메세지라이터(SimpleMailMessageItemWriter)는 메일 메시지를 보낼 수 있는 아이템라이터(ItemWriter)이다. 실제 메시지 전송은 메일센더(MailSender) 인스턴스에 위임한다. 스프링 배치는 심플메일메세지아이템라이터(SimpleMailMessageItemWriter)의 인스턴스를 구성하기 위해 심플메일메세지아이템라이터빌더(SimpleMailMessageItemWriterBuilder)를 제공한다.

AvroItemWriter

Avro아이템라이터(AvroItemWrite)는 주어진 타입 또는 스키마에 따라 자바 객체를 라이터블리소스(WriteableResource)로 직렬화(serializes)한다. 라이터(writer)는 출력에 Avro 스키마를 포함하거나 포함하지 않도록 선택적으로 구성할 수 있다. 스프링 배치는 Avro아이템라이터빌더(AvroItemWriterBuilder)를 제공하여 Avro아이템라이터(AvroItemWriter)의 인스턴스를 구성한다.

6.13.7. Specialized Processors

스프링 배치는 다음과 같은 특수 프로세서를 제공한다:

  • ScriptItemProcessor

ScriptItemProcessor

스크립트아이템프로세서(ScriptItemProcessor)는 처리할 현재 아이템을 제공된 스크립트에 전달하고 스크립트의 결과를 프로세서에서 반환하는 아이템프로세서(ItemProcessor)이다. 스프링 배치는 스크립트아이템프로세서(ScriptItemProcessor)의 인스턴스를 생성하기 위해 스크립트아이템프로세서빌더(ScriptItemProcessorBuilder)를 제공한다.