ABOUT ME

평범한 IT 직장인

Today
Yesterday
Total
  • [Spring Batch] ItemReader(1) - JDBC
    Java/Spring Batch 2021. 5. 30. 12:46

    스프링 배치는 개발자가 별도로 코드를 작성하지 않아도 거의 모든 유형의 입력 데이터를 처리 할 수 있는 표준 방법을 제공하며, 웹 서비스로 데이터를 읽어드리는 것처럼 스프링 배치가 지원하지 않는 포맷의 데이터를 처리 할 수 있는 커스텀 리더를 개발하는 기능도 제공한다.

    1. 파일입력
      1. 플랫파일
      2. XML
    2. JSON
    3. 데이터베이스 입력
      1. JDBC
      2. 하이버네이트
      3. JPA
      4. 저장 프로시저 (Stored Procedure)
      5. 스프링 데이터

    JDBC

    배치 처리를 할 때 대용량 데이터를 처리하는 요구 사항은 흔히 있는 일이다. 레코드 수백만 건을 반환하는 쿼리가 있다면 아마도 전체 데이터를 한 번에 메모리에 적재하고 싶지는 않을 것이다. 하지만 스프링이 제공하는 JdbcTemplate을 사용하고 싶지 않은 일이 벌어진다. JdbcTemplate이 전체 ResultSet에서 한 로우(row)씩 순서대로 가져오면서, 모든 로우를 피요한 도메인 객체로 변환해 메모리에 적재한다. 

     

    이에 대한 대안으로 스프링 배치는 한 번에 처리할 만큼의 레코드만 로딩하는 별도의 두 가지 기법을 제공하는데, 바로 커서(cursor)페이징(paging)이다. 커서는 표준 java.sql.ResultSet으로 구현된다. ResultSet이 open되면 next() 메소드를 호출할 때마다 데이터베이스에서 배치 레코드를 가져와 반환한다. 이렇게 하면 원하는 대로 데이터베이스에서 레코드를 스트리밍 받을 수 있으며 이는 커서 기법에 필요한 동작이다.

     

    한편 페이징 기법을 사용하려면 좀 더 작업이 필요하다. 페이징의 개념은 데이터베이스에서 페이지라고 부르는 청크 크기만큼의 레코드를 가져오는 것이다. 각 페이지는 해당 페이지만큼의 레코드만 가져올 수 있는 고유한 SQL 쿼리를 통해 생성된다. 한 페이지의 레코드를 다 읽고 나면 새로운 쿼리를 사용해 데이터베이스에서 새 페이지를 읽어온다. 

     

    • 커서 : 하나의 로우를 가져옴
    • 페이징 : 10개씩 전달 받음(청크 크기가 10이라고 할 때 예시)

     

    JDBC 커서 처리

    커서 기반 또는 페이지 기반 JDBC 리더를 구현하려면 두 가지 작업이 필요하다. 먼저 필요한 쿼리를 실행 할 수 있도록 리더를 구성해야 한다. 그다음으로 스프링의 JdbcTemplate이 ResultSet을 도메인 객체로 매핑하는 데 RowMapper 구현체가 필요했듯이 이 예제에서도 RowMapper 구현체를 작성해야 한다.

     

    public class Customer {
    
    	private Long id;
    
    	private String firstName;
    	private String middleInitial;
    	private String lastName;
    	private String address;
    	private String city;
    	private String state;
    	private String zipCode;
    
    
    	@Override
    	public String toString() {
    		return "Customer{" +
    				"id=" + id +
    				", firstName='" + firstName + '\'' +
    				", middleInitial='" + middleInitial + '\'' +
    				", lastName='" + lastName + '\'' +
    				", address='" + address + '\'' +
    				", city='" + city + '\'' +
    				", state='" + state + '\'' +
    				", zipCode='" + zipCode + '\'' +
    				'}';
    	}
    
    }
    

     

    도메인 객체를 적절히 정의했으므로 이제 RowMapper 구현체를 살펴보자.

    RowMapper는 스프링프레임워크 코어가 제공하는 JDBC 지원 표준 컴포넌트로, 이름 그대로 ResultSet에서 로우를 하나 전달받아 도메인 객체의 필드로 매핑한다. 예제에서는 Customer테이블의 필드를 Customer 도메인 객체로 매핑한다. 

    아래의 예제는 JDBC 구현체가 사용할 CustomerMapper이다.

    import batchrestapi.domain.Customer;
    import org.springframework.jdbc.core.RowMapper;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    public class CustomerRowMapper implements RowMapper<Customer> {
    
        @Override
        public Customer mapRow(ResultSet resultSet, int rowNumber) throws SQLException{
            Customer customer = Customer.builder()
                    .id(resultSet.getLong("id"))
                    .address(resultSet.getString("address"))
                    .city(resultSet.getString("city"))
                    .firstName(resultSet.getString("firstName"))
                    .lastName(resultSet.getString("lastName"))
                    .middleInitial(resultSet.getString("middleInitial"))
                    .state(resultSet.getString("state"))
                    .zipCode(resultSet.getString("zipCode"))
                    .build();
    
            return customer;
        }
    }
    

     

    쿼리 결과를 도메인 객체로 매핑하는 기능을 작성했으므로, 요청에 따라 결과를 반화할 수 있도록 커서를 열어 쿼리를 실행할 수 있어야 한다. 그러려면 스프링 배치가 제공하는 org.springframework.batch.item.database.JdbcCursorItemReader를 사용한다. JdbcCursorItemReader는 ResultSet을 생성하면서 커서를 연 다음, 스플이 배치가 read 매서드를 호출할 때마다 도메인 객체가 매핑할 로우를 가져온다. 

    JdbcCursorItemReader를 구성하려면 데이터 소스, 실행할 쿼리, 사용할 RowMapper 구현체 같은 최소한 세 가지 의존성을 제공해야 한다. 

    import batchrestapi.domain.Customer;
    import org.springframework.batch.item.database.JdbcCursorItemReader;
    import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
    import org.springframework.context.annotation.Bean;
    
    import javax.sql.DataSource;
    
    public class customerItemReader {
        @Bean
        public JdbcCursorItemReader<Customer> customerItemReader(DataSource dataSource){
            return new JdbcCursorItemReaderBuilder<Customer>()
                    .name("customerItemReader")
                    .dataSource(dataSource)
                    .sql("select * from custsomer where city = ?")
                    .rowMapper(new CustomerRowMapper())
                    .build();
        }
    }
    
    @Bean
    @StepScope
    public ArgumentPreparedStatementSetter citySetter(
    	@Value("#{jobParameters['city']}") String city){
        
        return new ArgumentPreparedStatementSetter(new Object[] {city});    
    }
    

     

    이 기법에는 장점과 단점이 있다. 특정한 상황에서는 레코드를 스크리밍하는 것은 괜찮은 방법이다. 그러나 백만 단ㄴ위의 레코드를 처리할 때라면 매번 요청을 할 때마다 네트워크 오버헤드가 추가되는 단점이 있다.

    거기에 추가로 ResultSet은 스레드 안전이 보장되지 않으므로 다중 스레드 환경에서는 사용할 수 없다. 이런 단점 때문에 또 다른 선택지인 페이징을 선택하게 된다.

     

     

    JDBC 페이징처리

    페이징 기법으로 작업할 때는 스프링 배치가 페이지라 부르는 청크로 결과목록을 반환한다. 각 페이지는 사전에 정의된 개수만큼 데이터베이스가 반환한 레코드로 구성된다. 페이징 기법을 사용할 때도 커서 기법과 마찬가지로 잡이 처리할 아이템은 여전히 한 건씩 처리된다는 점에 주목하기 바란다. 레코드 처리 자체에는 차이가 없다. 

    커서 기법과 차이가 있는 부분은 데이터베이스에서 가져오는 방법이다. 페이징 기법은 한 번에 SQL 쿼리 하나를 실행해 레코드 하나씩을 가져오는 대신, 각 페이지마다 새로운 쿼리를 실행한 뒤 쿼리 결과를 한 번에 메로리에 적재한다. 이 절에서는 페이지당 레코드를 10개씩 반환하도록 구성을 변경할 것이다.

     

    페이징 기법을 사용하려면 페이지 크기와 페이지 번호를 가지고 쿼리를 할 수 있어야 한다. 예를 들어 전체 레코드 개수가 10,000개이고 한 페이지 크기가 100 레코드라고 하자. 이때 20번째 페이지(또는 2,001 ~ 2,100 까지의 레코드)를 요청한다는 것을 조건으로 지정할 수 있어야 한다.

    그러려면 JdbcPagingItemReader에 org.springframework.batch.item.database.PagingQueryProvider 인터페이스의 구현체를 제공해야 한다. PagingQueryProvider인터페이스는 페이징 기반 ResultSet을 탐색하는데 필요한 모든 기능을 제공한다.

     

    안타깝게도 각 데이터베이스마다 개별적인 페이징 구현체를 제공한다. 그러므로 두 가지 선택지가 존재한다.

    1. 사용하려는 데이터베이스 전용 PagingQueyrProvider 구현체를 구성한다. 
    2. 리더가 org.sringframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean을 사용하도록 구성한다. 이 팩토리는 사용하는 데이터베이스가 어떤 것인지 감지할 수 있다. 

    일반적으로 SqlPagingQueryProviderFactoryBean를 쓰면 사용 중인 데이터베이스를 자동으로 감지해 적절한 PagingQueryProvider를 반환하므로 예제에서는 SqlPagingQueryProviderFactoryBean을 사용한다.

     

    JdbcPagingItemReader를 구성하려면 네 가지 의존성이 필요하다. 데이터 소스, PagingQueryProvider 구현체, 직접 개발한 RowMapper 구현체 페이지의 크기가 그것이다. 또한 스프링이 SQL문에 파라미터를 주입하도록 구성할 수도 있다. 아래는 JdbcPagingItemReader의 구성이다.

     

    ...
    
    @Bean
    @StepScpoe
    public JdbcPaingItemReader<Customer> customerItemReader(DataSource dataSource,
    	PagingQueryProvider queryProvier, 
        @Value("#{jobParameters['city']}") String city){
     	
        	Map<String, Object> parameterValues = new HashMap<>(1);
            parameterValues.put("city", city);
            
            return new JdbcPagingItemReaderBuilder<Customer>()
            	.name("customerItemReader")
                .dataSource(dataSource)
                .queryProvider(queryProvider)
                .parameterValues(parameterValues)
                .pageSize(10)
                .rowMapper(new CustomerRowMapper())
                .build();    
    }
    
    @Bean
    public SqlPagingQueryProviderFactoryBean pagingQueryProvider(DataSource dataSource){
    	SqlPaingQueryProviderFactoryBean factoryBean = new SqlPagingQueryProviderFactoryBean();
        
        factoryBean.setSelectClause("select *");
        factoryBean.setFromClause("from Customer");
        factoryBean.setWhereClause("wheere city = :city");
        factoryBean.setSortKey("lastName");
        factoryBean.setDataSource(dataSource);
        
        return factoryBean;
    }

     

    위 예제에서 볼 수 있듯이 JdbcPagingItemReader를 구성하려면 데이터 소스, PagingQueryProvider, SQL에 주입해야 할 파라미터, 각 페이지의 크기, 결과를 매핑하는 데 사용할 RowMapper 구현체를 제공해야 한다.

     

    PagingQueryProvider를 구성하면서 다섯 가지 항목을 설정했다. 처음 세 가지 항목은 작성하는 SQL문의 각 부분을 select 절, from 절, where 절이다. 그 다음으로 정렬 키를 설정했다. 페이징 기법을 사용할 때는 결과를 정렬하는 것이 중요하다. 페이징 기법은 한 번에 쿼리 하나를 실행한 뒤 결과를 스트리밍 받는 대신 각 페이지에 해당하는 쿼리를 실횅한다. 이처럼 각 페이지의 쿼리를 실행할 때마다 동일한 레코드 정렬 순서를 보장하려면 order by 절이 필요하며, 생성되는 SQL문에도 order by 절에 sortKey로 지정한 모든 필드가 적용돼야 한다. 

    또한 이 정렬 키가 ResultSet 내에서 중복되지 않아야 한다. 스프링 배치가 실행할 SQL 쿼리를 생성하는 과정에서 정렬 키를 사용하기 때문이다. 마지막으로 데이터 소스 참조를 설정했다.

    왜 SqlPaingQueryProviderFactoryBean과 JdbcPagingItemReader 양쪽에 데이터 소스를 구성하는지 의아할 수도 있다. SqlPagingQueryProviderFactoryBean은 제공된 데이터 소스를 사용해 작업 중인 데이터베이스의 타입을 결정한다. 

    원한다면 setDatabaseType 메소드를 사용해 데이터베이스 타입을 명시적으로 구성할 수도 있다. 이렇게 구성하면 SqlPaingQueryProviderFactoryBean은 개밣는 리더에서 사용할 적절한 Paging QueryProvider 구현체를 제공한다.

     

    페이징 기법을 사용할 때의 SQL 파라미터 사용법은 커서 기법을 사용할 때의 사용법과 다르다. 예제에서는 placeholder로 물음표를 사용하는 단일 SQL문장을 만드는 대신 SQL문장을 여러 조각으로 생성했다. 

    whereClause에 지정한 문자열 내에서는 표준인 물음표 placeholder 를 사용할 수 있으며, 그 대신 예제의 customerItemReader처럼 네임드 파라미터(named parameter)를 사용할 수도 있다. 네임드 파라미터를 사용한다몀면 구성에는 파라미터 값을 맵 형태로 만들어 주입할 수 있다. 예제에서 parameterValues 맵의 city 엔트리는 whererClause에 지정한 문자열 내 city 네임드 파라미터로 매핑된다.

    네임드 파라미터 대신 물음표를 사용하려 한다면 각 파라미터를 값으로 매핑하는 키로 물음표의 순번을 사용한다. 

     

    지금까지 살펴본 것처럼 직접적인 JDBC통신을 사용해서 데이터베이스에서 처리 대상 아이템을 조회하는 작업은 실제로 매우 간단하다. 몇 줄 안되는 자바 코드만 작성하면 잡에 데이터를 공급할 수 있는 성능 좋은 ItemReader를 갖출 수 있다. 그러나 JDBC가 데이터베이스 레코드에 접근할 수 있는 유일한 방법은 아니다.

    하이버네이트나 마이바티스, ORM 기술은 관계형 데이터베이스 테이블을 객체로 매핑하는 훌륭한 솔루션을 제공하면서 인기 있는 데이터 접근 도구르 부상했다. 

     

     

     


     

    전체 소스

     

    AcornPublishing/definitive-spring-batch

    스프링 배치 완벽 가이드 2/e. Contribute to AcornPublishing/definitive-spring-batch development by creating an account on GitHub.

    github.com

    스프링 배치 완벽 가이드, 마이클 미넬라

    댓글

Designed by Tistory.