메서드가 연속으로 실행된다면, 트랜잭션이 중첩되어 데이터가 저장되지 않고 계속 수정되는 거 아냐??

2023. 7. 26. 00:07프로젝트/[Dotoring] 멘토링 어플리케이션

728x90

에코노베이션이라는 동아리에서 프로젝트를 수행하며 "메서드가 연속으로 실행되면, 트랜잭션이 중첩되어 데이터가 저장되지 않고 계속 수정되는 거 아냐??"라는 생각을 해본 적이 있다.

이 생각에 대한 과정과 그와 관련된 개념들을 정리해보려고 한다.

 

먼저, 스프링의 기본 전략을 알아야한다.

 

스프링의 기본 전략은 트랜잭션 범위의 영속성 컨텍스트 전략, 즉 트랜잭션의 범위와 영속성 컨텍스트의 범위가 같다는 것이다.

 

 

 

 

이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 즉, 같은 트랜잭션과 같은 영속성 컨텍스트를 사용한다는 것이다.

 

보통 트랜잭션은 service 계층의 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다.

 

트랜잭션이 시작되는 데 숨은 원리는 스프링 트랜잭션 AOP이다.

 

이때, 메서드가 정상 종료되면 트랜잭션이 커밋되어 영속성 컨텍스트를 플러시하여 변경내용을 DB에 최종반영한다.

만약, 메서드가 정상 종료되지 못 하면 트랜잭션을 롤백하고 종료한다.

 

 

아래는 현재 진행 중인 프로젝트의 코드의 일부이다.

 

(thank you, Devdongbaek!!)

DEVdongbaek - Overview

 

DEVdongbaek - Overview

DEVdongbaek has 17 repositories available. Follow their code on GitHub.

github.com

 

 

@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<>();	

	...
	
}

 

 

위 메서드로 위의 개념을 설명하자면, 아래와 같다.

  1. sendLetterWhereIn() 메서드가 실행되기 전 스프링 트랜잭션 AOP에 의해 트랜잭션이 시작된다.
  2. letterRepository.save()에 의해서 letter 엔티티는 영속성 컨텍스트의 관리를 받는다. 그리고 현재 1차 캐시에 저장되어 있다.(letter : 영속, room과 user는 비영속)
  3. sendLetterWhereIn() 메서드가 종료되면 트랜잭션이 끝나 영속성 컨텍스트 또한 종료된다. 따라서 letter 엔티티는 준영속상태가 된다.

 

필자는 위 메서드가 연속해서 실행되면, 아래와 같은 상황이 발생할 수 있다고 생각해 아래와 같은 가설을 세워봤다.

 

 

letterMentoService.sendLetterWhereIn(letter1, mento, room1);
letterMentoService.sendLetterWhereIn(letter2, mento, room1);
letterMentoService.sendLetterWhereIn(letter3, mento, room1);
letterMentoService.sendLetterWhereIn(letter4, mento, room1);
...



  1. sendLetterWhereIn() 메서드가 실행되기 전 스프링 트랜잭션 AOP에 의해 트랜잭션이 시작된다.
  2. letterRepository.save()에 의해서 letter 엔티티는 영속성 컨텍스트의 관리를 받는다. 그리고 현재 1차 캐시에 저장되어 있다.(letter 엔티티는 영속상태, room 엔티티는 준영속 상태, member 엔티티 또한 준영속 상태)
    1. 다시 sendLetterWhereIn() 메서드가 실행되기 전 트랜잭션이 시작된다??
    2. @Transactional 어노테이션의 propagation 속성의 기본 전략은 REQUIRED이다.
    3. 이 말은 즉, sendLetterWhereIn() 메서드가 중첩으로 수행된다면, 기존의 트랜잭션 내에서 계속 실행될 수 있다는 뜻이다.
      1. 그렇다면, 혹시 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

 

GitHub - JNU-econovation/Dotoring-BE: 더지팀 백엔드 레포지터리입니다.

더지팀 백엔드 레포지터리입니다. Contribute to JNU-econovation/Dotoring-BE development by creating an account on GitHub.

github.com

 

728x90