들어가기

이번 포스팅에서는 트랜잭션 전파과정에 대해서 알아보겠습니다.

트랜잭션 격리 수준에 대해서 궁금하신 분은 아래 포스팅을 참고하시는 것을 추천드리겠습니다.

 

https://hoestory.tistory.com/86

 

[MySQL] 트랜잭션 격리 수준

들어가기 전 이번 포스팅에서는 데이터베이스 트랜잭션의 격리 수준에 대해서 알아보겠습니다. 트랜잭션 격리 수준트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행되는 상황에서 특정 트

hoestory.tistory.com

 

 

 

트랜잭션 전파과정이란?

트랜잭션 전파과정은 하나의 트랜잭션이 다른 트랜잭션과 상호작용 하는 것을 의미합니다. 설정에 따라 하나의 트랜잭션이 다른 트랜잭션에 합류하거나, 새로운 트랜잭션을 생성하는 등 다른 트랜잭션과 상호작용을 합니다.

 

트랜잭션 전파과정의 종류

  • Required : 기존 트랜잭션이 존재하면 합류를 합니다. 존재하지 않으면 새로운 트랜잭션을 생성합니다.
  • Requires_New : 기존 트랜잭션이 존재하여도 무조건 새로운 트랜잭션을 생성합니다.
  • Nested : 기존 트랜잭션이 존재하면 중첩 트랜잭션을 생성하고 그렇지 않으면 새로운 트랜잭션을 생성합니다.
  • Mandatory : 기존 트랜잭션이 존재하면 합류하지만 존재하지 않을 경우 예외가 발생합니다.
  • Never : 트랜잭션 동작을 하지 않고 기존 트랜잭션이 존재할 경우 예외가 발생합니다.
  • Not_Supported : 기존 트랜잭션이 존재하여도 트랜잭션 동작을 하지 않습니다.
  • Supports : 기존 트랜잭션이 존재하면 합류하고 그렇지 않으면 트랜잭션 동작을 하지 않습니다.

 

지금까지 트랜잭션 전파과정과 종류에 대해서 간단하게 알아보았습니다.
이제부터 전파과정을 예제를 통해 각 종류별로 합류하는 트랜잭션이 새로운 트랜잭션인지 아닌지, 커밋이 기존 트랜잭션과 같이 되는지 안되는지 예제를 통해서 알아보겠습니다.

 

예제에서 사용할 코드들을 먼저 정리하겠습니다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private int totalCount;

    private Team(String name, int totalCount) {
        this.name = name;
        this.totalCount = totalCount;
    }

    public void updateTotalCount(int count) {
        this.totalCount += count;
    }
    public static Team of(String name, int count) {
        return new Team(name, count);
    }
}


============================

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;



    private Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }

    public static Member of(String name, Team team){
        return new Member(name, team);
    }
}

=================================================

public interface MemberRepository extends JpaRepository<Member, Long> {
}

=================================================

public interface TeamRepository extends JpaRepository<Team, Long> {
}

=================================================

public record Result(
    String memberServiceTransactionName,
    Boolean isNewTransactionMemberService,
    String teamServiceTransactionName,
    Boolean isNewTransactionTeamService) {
}

 

 

예제에서 사용할 코드를 기반으로 트랜잭션 전파과정에 대해서 알아보겠습니다.
(참고 1 : 예제는 개발을 하면서 많이 접할 수 있는 Required, Requires New만 진행하겠습니다.
참고 2: Nested는 JPA 트랜잭션에서 지원하지 않아 DataSourceTransactionManager에서 동작합니다.)

 

Required

 

Required는 기존 트랜잭션이 존재하면 합류를 합니다. 존재하지 않으면 새로운 트랜잭션을 생성합니다.

기존 트랜잭션에 합류를 한다는 것은 기존 트랜잭션이 커밋 또는 롤백이 이루어지면 합류한 트랜잭션도 커밋 또는 롤백이 됩니다.

(전파과정의 기본 값은 Required라 생략 가능합니다)

 

 

 

1-1. 새로운 트랜잭션은 기존 트랜잭션이 존재하면 합류

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final TeamService teamService;
    @Transactional(propagation = Propagation.REQUIRED)
    public Result isNewTransactionPropagationRequired() {
        String memberTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean isMemberNewTransaction = TransactionAspectSupport.currentTransactionStatus()
            .isNewTransaction();
        TeamResult teamResult = teamService.isNewTransactionPropagationRequired();
        return new Result(memberTransactionName,
            isMemberNewTransaction,
            teamResult.transactionName(),
            teamResult.isNewTransaction());
    }
}

