Transaction은 DB의 명령어들의 논리적인 묶음으로 ACID속성을 가진다.
ACID
- Atomicity(원자성)
- 부분적 성공을 허용하지 않음
- 하나의 Transaction에서 RuntimeException이나 error가 발생되면 commit없이 모두 rollback된다.
- checkedException의 경우에는 rollback되지 않는다.
- Transaction에 대해서 RuntimeException이 아닌 다른 예외를 사용할 경우 개발자가 직접 catch문 내에서 rollback까지 수행해줘야한다.
- @Transactional(rollbackFor = Exception.class)처럼 Annotation을 작성하면 checkedException에서도 Rollback이 수행된다.
- 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)
- 최고 수준의 격리 단계
- 다른 transaction이 끝날 때까지 lock상태가 된다.
- https://ilhee.tistory.com/32
- 보통 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이 높다.