에코노베이션이라는 동아리에서 프로젝트를 수행하며 "메서드가 연속으로 실행되면, 트랜잭션이 중첩되어 데이터가 저장되지 않고 계속 수정되는 거 아냐??"라는 생각을 해본 적이 있다.
이 생각에 대한 과정과 그와 관련된 개념들을 정리해보려고 한다.
먼저, 스프링의 기본 전략을 알아야한다.
스프링의 기본 전략은 트랜잭션 범위의 영속성 컨텍스트 전략, 즉 트랜잭션의 범위와 영속성 컨텍스트의 범위가 같다는 것이다.
이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 즉, 같은 트랜잭션과 같은 영속성 컨텍스트를 사용한다는 것이다.
보통 트랜잭션은 service 계층의 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다.
트랜잭션이 시작되는 데 숨은 원리는 스프링 트랜잭션 AOP이다.
이때, 메서드가 정상 종료되면 트랜잭션이 커밋되어 영속성 컨텍스트를 플러시하여 변경내용을 DB에 최종반영한다.
만약, 메서드가 정상 종료되지 못 하면 트랜잭션을 롤백하고 종료한다.
아래는 현재 진행 중인 프로젝트의 코드의 일부이다.
(thank you, Devdongbaek!!)
@Transactional
public Letter sendLetterWhereIn(LetterByMemberRequestDTO letterRequestDTO, Menti user, Room room) {
Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
letter.addLetter(room);
letterRepository.save(letter);
return letter;
}
위는 쪽지를 보낼 때 사용하는 service계층의 메서드이다.
간단히 설명하자면, letterRequestDTO에는 쪽지의 내용이 있고, room은 쪽지를 보내는 방을 뜻한다.
엔티티들의 관계는 아래와 같다.
@Entity
public class Room {
...
OneToMany(mappedBy = "room", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private List<Letter> letterList = new ArrayList<>();
...
}
위 메서드로 위의 개념을 설명하자면, 아래와 같다.
- sendLetterWhereIn() 메서드가 실행되기 전 스프링 트랜잭션 AOP에 의해 트랜잭션이 시작된다.
- letterRepository.save()에 의해서 letter 엔티티는 영속성 컨텍스트의 관리를 받는다. 그리고 현재 1차 캐시에 저장되어 있다.(letter : 영속, room과 user는 비영속)
- sendLetterWhereIn() 메서드가 종료되면 트랜잭션이 끝나 영속성 컨텍스트 또한 종료된다. 따라서 letter 엔티티는 준영속상태가 된다.
필자는 위 메서드가 연속해서 실행되면, 아래와 같은 상황이 발생할 수 있다고 생각해 아래와 같은 가설을 세워봤다.
letterMentoService.sendLetterWhereIn(letter1, mento, room1);
letterMentoService.sendLetterWhereIn(letter2, mento, room1);
letterMentoService.sendLetterWhereIn(letter3, mento, room1);
letterMentoService.sendLetterWhereIn(letter4, mento, room1);
...
- sendLetterWhereIn() 메서드가 실행되기 전 스프링 트랜잭션 AOP에 의해 트랜잭션이 시작된다.
- letterRepository.save()에 의해서 letter 엔티티는 영속성 컨텍스트의 관리를 받는다. 그리고 현재 1차 캐시에 저장되어 있다.(letter 엔티티는 영속상태, room 엔티티는 준영속 상태, member 엔티티 또한 준영속 상태)
- 다시 sendLetterWhereIn() 메서드가 실행되기 전 트랜잭션이 시작된다??
- @Transactional 어노테이션의 propagation 속성의 기본 전략은 REQUIRED이다.
- 이 말은 즉, sendLetterWhereIn() 메서드가 중첩으로 수행된다면, 기존의 트랜잭션 내에서 계속 실행될 수 있다는 뜻이다.
- 그렇다면, 혹시 letter1의 내용이 letter2의 내용으로 letter2의 내용이 letter3의 내용으로 계속 덮어씌워질 수 있지 않을까 라는 생각을 해보았다!!
@Transactional의 기본 전략
@Transactional의 기본전략은 아래와 같다.
지금부터 각각의 의미를 살펴보도록 하자.
- Propagation propagation() default Propagation.REQUIRED
- 해당 메서드를 호출한 곳에서 트랜잭션이 설정되어 있지 않으면 트랜잭션을 새로 시작한다.
- 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존의 트랜잭션 내에서 실행된다.
- Isolation isolation() default Isolation.DEFAULT;
- Use the default isolation level of the underlying datastore. All other levels correspond to the JDBC isolation levels.
- 즉, DB에 따라 달라진다고 나와있다.
- Mysql을 기준으로 설명하자면, MySql은 REPEATABLE_READ를 사용
REPEATABLE_READ란 반복 가능한 읽기로 PHANTOM READ 문제가 생길 가능성이 있다.
PHANTOM READ란 반복해서 조회할 시 조회되는 대상이 달라지는 것을 뜻한다.
트랜잭션의 격리성과 고립성은 다음 포스팅에서 더 자세히 다루도록 하겠습니다!!
아래의 레포에서 자세한 코드를 확인할 수 있습니다.
https://github.com/JNU-econovation/Dotoring-BE
'Project Trouble Shooting > [Dotoring] 멘토링 어플리케이션' 카테고리의 다른 글
QueryDSL을 사용해 통계(Count)쿼리의 결과를 사용해서 정렬하기 (0) | 2023.08.15 |
---|---|
데이터 무결성 지켜야지, 안 지킬거야?? (0) | 2023.08.07 |
식별자, 비식별자 관계는 뭐고 언제 사용해야하는 걸까?? (0) | 2023.08.04 |
엔티티의 통합과 분리의 기준이 뭐야?? (0) | 2023.08.04 |
읽기 전용 쿼리의 성능 최적화에 대한 고민 (0) | 2023.07.31 |