ItemReader – Spring Batch Chapter 3

DB에서는 단일 레코드, 애플리케이션에서는 단일 도메인 객체, 배치 작업에서는 Item. 이 녀석을 읽어들일 때 사용하는 것이 바로 ItemReader.

public interface ItemReader {

  Object read() throws Exception;

  void mark() throws MarkFailedException;

  void reset() throws ResetFailedException;
}

아주 깔끔한 인터페이스 read()는 읽고, mark()는 표시해두고, reset()은 최근에 mark() 된 곳으로 이동. 마치 책갈피가 있는 Iterator 느낌이 납니다.

그리고 이 인터페이스를 구현한 녀석이 DB나 파일등에 연결이 필요한 경우 반드시 같이 구현하는 인터페이스가 있는데 그건 바로 ItemStream.

public interface ItemStream {

  void open(ExecutionContext executionContext) throws StreamException;

  void update(ExecutionContext executionContext);
 
  void close(ExecutionContext executionContext) throws StreamException;
}

ExecutionContext를 사용해서 처음 위치부터 시작하지 않고 이전에 멈췄던 곳에서 다시 시작 할 수 있는 방법을 제공합니다. open() 해서 연결하고, update()는 ExecutionContext에서 현재 상태 읽어옵니다. 따라서 커밋하기 전에 update()를 호출해서 현재상태가 DB에 보존되고 있는지 확인할 때 사용하고, close()는 open으로 읽어온 자원들 반납합니다. 작업 마친 담에 꼭 호출해주는게 좋겠네요.

DB에 있는 Item을 읽어올 때, 스프링의 JdbcTemplate에서는 RowMapper를 사용해서 반환된 ResultSet에 있는 데이터들을 모두. 왕창. 한방에. 객체로 맵핑해서 반환해줬습니다. 이게 이슈가 되기도 합니다. 왜냐면, 데이터가 너무 많으면 메모리가 모자라서 죽게 될테니까요. 그런데 배치에서도 역시 RowMapper를 사용하는데, 그런 일이 벌어지지 않게 하기 위해서 두 가지 방법을 마련했습니다. 하나는 커서Cursor, 하나는 드라이빙쿼리DrivingQuery.

Cursor 스타일이란?
맵핑을 한 번에 전부 하는게 아니라, 한 번에 한 줄씩 합니다. “스트리밍” 방식이라고 할 수 있습니다. 위에서 살펴본 ItemReader의 next()를 호출 할 때마다 한 줄(레코드)씩 이동합니다.

  HibernateCursorItemReader itemReader = new HibernateCursorItemReader();
  itemReader.setQueryString(“from CustomerCredit”);
  //For simplicity sake, assume sessionFactory already obtained.
  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(executionContext);

itemReader.read()를 할 때마다 RowMapper를 적용해서 객체를 던져주고, 다음 레코드로 넘어갑니다.

Driving Query 스타일이란?
레코드 전체를 가져오는게 아니라, 주키만 가져온 다음에 필요시에 애플리케이션의 DAO를 사용해서 객체를 DB에서 로딩하는 방법. 이 방법을 쓰는 이유는 DB2 처럼 Pessimistic Locking(읽는 롹킹)하는 경우에 Cursor 방법이 비효율적일 수 있기 때문입니다.

이 녀석의 구현체인 DrivingQueryItemReader가 의존하고 있는 인터페이스 KeyCollector.

 public interface KeyCollector {

    List retrieveKeys(ExecutionContext executionContext);

    void updateContext(Object key, ExecutionContext executionContext);
  }

첫번째 메소드야 뻔하고, updateContext는 현재 어디까지 읽었나, 책갈피를 끼워두는 겁니다.

KeyCollector 구현체
– SingleColumnJdbcKeyCollector : 주키가 컬럼 하나 일때 사용.

  SingleColumnJdbcKeyCollector keyCollector = new SingleColumnJdbcKeyCollector(getJdbcTemplate(),
  “SELECT ID from T_FOOS order by ID”);

  keyCollector.setRestartSql(“SELECT ID from T_FOOS where ID > ? order by ID”);

  ExecutionContext executionContext = new ExecutionContext();

  List keys = keyStrategy.retrieveKeys(new ExecutionContext());

  for (int i = 0; i < keys.size(); i++) {
    System.out.println(keys.get(i));
  }