=========================================================================

@Service
public class TeamService {
    @Transactional(propagation = Propagation.REQUIRED)
    public TeamResult isNewTransactionPropagationRequired() {
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus()
            .isNewTransaction();
        return new TeamResult(currentTransactionName, isNewTransaction);
    }
}

 

 

@SpringBootTest
class TransactionTest {
    @Autowired
    private MemberService memberService;

    @Test
    @DisplayName("기존 트랜잭션(MemberService)이 존재하면 새로운 트랜잭션(TeamService)은 기존 트랜잭션에 합류한다.")
    void returnTrueNewTransaction() {
        //give && when
        Result result = memberService.isNewTransactionPropagationRequired();
        //then
        assertThat(result.memberServiceTransactionName()).isEqualTo(
            result.teamServiceTransactionName());
        assertThat(result.isNewTransactionMemberService()).isTrue();
        assertThat(result.isNewTransactionTeamService()).isFalse();
    }
}

 

 

위에 테스트 코드를 확인해 보면 새로운 트랜잭션(TeamService)이 기존 트랜잭션(MemberService)에 합류하게 되면서 기존 트랜잭션의 이름과 동일하고 새로운 트랜잭션은 새로운 생성이 아닌 기존 트랜잭션으로 판단하는 것을 확인할 수 있습니다.

 

 

1-2. 기존 트랜잭션이 존재하지 않을 경우 새로운 트랜잭션 생성

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final TeamService teamService;
    
    public Result notTransactionMemberServiceAndExistsTransactionTeamService() {
        TeamResult teamResult = teamService.isNewTransactionPropagationRequired();
        try {
            String memberTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
            boolean isMemberNewTransaction = TransactionAspectSupport.currentTransactionStatus()
                .isNewTransaction();
            return new Result(memberTransactionName,
                isMemberNewTransaction,
                teamResult.transactionName(),
                teamResult.isNewTransaction());
        }catch (NoTransactionException e) {
            return new Result(null,
                null,
                teamResult.transactionName(),
                teamResult.isNewTransaction());

        }
    }
}

=========================================

@Service
public class TeamService {
    @Transactional(propagation = Propagation.REQUIRED)
    public TeamResult isNewTransactionPropagationRequired() {
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus()
            .isNewTransaction();
        return new TeamResult(currentTransactionName, isNewTransaction);
    }
}

 

 

@SpringBootTest
class TransactionTest {
    @Autowired
    private MemberService memberService;

    @DisplayName("기존 트랜잭션이 없으면 새로운 트랜잭션을 생성한다.")
    @Test
    void notBaseTransactionCreateNewTransaction() {
        //give && when
        Result result = memberService.notTransactionMemberServiceAndExistsTransactionTeamService();

        //then
        assertThat(result.memberServiceTransactionName()).isNull();
        assertThat(result.teamServiceTransactionName()).isNotNull();
        assertThat(result.isNewTransactionMemberService()).isNull();
        assertThat(result.isNewTransactionTeamService()).isTrue();
    }
}

 

새로운 트랜잭션은 기존 트랜잭션(MemberService)이 존재하지 않으면 새로운 트랜잭션을 생성합니다. 

 

2. 커밋 또는 롤백 여부

 

기존 트랜잭션 또는 합류한 새로운 트랜잭션에서 커밋 또는 롤백이 발생이 하면 기존 트랜잭션과 새로운 트랜잭션은 같이 커밋 또는 롤백이 발생합니다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final TeamService teamService;
    private final MemberRepository memberRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void save(List<String> memberName, String teamName) {
        Team team = teamService.save(teamName, memberName.size());
        List<Member> newMembers = memberName.stream()
            .filter(name -> memberRepository.findByName(name).isEmpty())
            .map(name -> Member.of(name, team))
            .collect(Collectors.toList());
        memberRepository.saveAll(newMembers);
    }
}

====================================================

@Service
@RequiredArgsConstructor
public class TeamService {
    private final TeamRepository teamRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public Team save(String name, int newMemberCount) {
        Optional<Team> findTeam = teamRepository.findByName(name);
        Team team = null;
        if(findTeam.isPresent()) {
            team = findTeam.get();
            team.updateTotalCount(newMemberCount);
        }else {
             team = teamRepository.save(Team.of(name, newMemberCount));
        }
        return team;
    }
}

 

