ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Batch] 에러처리(레코드 건너뛰기, 잘못된 레코드 로그 남기기, 입력이 없을 때의 처리)
    Java/Spring Batch 2021. 8. 10. 08:13

    스프링 배치 애플리케이션에서 시작할 때와 처리 중에 또는 결과를 기록할 때 문제가 발생할 수 있다. 

    배치 처리 중에 발생하는 여러 가지 에러를 다루는 방법을 살펴보자.

     

    레코드 건너뛰기

    입력에서 레코드를 읽는 중에 에러가 발생했을 때는 몇 가지 다른 선택지가 존재한다. 

    먼저 예외를 던져 처리를 멈추는 것이다.

    얼마나 많은 레코드를 처리해야 하는가와 에러가 발생한 레코드 한 개를 처리하지 않았을 때의 영향도에 따라 에러를 던져 처리를 멈추는 것은 극단적인 방버일 수 있다.

    스프랭 배치는 그 대신 특정 예외가 발생했을 때 레코드를 건너뛰는 skip 기능을 제공한다. 

     

    레코드를 건너뛸지 여부를 결정할 때 고려해야 할 두 가지 요소가 있다. 

    먼저 어떤 조건에서 레코드를 건너뛸 것인가, 특히 어떤 예외를 무시할 것인가이다. 

    레코드를 조회화는 도중 에러가 발생하면 스프링 배치는 예외를 던진다. 어떤 것을 건너뛸지 결정하려면 어떤 예외가 발생했을 때 건너뛸 것인지 식별해야 한다.

     

    입력 레코드를 건너뛸지 여부를 결정하는 두 번째 요소는 스텝 실행이 실패했다고 결정하기 전에 얼마나 많은 레코드를 건너뛸 수 있게 할 것인가다.

    백만 건의 레코드 중 한 두건만 건너뛴다면 대수롭지 않은 일일 수도 있다.

    그러나 백만 건 중 오십만 건을 건너뛴다면 뭔가 잘못된 것이다. 기준을 정하는 건 개발자의 몫이다.

     

    실제로 레코드를 건너뛰려면 스프링 배치가 어떤 예외를 건너뛰게 할지, 몇 번까지 예외를 허용할지 설정하기만 하면 된다. 모든  org.springframework.batch.item.ParseException을 10회까지 건너뛰게 설정해보자. 

     

    예제 7-71

    @Bean
    public Step copyFileStep(){
    	
        return this.stepBuilderFactory.get("copyFileStep")
        	.<Customer, Customer>chunk(10)
            .reader(customItemReader())
            .writer(itemWriter())
            .faultTolerant()
            .skip(ParseException.class)
            .skipLimit(10)
            .build();
    	
    }

     

    이 시나리오에서 건너뛸 대상인 예외는 하나다. 

    그러나 어떨 때는 건너뛸 예외가 많아서 훨씬 포괄적인 목록이 될 수도 있다. 

    7-71의 구성은 특정 예외를 건너뛰게 한다. 그러나 건너뛰고 싶은 예외보다는 건너뛰고 싶지 않은 예외를 설정하는 구성이 더 간편할 수도 있다. 

    그러려면 7-71의 skip메서드처럼 건너뛸 대상을 지정하는 것 이외에 건너뛰지 말아야 할 대상을 지정한느 메서드를 사용해야 한다. 7-72는 바로 이전 예제와 반대(ParseException을 제외한 모든 예외는 건너뜀)되는 구성이다.

     

    예제7-72

    @Bean
    public Step copyFileStep(){
    	
        return this.stepBuilderFactory.get("copyFileStep")
        	.<Customer, Customer>chunk(10)
            .reader(customItemReader())
            .writer(itemWriter())
            .faultTolerant()
            .skip(Exception.class)
            .noSkip(ParseException.class)
            .skipLimit(10)
            .build();
    	
    }

     

    7-72의 구성은 org.springframework.batch.item.ParseException을 제외한 java.lang.Exception을 상속한 모든 예외를 10번까지 건너뛸 수 있음을 나타낸다.

     

    건너 뛸 대상 예외와 몇 번까지 건너뛸지를 지정하는 별도의 방법이 존재한다. 스프링 배치는 org.springframework.batch.core.step.skip.SkipPolicy라는 인터페이스를 제공한다.

    SkipPolicy에는 shouldSkip 메서드 하나만 존재하며, 대상 예외와 건너뛸 수 있는 횟수를 전달받는다. 

    그러므로 SkipPolicy 구현체는 건너뛸 예외와 허용 횟수를 판별할 수 있다. 예제 7-73은 java.io.FileNotFoundException를 건너뛰지 못하게 하며 ParseException은 10번까지 건너뛸 수 있게 하는 코드다.

     

    예제 7-73

    ...
    public class FileVerificationSkipper implements SkipPolicy{
    	public boolean shouldSkip(Throwable exception, int skipCount)
        	throws SkipLimitExceededException{
            
         	if(exception instanceof FileNotFoundException){
            	return false;
            }else if(exception instanceof ParseException && skipCount <= 10){
            	return true;
            }else{
            	return false;
            }
        }
    }

     

    레코드를 건너뛰는 것은 배치처리에서 일반적인 일이다.

    이런 처리기법을 사용하면 단일 레코드보다는 훨씬 많은 양의 처리를 최소한의 영향도로 계속 실행할 수 있다.

    에러가 발생한 레코드를 건너뛸 때는 향후 원인 분석을 할 수 있도록 로그를 남기는 것 같은 추가적인 작업이 필요할 수도 있다. 

     

     

    잘못된 레코드 로그 남기기

    문제가 있는 레코드를 건너뛰는 것은 유용한 방법이긴 하지만, 건너뛰는 것 자체가 문제가 될 수도 있다.

    몇 가지 시나리오에서는 레코드를 건너뛰는 것도 좋은 방법이다.

    원시 데이터를 모아 처리하다가 해석할 수 없는 데이터를 만났다고 하자. 이런 데이터는 건너뛰어도 좋을 것이다.

    그러나 돈과 관련된 상황이라면 이갸기가 다르다. 거래 내역을 처리할 때라면 단순히 레코드를 건너뛰는 것은 제대로 된 해결책이 아니다.

    이럴 때는 에러를 일으킨 레코드의 로그를 남길 수 있다면 도움이 된다. 이 절에서는 ItemListener를 사용해서 잘못된 레코드를 기록하는 방법을 살펴본다.

     

    ItemReaderListener는 beforeRead, afterRead, onReadError같은 메서드 세개로 구성돼 있다. 

    잘못된 레코드를 읽어들였을 때 로그를 남기려면 ItemListenerSupport를 사용하고 onReadError 메서드를 오버라이드해서 발생한 에러를 기록한다. 또한 메서드에 @OnReadError 어노테이션을 추가한 POJO를 사용한다.

    파일을 파싱할 때는 어떤 에러가 발생했는지 그리고 왜 에러가 발생했는지를 알려주는 예외 생성 작업을 스프링 배치가 잘 처리한다는 점을 알아두기 바란다.

    그러나 데이터베이스를 이용하는 처리를 할때는 그렇지 않다. 실제 데이터베이스 입출력을 스프링 자체나 하이버네이트 같은 다른 프레임워크가 처리하므로 스프링 배치가 담당할 예외 처리가 많지 않다. 커스텀 ItemReader나 커스텀 RowMapper를 개발하는 것처럼 직접 처리를 개발할 때는 문제점을 분석하는 데 충분한 정보를 예외 자체에 담는 것이 중요하다. 

     

    Customer파일에서 데이터를 읽는다. 입력 도중에 예외가 발생하면 발생한 레코드를 로그로 남길 것이다.

    그러려면 발생한 예외를 CustomerItemListener가 전달받게 하며, 발생한 예외가 FlatFileParseException이라면 문제가 발생한 레코드와 오류 정보를 액세스할 수 있게 한다. 예제 7-74는 CustomerrItemListener이다.

     

    예제 7-74

    ...
    
    public CustomerItemListener{
    	
        private static final Log logger = LogFactory.getLog(CustomerItemListener.class);
        
        @OnReadError
        public void onReadError(Exception e){
        	if(e instanceof FlatFileParseException){
            	FlatFileParseException ffpe = (FlatFileParseException) e;
                StringBuilder errorMessage = new StringBuilder();
                errorMessage.append("An error occured while processing the " + 
                	ffpe.getLineNumber() +
                    " line of the file.Below was the faulty " + 
                    "input.\n");
                errorMessage.append(ffpe.getInput() + "\n");
                
                logger.error(errorMessage.toString(), ffpe);            
            }else{
            	logger.error("An error has occurred", e);            
            }
        }
    	
    }

     

    리스너를 구성하려면 파일을 조회하는 스텝을 수정해야 한다. 예제에서 copyJob에는 스텝이 하나뿐이다. 예제 7-75는 CustomerItemLIstener의 구성이다.

    ...
    
    @Bean
    public CustomerItemListener customerListener(){
    	return new CustomerItemListener();
    }
    
    @Bean
    public Step copyFileStep(){
    	return this.stepBuilderFactory.get("copyFileStep")
        		.<Customer, Customer> chunk(10)
                .reader(customerItemReader())
                .writer(itemWriter())
                .faultTolerant()
                .skipLimit(100)
                .skip(Exception.class)
                .listener(customerListener())
                .build();
    }

     

    예를 들어 고정 너비 레코드 잡을 처리하는 데 글자 수가 63개를 초과하는 입력 레코드가 포함된 파일을 처리한다면 예외가 발생한다.

    Exception을 상속한 모든 예외를 건너뛰도록 잡을 구성했으므로 예외는 잡의 실행 결과에 영향을 미치지 못한다. 

    대신 customerItemLogger를 사용해 원하던 아이템을 로그로 남긴다. 이 잡을 실행하면 두 가지 사실을 확인할 수 있다. 우선 잘못된 레코드를 만날 때마다 FlatFileParseException를 확인할 수 있다. 

    두 번째로 로그 메시지를 확인할 수 있다. 예제 7-76은  잡에서 에러가 발생했을 때 생성한 로그 메시지의 예이다.

     

    예제7-76 CustomerItemLogger의 출력

    2021-08-10 08:42:01 ERROR main [com.apress.springbatch.chapter7.CustomerItemListener] - 
    <An error occured while processing the 1 line of the file. Below was the faulty input.
    Michael TMinella 123 4th Street Chicago IL6060ABCDE
    >

     

    쓸 만한 로깅 프레임워크를 사용해 FlatFileParseException이 발생했을 때 처리에 실패한 해당 입력을 전달받아 로그 파일에 기록할 수 있다.

    그러나 잘못된 입력을 기록하는 것만으로는 에러 레코드를 파일에 기록하고 작업을 계속하는 목적을 달성할 수 없다. 

    이런 시나리오에서 해당 잡은 이슈가 발생해 실패한 레코드만 기록하므로 그 외의 예외 상황은 처리할 수 없기 때문이다. 

    마지막 절에서는 잡 실행 중에 입력이 없을 때의 처리 방법을 살펴본다.

     

    입력이 없을 때의 처리

    SQL 쿼리가 빈 결과를 반환하는 것은 흔한 일이다. 빈 파일이 존재할 때도 많다.

    그런데 이런 빈 입력이 배치 처리에서 상식적인가? 이 절에서는 스프링 배치가 데이터가 없는 입력 소스를 읽을 때 처리 방법을 살펴본다.

     

    리더가 입력 소스에서 조회를 시도했는데, 처음부터 null이 반환되더라도 스프링 배치는 이를 기본으로 평상시 null 받았을 때와 동일하게 처리한다.

    즉, 스텝이 완료된 것으로 처리한다.

    이런 접근법이 대부분 시나리오에서 문제 없이 작동하겠지만, 작성한 쿼리가 빈 결과를 반환하거나 파일이 비었을 때 이를 알아야 할 수도 있다.

     

    입력을 읽지 못했을 때 스텝을 실패로 처리하거나 이메일을 보내는 것 같은 다른 처리를 하려면 StepListener를 사용한다. 4장에서는 스텝의 시작과 끝의 로그를 남기는데 StepListener를 사용했다.

    이번 예제에서는 StepListener의 @AfterStep 메서드를 사용해서 조회한 레코드 수를 확인한 뒤에 레코드 수에 따라 적절한 처리를 한다. 예제 7-77은 레코드를 읽을 수 없어서 실패한 스텝을 기록하는 코드다. 

     

    예제 7-77

    ...
    
    public class EmptyInputStepFailer{
    	
        @AfterStep
        public ExitStatus afterStep(StepExecuttion execution){
        	if(execution.getReadCount() > 0){
            	return execution.getExitStatus();
            }else{
            	return ExitStatus.FAILED;
            }
        }
        
    }

     

    리스너의 구성은 다른 StepListener의 구성과 동일하다. 예제 7-78은 해당 인스턴스의 구성이다.

    ...
    
    @Bean
    public EmptyInputStepFailer emptyFileFailer(){
    	return new EmptyInputStepFailer();
    }
    
    @Bean
    public Step copyFileStep(){
    	return this.stepBuilderFactory.get("copyFileStep")
        .<Customer, Customer>chunk(10)
        .reader(customerFileReader(null))
        .writer(outputWriter(null))
        .listener(emptyFileFailer())
        .build();
    }
    
    ...

     

    위와 같이 스텝을 구성한 후 잡을 실행하면, 이볅이 없을 때 COMPLETED 상태로 잡이 끝나지 않는다. 따라서 원래 원하던 입력을 확보하고 잡을 재실행할 수 있다.

     

    스프링 배치 완벽 가이드 / 마이클 미넬라 지음

     

    댓글

Designed by Tistory.