흠.. 이 예제 코드는 잘 이해가 안 가네요. ItemReader 인터페이스는 안 사용하나;;

복합키 일땐?? ExecutionContextRowMapper

6 thoughts on “ItemReader – Spring Batch Chapter 3”

  1. DrivingQueryItemReader의 property로 keyCollector가 들어가서 동작하죠. 예제코드는 실전에서 저렇게 쓰라는 말보다는 해당 클래스의 동작을 보여주기 위한 성격이 강해 보이네요.

    그리고 ListItemReader처럼 ItemStream 이 아닌 ItemReader도 있기는해요 ^^;

    1. 그렇군요. 흠.. DrivingQuery를 볼 때 궁금한게 있었는데요. 왜 이 스타일 구현체는 iBatis만 있고 Hibernate는 없는걸까요? 또 반대로 Cursor 스타일의 구현체는 Hibernate가 있는데 iBatis용은 없네요. 무슨 이유가 있겠거니.. 하고 지나갔는데 마침 생각난김에 적어둡니다. ^^;;

      저 클래스는 보니까 테스트 용도인것 같구요. 사실상 ItemReader를 구현한 모든 (배치 로직 구현시에 필요한) 클래스들은 ItemStream을 구현했다고 봐도 괜찮을 것 같네요.

      Clients of an ItemReader that also implements ItemStream should call open before any calls to read, to open any resources such as files or obtain connections.

      레퍼런스에도 이렇게 나와있으니까요. 🙂

  2. 일단 저의 생각은 hibernate로도 DrivingQuery는 가능할 것 같은데, iBatis로는 Cursor 스타일이 안 될 것만 같군요, hibernate의 ScrollableResults interface와 같은 것이 iBatis에 없는한 말이죠 ^^;

    그리고 매뉴얼에 영어해석이 다소 저와 다른 것 같은데, 저는 ‘ItemStream도 구현한 ItemReader를 사용하는 Client는 읽기 위한 호출 전에 open을 호출해야 한다..’ 로 해석해서 ItemReader 중에서 ItemStream도 구현한 클래스들의 호출방식에 대한 설명이라고 생각했었습니다.

    뭐 실제 클래스들을 보면 DelegatingItemReader, ItemReaderAdapter, AggregateItemReader들도 ItemStream을 구현하지 않았는데, 어짜피 이 클래스들은 단독으로는 쓰이지 않는 것들이니 JMSItemReader 정도만이 구현 클래스중에 ItemStream이 아닌 것으로 보이기도 합니다.

    어쨓거나 대부분의 읽는 자원들이 file, DB등 열고 닫는 동작이 필요한 것들이고, Restarable한 장점을 살리기 위해서라도 ItemStream을 구현하지 않은 클래스는 별로 나오지 않을 것 같습니다. 아뭏든 저는 ItemReader와 ItemStream를 동시에 구현한 클래스는 그것이 반드시 붙어다니는 명세라기 보다는 그런 자원환경이 현실적으로 대부분이기 때문이라고 이해하고 있습니다. JMSItemReader 같은 예외도 있으니까요.

    1. 아.. 그렇군요. 제가 매뉴얼을 잘 못 이해한 것 같습니다. 감사합니다. 본문을 수정해야겠네요. ^^;;

      그럼 IbatisDrivingQueryItemReader의 read()를 호출하면, 이미 전부 키값들은 전부 읽어왔고, 그 상태에서 하나씩 순회하면서 키값을 던져주는 건가요?

      아.. 그렇게 되면 제가 포럼에 올린 질문 http://forum.ksug.org/viewtopic.php?f=6&t=157 에 대한 실마리를 찾을 수 있게 되는건데 말이죠.

      HibernateCursorItemReader의 read()를 호출하면, 그 때마다 한 row씩 가져와서 맵핑한 다음에 던져주는 것이고, 이런 게 커서 스타일 이란 거죠.

      ItemReader의 read()를 호출한다고 해서 무조건 커서 스타일이 아니라는 뜻이 되는데, 이렇게 되면 제가 포럼에 올렸던 궁금증 전부가 해결됩니다. 캬캬.

  3. 네, Driving query에서는 미리 키값들은 read() 전에 다 읽어져 있는 상태입니다.

    open() 메서드에서 그 일을 하고 있죠. 포럼에 코드를 적어놓겠습니다~

Leave a Reply

Your email address will not be published. Required fields are marked *