Spring

Spring Batch와 직렬화 엑세스(ORA-08177) 오류 원인 및 해결

천방지축 개발노트 2024. 12. 14. 22:43
SQLException: ORA-08177: can't serialize access for this transaction

Spring Batch 설정 중 '이 트랜잭션에 대한 직렬화 액세스를 할 수 없습니다.'는 에러가 발생했다. 결론부터 말하자면, 현재 애플리케이션의 트랜잭션 격리 수준이 직렬화(Serializable)로 설정되어 있는데, 이 규칙을 지키지 못해 발생한 에러이다. 여기서 '격리 수준'이란 트랜잭션 간의 동시성 문제를 해결하기 위한 규칙을 의미한다. 그중 Serializable 옵션은 한 트랜잭션이 특정 데이터를 읽는 동안, 다른 트랜잭션이 동일한 데이터에 접근/수정하는 것을 막기 위해 그 데이터에 잠금을 걸어 동시 접근을 방지한다. 따라서, 모든 트랜잭션이 마치 직렬로 실행되는 것처럼 보장하는 옵션이다.

 

이 Serializable 방식은 직관적이면서 "안전하다"라고 느껴지지만, 현실적으로 '성능'과 '읽기 일관성'때문에 보편적인 DB작업에서는 사용되지 않는다. 실제로 A라는 트랜잭션이 조작 중인 데이터를 다른 B 트랜잭션이 조작 이전의 상태로 읽는 것은 현실적으로 대부분의 애플리케이션에서 허용되며, 이는 사용자 경험과 시스템 성능을 개선하는데 기여하는 부분이기도 하다. 특히 Batch 작업처럼 대량의 데이터를 처리하거나 여러 트랜잭션이 병렬로 실행되는 경우, 충돌과 교착 상태가 빈번하게 발생할 수 있기 때문에 Serializable은 ORA-08177 오류를 초래할 가능성이 높다.

 

근데 사실 난 격리 수준을 Serializable로 설정한 적이 없다. 근데 왜 이런 오류가 발생할까?

찾아보니 Spring Boot는 기본적으로 애플리케이션이 이용 중인 데이터베이스 벤더별 기본 격리 수준을 따른다고 한다. @Transactional 애노테이션의 내부를 보면 isolation()이라는 추상 메소드의 기본 리턴 값이 Isolation.DEFAULT로 설정돼있는 점을 확인할 수 있다.

Transactional 애노테이션 소스 코드

그리고 이 Isolation.DEFAULT가 데이터베이스 벤더의 기본값을 따른다는 의미인데, Oracle의 기본 격리 수준은 'READ_COMMITTED'이다(참고로 MySQL은 'REPEATABLE_READ'). 그니까 직접적으로 옵션을 건들지 않는 한 'READ_COMMITTED'로 자동 설정돼야 하는데, 대체 왜 Serializable로 설정되는 건데...😨

어쨌든 뭔가 이상하지만 직접적으로 'Spring Boot의 전체 트랜잭션 격리 수준을 설정하는 옵션'을 주면 될 것 같아 아래 설정을 추가해 보기도 했다.

spring.transaction.default-isolation: READ_COMMITTED

하지만 위 방법으로도 에러가 해결되지 않았는데.. 열심히 삽질하여 드디어 이유를 발견했다.

결론적으로 Spring Batch의 필수 구성 요소인 JobRepository가 트랜잭션 관리에 있어 독립적인 설정을 가지기 때문이었다. 내부적으로 Spring Batch는 자동 구성에 의한 JobRepositoryFactoryBean 클래스를 통해서 JobRepository를 생성하는데, 이때 AbstractJobRepositoryFactoryBean의 기본 설정이 Serializable로 되어 있기 때문에, spring.transaction.default-isolation 설정이 적용되지 않았던 것이었다.

ISOLATION_SERIALIZABLE

이를 해결하려면 JobRepository 설정을 명시적으로 변경해야 한다.

 

 

JobRepository 의 격리 수준 변경 방법 2가지

1) JobRepository 를 직접 Bean으로 재정의/등록하며 속성 변경

@Bean
public JobRepositoryFactoryBean jobRepository(DataSource dataSource, PlatformTransactionManager transactionManager) {
	JobRepositoryFactoryBean jobRepository = new JobRepositoryFactoryBean();
	jobRepository.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED");
	jobRepository.setDataSource(dataSource);
	jobRepository.setTransactionManager(transactionManager);
	return jobRepository;
}

 

2) 설정 파일을 통한 속성 변경

다음은 배치 작업 테이블을 생성/조작할 때의 트랜잭션 격리 수준을 제어하는 옵션인데, 스프링부트를 이용할 경우 프로퍼티 파일을 이용하여 더 간편하게 설정할 수 있다.

spring.batch.jdbc.isolation-level-for-create: READ_COMMITTED