티스토리 뷰
스프링에서는 트랜잭션을 사용할 때 @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로 설정해주면 된다는 정보를 보게 되었다.
그래서 한 번 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에도 올라와 있었다.
그러던 중 우아한 형제들의 기술 블로그를 보았는데 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