@SpringBootTest
class TransactionTest {
    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private TeamRepository teamRepository;

    @DisplayName("합류하게 된 새로운 트랜잭션은 기존 트랜잭션이 커밋되면 커밋한다.")
    @Test
    void commitBaseTransactionAndCommitNewTransaction() {
        //given
        String teamName = "팀 이름";
        String memberName = "hoeStory";
        List<String> memberNames = List.of("hoeStory");

        //when
        memberService.save(memberNames, teamName);

        //then
        Team team = teamRepository.findByName(teamName).get();
        Member member = memberRepository.findByName(memberName).get();
        Assertions.assertThat(team).isNotNull();
        Assertions.assertThat(member).isNotNull();
    }
}

 

 

위와 같이 정상적으로 로직이 실행되고 커밋이 되면 기존 트랜잭션과 새로운 트랜잭션이 커밋되는 것을 확인할 수 있습니다.

그럼 하나의 트랜잭션에서 롤백이 발생하면 같이 롤백이 발생하는지 아래 예제를 통해서 확인해 보겠습니다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final TeamService teamService;
    private final MemberRepository memberRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void save(List<String> memberName, String teamName) {
        Team team = teamService.save(teamName, memberName.size());
        List<Member> newMembers = memberName.stream()
            .filter(name -> memberRepository.findByName(name).isEmpty())
            .map(name -> Member.of(name, team))
            .collect(Collectors.toList());
        memberRepository.saveAll(newMembers);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

====================================================

@Service
@RequiredArgsConstructor
public class TeamService {
    private final TeamRepository teamRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public Team save(String name, int newMemberCount) {
        Optional<Team> findTeam = teamRepository.findByName(name);
        Team team = null;
        if(findTeam.isPresent()) {
            team = findTeam.get();
            team.updateTotalCount(newMemberCount);
        }else {
             team = teamRepository.save(Team.of(name, newMemberCount));
        }
        return team;
    }
}

 

  • TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() : 롤백

 

@SpringBootTest
class TransactionTest {
    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private TeamRepository teamRepository;

    @DisplayName("합류하게 된 새로운 트랜잭션은 기존 트랜잭션이 커밋되면 커밋한다.")
    @Test
    void commitBaseTransactionAndCommitNewTransaction() {
        //given
        String teamName = "팀 이름";
        String memberName = "hoeStory";
        List<String> memberNames = List.of("hoeStory");

        //when
        memberService.save(memberNames, teamName);

        //then
        Team team = teamRepository.findByName(teamName).get();
        Member member = memberRepository.findByName(memberName).get();
        Assertions.assertThat(team).isNotNull();
        Assertions.assertThat(member).isNotNull();
    }
}

 

정상적으로 동작하는 로직과 동일한 테스트코드를 실행시키면 아래와 같이 값이 존재하지 않다는 오류를 만나게 됩니다.

이유는 기존 트랜잭션에서 롤백이 진행되어 새로운 트랜잭션도 롤백이 되어 값이 존재하지 않기 때문입니다.

 

 

 

Requires New

Requires New는 기존 트랜잭션의 존재 유무와 상관없이 새로운 트랜잭션을 생성합니다.

기존 트랜잭션과 새롭게 생성된 트랜잭션은 각각의 트랜잭션으로 동작하여 기존 트랜잭션에서 커밋 또는 롤백이 일어나도 새롭게 생성된 트랜잭션에는 영향이 없고 반대로 새롭게 생성된 트랜잭션에서 커밋 또는 롤백이 발생해도 기존 트랜잭션에 영향이 없습니다.

 

 

 

1.  기존 트랜잭션 존재 여부 상관없이 새로운 트랜잭션 생성

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final TeamService teamService;
    private final MemberRepository memberRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Result isNewTransactionPropagationRequiresNew() {
        String memberTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean isMemberNewTransaction = TransactionAspectSupport.currentTransactionStatus()
            .isNewTransaction();
        TeamResult teamResult = teamService.isNewTransactionPropagationRequiresNew();
        return new Result(memberTransactionName,
            isMemberNewTransaction,
            teamResult.transactionName(),
            teamResult.isNewTransaction());
    }
}

=================================================

