Home Transaction
Post
Cancel

Transaction

Transaction은 DB의 명령어들의 논리적인 묶음으로 ACID속성을 가진다.

ACID

  • Atomicity(원자성)
    • 부분적 성공을 허용하지 않음
    • 하나의 Transaction에서 RuntimeException이나 error가 발생되면 commit없이 모두 rollback된다.
      • checkedException의 경우에는 rollback되지 않는다.
        • Transaction에 대해서 RuntimeException이 아닌 다른 예외를 사용할 경우 개발자가 직접 catch문 내에서 rollback까지 수행해줘야한다.
      • @Transactional(rollbackFor = Exception.class)처럼 Annotation을 작성하면 checkedException에서도 Rollback이 수행된다.
  • Consistency(일관성)
    • 데이터간의 정합성을 맞추는 작업
  • Isolation(독립성)
    • Transaction내의 데이터 조작에 대해서는 다른 Transaction에 대해 독립성을 가진다.
  • Durability(지속성)
    • 데이터는 영구적으로 보관

많이 하는 실수

  • checked Exception은 Rollback이 되지 않는것을 처리하지 않음
  • @Transactional 없는 method로 Transactional method호출

    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
    
      @Service
      @RequiredArgsConstructor
      public class TestService {
    
          private final MemberRepository memberRepository;
          private final TeamRepository teamRepository;
        
          public void put(){
              this.putMemberAndTeam();
          }
            
          @Transactional
          public void putMemberAndTeam(){
    
              Member member = new Member();
              member.setName("A");
        
              memberRepository.save(book);
        
              Team team = new Team();
              team.setName("ATeam");
        
              TeamRepository.save(author);
              throw new RuntimeException();
          }
      }
    
    • Bean 외부에서 호출이 되는 시점에 AOP에서 Annotation을 읽고 처리하기 때문에 put()을 실행시키면 Transactional Annotation이 동작하지 않는다.

    • put을 실행시키면 RuntimException이 발생되어도 Transactional이 동작하지 않았으므로 DB에 데이터가 반영된다.

Transactional Annotation

  • 자동완성을 보면 javax의 Transactional, spring의 Transactional이 있다. 둘 다 같은 역할을 수행하지만 javax의 것은 더 범용적이고, spring의 것은 더 다앙한 기능을 제공해준다.
  • isolation
    • javax에는 없음
    • @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    • DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE의 5개의 옵션을 제공한다. 레벨이 높아질 수록 격리의 단계가 강력해지고, 데이터의 정합성을 보장해주지만 동시 처리 수행 능력이 떨어진다.
      • DEFAULT : DB의 default 격리 단계를 따름(mysql : REPEATABLE_READ)
      • READ_UNCOMMITTED(level 0)
        • 다른 Transaction의 commit 되지 않은 data를 읽을 수 있는 단계(dirty read)

          1
          2
          3
          4
          5
          6
          7
          8
          9
          
            @Test
            void isolationTest(){
                Member member = new Member();
                member.setName("A");
                memberRepository.save(member);
                         
                memberService.get(1L);
                System.out.println(memberRepository.findAll());
            }
          
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          
            @Service
            @RequiredArgsConstructor
            public class memberService {
          
                private final MemberRepository memberRepository;
                private final TeamRepository teamRepository;
                          
                @Transactional(isolation = Isolation.READ_UNCOMMITTED)    
                public void get(Long id){
                    System.out.println(memberRepository.findById(id));
                    System.out.println(memberRepository.findAll());
          
                    System.out.println(memberRepository.findById(id));
                    System.out.println(memberRepository.findAll());
          
                    Member member = memberRepository.findById(id).get();
                    member.setName("B");
                    memberRepository.save(member);
                }
            }
          
          • test를 Debug모드로 실행해 get Method에 Break point를 걸어두고 mysql console로 다른 transaction을 생성해 age값을 업데이트 했다.
          • 이후 test를 break point이후로 진행시키면 console의 transaction을 기다리게된다.
            • commit or rollback
          • 이때 문제가 발생하게되는데 console의 transaction을 rollback해서 console에서 변경한 age의 변경사항은 무시하려했지만 모든 동작이 끝난 뒤 조회를 해보면 age와 name모두 변경이되어 DB에 반영된다.
            • JPA 업데이트 방식으로 발생하는 문제이다.
            • 업데이트를 수행할 때 모든 property를 set하는 방식으로 진행된다.
            • 따라서 findById로 가져온 member는 cache에만 있는 age가 변경된 entity이고 update를할 때 모든 property를 set 해버리므로 console의 동작을 rollback했지만 age의 변경사항이 그대로 적용되는 것이다.
            • Entity에 @DynamicUpdate를 사용하면 필요한 Column만 update하기 때문에 이런 문제를 방지 할 수 있다.
      • READ_COMMITTED(level 1)
        • Commit된 data만 읽어온다.
        • @DynamicUpdate를 사용하지 않아도 해당 문제가 발생하지 않는다.
        • (문제)REPEATABLE_READ상태가 될 수 있다.
          • 조작을 하지 않았지만 transaction내에서 조회 값이 달라질 수 있는 상태
          • 다른 transaction에서 commit되었을 때(terminal)
      • REPEATABLE_READ(level 2)
        • transaction내부에서 몇 번을 조회해도 항상 같은 값이 나오는것을 보장해준다.
        • 현재 transaction이 실행 됐을 때의 snapshot을 별도로 저장하고 transaction종료 까지 저장한 snapshot을 사용한다.
        • (문제)Phantom read : 한 트랜잭션 내에서 같은 쿼리문이 실행되었음에도 불구하고 조회 결과가 다른 경우를 뜻한다.
      • SERIALIZABLE(level 3)
    • 보통 READ_COMMITTED, REPEATABLE_READ를 많이 사용한다.
  • mysql transaction

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      mysql> start transaction;
      Query OK, 0 rows affected (0.00 sec)
        
      mysql> update member set age=9999;
      Query OK, 1 row affected (0.00 sec)
      Rows matched: 1  Changed: 1  Warnings: 0
        
      mysql> select * from member;
      +----+----------------------------+----------------------------+---------+-----------+
      | id | updated_at                 | created_at                 |Age      | name      |
      +----+----------------------------+----------------------------+---------+-----------+
      |  1 | 2021-06-29 14:42:43.509258 | 2021-06-29 14:42:43.509258 |9999     | B         | 
      +----+----------------------------+----------------------------+---------+-----------+
      1 row in set (0.00 sec)
        
      mysql> commit;
      Query OK, 0 rows affected (0.00 sec)
    

