인프런 커뮤니티 질문&답변

김대호님의 프로필 이미지
김대호

작성한 질문수

스프링 배치

정리

좋은 강의 감사합니다. 질문이 있습니다.

작성

·

1.2K

1

강의를 보고 부족하나마 제가 이해한대로 코드를 조금 만들어봤습니다.

 

하지만 JpaPagingReader에서 Transaction already active 에러가 나는 현상을 이해하지 못하여 질문드립니다.

 

상황을 요약드리면..

인위적으로 프로세서에서 에러를 발생시킬시 retry와 skip이 잘 작동합니다.

하지만 db단에서 에러를 터트릴 경우 skip을 시도하면서 다음 Reader에서 java.lang.IllegalStateException: Transaction already active 에러를 무한대로 던집니다. (메모리가 터질때까지 던집니다)

정말 혼자서 해결해보려 백방 노력하였으나 이유를 찾지 못하겠습니다. 분명 RuntimeException에 대해 skip처리를 하고 넘어가야 할 것 같은데 넘어가질 못하고 있습니다.

 

송구합니다만 괜찮으시다면 코드 리뷰를 한번만 부탁드려도 될지요 ㅠㅠ

 

https://github.com/zzangisdaeho/spring-batch/tree/error-case

답변 2

0

정수원님의 프로필 이미지
정수원
지식공유자

이 부분은 스프링 배치 자체의 문제라기 보다는 트랜잭션 관련 이슈라고 볼 수 있습니다.

JpaPagingItemReader 에 보시면 아래와 같은 구분이 있습니다.

if (transacted) {
tx = entityManager.getTransaction();
tx.begin();

entityManager.flush();
entityManager.clear();
}//end if

즉 entityManager 를 통해 새로운 트랜잭션을 얻어 오고 있습니다

기본적으로 스프링 배치는 chunk  단위로 트랜잭션이 생성되고 종료됩니다.

그렇기 때문에 위의 구문이 시작되기 전에 이미 chunk 처리 과정에서 트랜잭션은 시작되었다고 볼 수 있습니다.

근데 JpaPagingItemReader 에서는 별도의 트랜잭션 설정을 하고 있습니다.

이것 자체가 문제가 될 것은 없습니다.

트랜잭션은 중첩되어 실행될 수 있기 때문입니다.

그렇다면 위의 오류가 나는 것은 어떤 원인일까요?

트랜잭션은 예외나 오류가 발생하면 롤백 처리를 하게 됩니다. 그래서 chunk 에서 시작했던  트랜잭션을 롤백할려고 시도합니다. 

근데 skip 정책에 의해서 해당 예외를 무시하고 롤백이 아닌 다음 과정으로 진행할려고 합니다.

진행 중에 다음 구문을 만나게 됩니다.\

바로 tx.begin() 입니다.  tx.begin() 안으로 들어가 보면 이런 구문이 나옵니다.

@Override
public void begin() {
if ( !session.isOpen() ) {
throw new IllegalStateException( "Cannot begin Transaction on closed Session/EntityManager" );
}

if ( transactionDriverControl == null ) {
transactionDriverControl = transactionCoordinator.getTransactionDriverControl();
}

// per-JPA
if ( isActive() ) {
if ( jpaCompliance.isJpaTransactionComplianceEnabled()
|| !transactionCoordinator.getTransactionCoordinatorBuilder().isJta() ) {
throw new IllegalStateException( "Transaction already active" );
}
else {
return;
}
}

LOG.debug( "begin" );

this.transactionDriverControl.begin();
}

위에서 isActive() 가 나옵니다.

isActive() 가 참이면 new IllegalStateException( "Transaction already active" ) 예외가 발생합니다.

isActive() 안으로 들어가 보겠습니다.

@Override
public boolean isActive() {
// old behavior considered TransactionStatus#MARKED_ROLLBACK as active
// return isActive( jpaCompliance.isJpaTransactionComplianceEnabled() ? false : true );
return isActive( true );
}

여기서 아래 isMarkedForRollbackConsideredActive 파라미터에 true 값을 전달하고 있습니다.

@Override
public boolean isActive(boolean isMarkedForRollbackConsideredActive) {
if ( transactionDriverControl == null ) {
if ( session.isOpen() ) {
transactionDriverControl = transactionCoordinator.getTransactionDriverControl();
}
else {
return false;
}
}
return transactionDriverControl.isActive( isMarkedForRollbackConsideredActive );
}

다시 transactionDriverControl.isActive( isMarkedForRollbackConsideredActive ) 로 다시 들어가 보면 

default boolean isActive(boolean isMarkedRollbackConsideredActive) {
final TransactionStatus status = getStatus();
return TransactionStatus.ACTIVE == status
|| ( isMarkedRollbackConsideredActive && TransactionStatus.MARKED_ROLLBACK == status );
}

가 나옵니다.

런타임에서  실행한 디버깅 모드인데 DB 에서 예외가 발생한 이후입니다.

