티스토리 뷰

Spring

트랜잭션의 전파속성 중 Nested 사용해보기

개발하고싶은개발자 2023. 12. 2. 14:58

스프링에서는 트랜잭션을 사용할 때 @Transactional 어노테이션을 사용한다.

 

@Transactional 어노테이션의 속성 중 propagation(전파) 이라는 것이 있는데, 이미 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것이 전파 속성(Propagation)이다. 

 

전파 속성에는 총 7가지가 옵션이 있는데 이에 대한 자세한 내용은 https://mangkyu.tistory.com/269 해당 블로그에 자세히 나와 있으니 참고하면 되겠다.

 

 

 

오늘은 이 중 Nested라는 옵션을 사용해보도록 하겠다.

 

위의 블로그에 나와 있듯이 Nested 옵션은 JPA에서 지원하지 않는다. 따라서 Jpa를 사용하지 않고 단순히 JdbcTemplate을 사용하면 될 것 같지만, 따로 설정을 해주지 않으면 JdbcTemplate을 사용해도 TransactionManager로 JpaTransactionManager을 사용하기 때문에 여전히 작동하지 않는다.

 

 

Member 테이블이 아래와 같이 있고 name에는 unique 제약조건이 있다고 가정해 보자.

id name created_at
1 홍길동 2023-11-24 00:00:00.000000
2 김길동 2023-11-23 23:59:59.000000
3 이길동 2023-11-23 00:00:00.000000

 

 

 

 

아래와 같이 코드를 작성하고 실행해 보자.

//MemberAppService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberAppService {
	private final MemberService memberService;

	@Transactional
	public void createMember() {
		List<String> names = List.of("최길동", "홍길동", "고길동");
		List<String> duplicateNames = new ArrayList<>();

		for(String name : names) {
			try {
				memberService.createMemberByName(name);
			}catch (Exception e) {
				duplicateNames.add(name);
				log.error(e.getMessage());
			}
		}
	}
}

//MemberService.java
@Service
@RequiredArgsConstructor
public class MemberService {
	private final JdbcTemplate jdbcTemplate;

	@Transactional(propagation = Propagation.NESTED)
	public void createMemberByName(String name) {
		String sql = "insert into Member (name) values (?)";

		jdbcTemplate.update(sql, name);
	}
}

 

실행하면 error가 발생하는데 catch 블록의 e.getMessage()의 에러 내용이 'JpaDialect does not support savepoints - check your JPA provider's capabilities' 라고 나온다. 

 

 

 

 

baeldung에서 nestedTransactionAllowed라는 옵션을 true로 설정해주면 된다는 정보를 보게 되었다.

https://www.baeldung.com/spring-transactional-propagation-isolation

 

그래서 한 번 nestedTransactionAllowed 옵션을 true로 설정해주고 실행해 보았다. 위의 코드에서 아래 코드만 추가해보고 실행해보았다.

//DatabaseConfig.java
@Configuration
@EnableJpaRepositories(
	basePackages = "hello.hellospring.repository",
	transactionManagerRef = "jpaTransactionManager"
)
@EnableTransactionManagement
public class DbConfig {

	@Bean
	public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory entityManagerFactory) {
		JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
		jpaTransactionManager.setNestedTransactionAllowed(true);
		jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
		return jpaTransactionManager;
	}
}

 

그러나 여전히 'JpaDialect does not support savepoints - check your JPA provider's capabilities' 에러가 발생하며 되지 않았다. 그리고 nestedTransactionAllowed 옵션의 default 값이 true인 것 같았다.nestedTransactionAllowed 옵션과는 상관없이 실행되지 않았다.

 

해당 내용이 stackoverflow에도 올라와 있었다.

https://stackoverflow.com/questions/45517330/what-is-the-purpose-of-setnestedtransactionallowed-in-jpatransactionmanager

 

 

 

 

그러던 중 우아한 형제들의 기술 블로그를 보았는데 JtaTransactionManager에서도 안되고 DataSourceTransactionManager 에서는 동작한다는 것을 알게 되었다. 

 

 

이제 아래와 같이 DataSourceTransactionManager 빈을 등록하고 해당 트랜잭션 매니저를 사용하면 @Transactional의 전파속성으로 Nested를 사용할 수 있다.

//DatabaseConfig.java
@Configuration
@EnableJpaRepositories(
	basePackages = "hello.hellospring.repository",
	transactionManagerRef = "jpaTransactionManager"
)
@EnableTransactionManagement
public class DbConfig {

	@Bean
	public PlatformTransactionManager datasourceTransactionManager(DataSource dataSource){
		DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
		dataSourceTransactionManager.setDataSource(dataSource);
		return dataSourceTransactionManager;
	}

	@Bean
	public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory entityManagerFactory) {
		JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
		jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
		return jpaTransactionManager;
	}
}

//MemberAppService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberAppService {
	private final MemberService memberService;

	@Transactional(transactionManager = "datasourceTransactionManager")
	public void createMember() {
		List<String> names = List.of("최길동", "홍길동", "고길동");
		List<String> duplicateNames = new ArrayList<>();

		for(String name : names) {
			try {
				memberService.createMemberByName(name);
			}catch (Exception e) {
				duplicateNames.add(name);
				log.error(e.getMessage());
			}
		}
	}
}

//MemberService.java
@Service
@RequiredArgsConstructor
public class MemberService {
	private final JdbcTemplate jdbcTemplate;

	@Transactional(transactionManager = "datasourceTransactionManager", propagation = Propagation.NESTED)
	public void createMemberByName(String name) {
		String sql = "insert into Member (name) values (?)";

		jdbcTemplate.update(sql, name);
	}
}

 

트랜잭션 매니저로 DataSourceTransactionManager를 사용한다고 명시해 주고 실행해 보자. 그러면 이번에도 error가 발생하지만 e.getMessage()의 내용은 'duplicate key value violates unique constraint "name_unique"'라고 나온다. 즉 정상적으로 실행되고  '홍길동'이 기존 데이터와 중복되기 때문에 중복 에러가 발생한다. 그리고 나머지 '최길동', '고길동'의 경우 중복되지 않고 부모 Transaction에서 에러가 발생하지 않았기 때문에 정상적으로 저장된다.

 

실행 후 Member 테이블의 존재하는 데이터는 아래와 같다.

id name created_at
1 홍길동 2023-11-24 00:00:00.000000
2 김길동 2023-11-23 23:59:59.000000
3 이길동 2023-11-23 00:00:00.000000
4 최길동 2023-12-02 14:48:56.606572
5 고길동 2023-12-02 14:48:56.606572

 

만약 MemberAppService의 createMember() 메소드의 catch 블록에서 throw e; 를 하게 되면 부모 Transaction에서 에러가 발생했기 때문에 '최길동', '고길동' 모두 저장되지 않는다.

 


ref

'Spring' 카테고리의 다른 글

[Security] hasIpAddress 사용하기  (0) 2024.03.09
데이터의 모든 변경이력 관리하기 with Hibernate  (1) 2023.12.23
윈도우 함수 사용하기 - (1/3)  (0) 2023.12.22
DispatcherServlet  (1) 2023.11.18
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30