Transaction propagation

  • 7가지의 propagation옵션을 지원한다.
  • REQUIRED(default)
    • @Transactional(propagation = Propagation.REQUIRED)
    • 기존 사용하던 transaction이 있다면 해당 transaction을 사용하고 없다면 transaction을 생성
    • save같은 method가 REQUIRED로 되어있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      @Service
      @RequiredArgsConstructor
      public class MemberService {
          private final MemberRepository memberRepository;
          private final TeamRepository teamRepository;
          private final EntityManager em;
          private final TeamService teamService;
        
          @Transactional(propagation = Propagation.REQUIRED)
          public void putMemberAndTeam(){
    
              Member member = new Member();
              member.setName("A");
        
              memberRepository.save(member);
              teamService.putTeam();
              //throw new RuntimeException();
          }
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      @Service
      @RequiredArgsConstructor
      public class TeamService {
          private final TeamRepository teamRepository;
        
          @Transactional(propagation = Propagation.REQUIRED)
          public void putTeam(){
              Team team = new Team();
              team.setName("ATeam");
              teamRepository.save(team);
      		throw new RuntimeException();
          }
      }
    
    • putMemberAndTeam를 수행 하면 Propagation이 REQUIRED이기 때문에 putTeam에서 같은 transaction을 사용한다. 따라서 putTeam에서 발생한 RuntimeException으로 putMemberAndTeam의 수행 동작도 rollback이 된다.
  • REQUIRES_NEW
    • 항상 새로운 transaction을 생성한다.
    • putMemberAndTeam를 실행 했을 때 putTeam에서 예외가 발생하면 team만 roll back되고, putMemberAndTeam에서 예외가 발생하면 member에 대해서만 rollback된다.
  • NESTED
    • 종속적인 transaction을 사용한다.
    • putMemberAndTeam를 실행 했을 때 putTeam의 예외는 team만 rollback 시키고, putMemberAndTeam의 예외는 두 가지 모두 rollback시킨다.
  • SUPPORTED
    • 기존 사용하던 transaction이 있다면 해당 transaction을 사용하고 없으면 생성하지 않는다.
  • NOT_SUPPORTED
    • transaction을 사용하지 않는다.
  • MENDATORY
    • 이미 생성된 transaction이 반드시 있어야 하고 없다면 error가 발생된다.
  • NEVER
    • 이미 생성된 transaction이 반드시 없어야 하고 없다면 error가 발생한다.
  • 보통 REQUIRES 혹은 REQUIRES_NEW를 사용한다.

Transactional의 범위

  • Transactional Annotation은 method뿐만 아니라 class에도 사용할 수 있다.
  • method에 사용할 경우 method가 호출 됐을 때부터 종료 될 때 까지가 적용 범위이다.
  • class에 적용할 경우 class내부의 모든 method에 Transactional Annotation을 적용하는 것이다.
  • 우선 순위는 method의 annotation이 높다.
This post is licensed under CC BY 4.0 by the author.