TransactionStatus 의 상태를 확인하고  isMarkedRollbackConsideredActive  및 TransactionStatus.MARKED_ROLLBACK == status  의 상태를 체크하는데 해석해보면 현재 트랜잭션이 롤백으로 사용되도록 마킹되어 있음을 확인할 수 있습니다.

즉 현재 트랜잭션의 상태는 롤백으로 마킹되어 있다는 의미고 이것은 현재 트랜잭션은  커밋이나 다른 용도로 사용할 수 없음을 의미합니다.

skip() 으로 예외를 무시하고 넘어 갔지만 현재 활성화된 트랜잭션이 롤백용으로 마킹된 트랜잭션이라면  롤백처리 외 다른 트랜잭션 설정을 할 수 없다는 예외를 발생한 것입니다.

일단 여기에 대한 해결책은 JpaPagingItemReader 에서 tx.begin() 을 실행하지 않도록 해야 하는데 API 옵션 중에서 transacted 을 false 로 할 수 있는데 그렇데 되면 


if
(transacted) { // fasle 이면 실행되지 않는다 기본은 true

tx = entityManager.getTransaction();
tx.begin();

entityManager.flush();
entityManager.clear();

}

위의 구문이 실행되지 않기 때문에 해당 이슈는 발생하지 않지만 Jpa 는 트랜잭션 안에서 영속성이 적용되기 떄문에 연관된 객체를 가지고 올 때 Lazy loading 시  no session 의 에러를 만나게 됩니다.

아니면 연관객체의 Fetch  모드를 Eager 로 변경해서 사용해야 합니다.

다른 해결책으로는 JpaPagingItemReader 를 똑같이 복사한 새로운 커스텀 클래스를 만들고 위의 구문을

if (transacted) 
entityManager.clear();
}

와 같이 만드는 것입니다.  즉 오류가 나는  트랜잭션 설정을 제외합니다.

근데 이 부분은 충분한 검증을 해야 하기 때문에 실무에 적용하기가 조심스럽습니다.

일단 제 판단으로는 skip() 에 의한 예외를 처리하면서 위의 오류를 해결하는 것은 Jpa 기반이 아닌 Jdbc 기반 ItemReader 을 사용하는 것입니다.

다른 해결책이 있는지 좀 더 분석이나 여러 자료들을 참고해 봐야 할 것 같습니다.

김대호님의 프로필 이미지
김대호
질문자

감사합니다. 저도 이리 저리 삽질하다가.. JdbcPagingReader 를 사용해서 해결해놓는게 최선이었습니다.

 

말씀해주신 skip쪽 문제라면 제가 강의를 충분히 이해하지 못해서 발생한 문제인것 같습니다. 당연히 캐시된 itemReader로 부터 읽어오면서 chunk단위로 재시작되기 때문에 트랜잭션이 청크단위로 묶어서 흘러가면 당연히 기존 변경사항은 롤백시키고 처음부터 재시작 하는줄 알았는데 같은 트랜잭션안에서 묶여서 롤백마킹이 된 상태인줄은 몰랐습니다.

 

아직 2년차에 실력이 부족하여 강사님처럼 라이브러리 코드를 보는 실력이 부족하다 보니.. 강의 외적인 부분으로 질문드려서 죄송합니다. 

 

그럼에도 자세히 알려주시고 가이드 잡아주셔서 감사합니다. 스프링 시큐리티에서도, 이번강의에서도 디버그로 코드의 흐름을 보여주신것을 좀 더 성실히 따라가보도록 하겠습니다.

 

혹시 다음강의를 또 준비해주신다면 디버그 위치를 찍는과정도 강의에 포함시켜주시면 저같이 부족한 개발자들에게 많은 도움이 될 수 있지 않을까 싶습니다.

 

감사합니다!! 꾸벅

0

김대호님의 프로필 이미지
김대호
질문자

쿼츠로 자동실행 crontab으로 1분마다 되도록 해놨습니다.. 혹시 봐주실수 있다면 DB만 생성해주시고 실행해주시면 감사하겠습니다 ㅠㅠ

정수원님의 프로필 이미지
정수원
지식공유자

죄송하지만 실행해야 할 Job 파일이 많아서 정확하게 어떤 선행 조건과 job 을 실행시켜야 하는지 알려 주시면 감사하겠습니다.

김대호님의 프로필 이미지
김대호
질문자

관심있게 봐주셔서 감사합니다.

그냥 SpringBatchApplication을 실행하시면 알아서 에러가 터지게 세팅해놨습니다.

payment, vacation job 두가지가 있는데 quartzRegister클래스에서 두 job모두 1분마다 실행하도록 설정해놨습니다.

job은 column size가 벗어나게 구성해놔서 어떤잡을 돌려도 같은에러가 터집니다.

수동실행하시려면 그냥 joblauncher로 아무거나 실행해주시면 될 것 같습니다.

DB커넥션 정보는 dataSources.yml에 있습니다. mysql을사용했습니다.

 

profile은 그냥 default로 놓으나, mysql을 사용하나 결과는 같습니다.

감사합니다.

김대호님의 프로필 이미지
김대호

작성한 질문수

질문하기