@Service
@RequiredArgsConstructor
public class TeamService {
    private final TeamRepository teamRepository;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public TeamResult isNewTransactionPropagationRequiresNew() {
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus()
            .isNewTransaction();
        return new TeamResult(currentTransactionName, isNewTransaction);
    }
}

 

 

@SpringBootTest
class TransactionTest {
    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private TeamRepository teamRepository;


    @DisplayName("기존 트랜잭션의 존재유무와 상관없이 새로운 트랜잭션을 생성한다.")
    @Test
    void ignoreBaseTransactionCreateNewTransaction() {
        //given && when
        Result result = memberService.isNewTransactionPropagationRequiresNew();

        //then
        Assertions.assertThat(result.memberServiceTransactionName())
            .isNotEqualTo(result.teamServiceTransactionName());
        Assertions.assertThat(result.isNewTransactionMemberService()).isTrue();
        Assertions.assertThat(result.isNewTransactionTeamService()).isTrue();
    }
}

 

 

위의 테스트코드를 보면 기존 트랜잭션과 새로운 트랜잭션은 각각 새롭게 생성된 트랜잭션이고 트랜잭션의 이름이 다른 것을 확인할 수 있습니다. 즉 Requires New는 Required처럼 새로운 트랜잭션이 기존 트랜잭션에 합류한 것이 아니라 트랜잭션을 새롭게 생성한 것을 확인할 수 있습니다.

 

2. 커밋 또는 롤백 여부

Requires New는 하나의 트랜잭션에서 롤백이 발생해도 새롭게 생성된 트랜잭션은 커밋이 될 수 있고 반대로 하나의 트랜잭션에서 커밋이 발생해도 새롭게 생성된 트랜잭션에서 롤백이 될 수 있습니다. 하나의 트랜잭션에서 롤백이 되더라도 다른 트랜잭션에 영향을 주지 않아 각각의 역할에 맞게 트랜잭션을 수행합니다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final TeamService teamService;
    private final MemberRepository memberRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveNew(List<String> memberName, String teamName) {
        Team team = teamService.saveNew(teamName, memberName.size());
        List<Member> newMembers = memberName.stream()
            .filter(name -> memberRepository.findByName(name).isEmpty())
            .map(name -> Member.of(name, team))
            .collect(Collectors.toList());
        memberRepository.saveAll(newMembers);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
 }
 
 ==========================================
 
 @Service
@RequiredArgsConstructor
public class TeamService {
    private final TeamRepository teamRepository;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Team saveNew(String name, int newMemberCount) {
        Optional<Team> findTeam = teamRepository.findByName(name);
        Team team = null;
        if(findTeam.isPresent()) {
            team = findTeam.get();
            team.updateTotalCount(newMemberCount);
        }else {
            team = teamRepository.save(Team.of(name, newMemberCount));
        }
        return team;
    }
}

 

  • TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() : 롤백

 

@SpringBootTest
class TransactionTest {
    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private TeamRepository teamRepository;

    @DisplayName("기존 트랜잭션에서 롤백이 발생해도 새로운 트랜잭션은 커밋한다.")
    @Test
    void rollbackBaseTransactionButCommitNewTransaction() {
        //given
        String teamName = "팀 이름";
        List<String> memberNames = List.of("hoeStory");

        //when
        memberService.saveNew(memberNames, teamName);

        //then
        Optional<Team> team = teamRepository.findByName(teamName);
        Optional<Member> member = memberRepository.findByName(memberNames.get(0));
        Assertions.assertThat(team.isPresent()).isTrue();
        Assertions.assertThat(member.isPresent()).isFalse();
    }
}

 

위 테스트코드를 확인해 보면 Member의 값은 존재하지 않고 Team에 대한 값만 존재하는 것을 확인할 수 있습니다.

이유는 기존 트랜잭션(MemberService)은 롤백이 되었고 새로운 트랜잭션(TeamService)은 롤백이 되지 않고 커밋이 되었기 때문입니다. 이걸 확인함으로써 Requires New 전파 과정은 기존 트랜잭션에서 롤백이 되더라도 새로운 트랜잭션에는 영향이 없는 것을 확인할 수 있습니다.

 

 

Github

예제에 사용된 코드가 작성되어 있는 깃허브 링크입니다.

https://github.com/cousim46/study-collection/tree/main/transaction-propagation/propagation

 

study-collection/transaction-propagation/propagation at main · cousim46/study-collection

Contribute to cousim46/study-collection development by creating an account on GitHub.

github.com