트랜잭션은 어떤 작업을 하는데의 단위 묶음이라 한다. Jpa에서 중요한 역할을 하는데 Jpa에서 쿼리가 나가는 시점은 트랜잭션이 시작되고 끝나는 시점에 커밋이 되는 시점에서 쿼리가 발생한다. 또한 강제로 flush() 를 하면 쓰기 지연 sql저장소에 들어 있던 쿼리가 비워지면서 쿼리를 발생시킨다.
영속성과 트랜잭션의 관계
@Test
@DisplayName("member save")
@Transactional
@Rollback(value = false)
void memberSave() {
Member member = new Member("이기영", "기영@naver.com", Gender.MALE);
em.persist(member);
}
memberSave() 메서드를 한 작업의 묶음으로 설정하고 member 객체를 영속성에 올리는 과정이다. 어느정도 익숙한 코드일 것이다. 여기서 중요한 점은 db에 insert 를 시키는 코드가 없다.
테스트를 돌려보면 insert 쿼리가 발생하는 것을 볼 수 있고 연결된 db를 확인 해 보면 데이터가 들어간 것을 볼 수 있다.
만약 트랜잭션 단위로 되어 있지 않다면 어떻게 될까? 지속성이 필요한 작업의 단위에서 작업하라고 예외가 발생한다.
즉, 영속성(persistence)도 트랜잭션과 관련이 있다는 것을 알 수 있다.
트랜잭션 적용 범위
앞서 Member 객체를 저장하는 행위를 하였다. 코드를 리펙터링 해보자.
@Test
@DisplayName("member save")
@Rollback(value = false)
void memberSave() {
Member member = new Member("이기영", "기영@naver.com", Gender.MALE);
save(member);
}
@Transactional
private void save(Member member) {
em.persist(member);
}
save하는 과정을 메서드를 추출하여 private으로 설정하고 트랜잭션도 적용하였다. 과연 이 코드는 동작이 되는가?
😅 컴파일에서 바로 에러가 발생한다.
스프링에서 @Transactional 은 AOP 방식으로 프록시 패턴 동작이 된다.
해당 에노테이션이 붙은 메서드를 프록시 모드로 만들어 메서드를 사용하는데 non-public 메서드면 사용하지 못한다.
다음으로 final 키워드가 붙은 메서드도 사용하지 못한다. 기본적으로 오버라이딩 규칙을 생각하면 편할 것이다.
@Test
@DisplayName("member save")
@Rollback(value = false)
void memberSave() {
Member member = new Member("이기영", "기영@naver.com", Gender.MALE);
save(member);
}
@Transactional
public void save(Member member) {
em.persist(member);
}
이 코드는 성공일까?
실패다. 지속성이 있는 단위에서 작업을 하라고 예외가 발생한다.
🤔 분명 save()에 트랜잭션을 적용했는데 왜 적용이 안될까?
여기서 짚고 넘어가야 할 부분이 있다. 트랜잭션의 동작 원리이다.
트랜잭션의 동작은 AOP 기반으로 동작이 되고 있다. AOP는 관점 지향 프로그래밍으로 프록시 패턴으로 사용되고 있다.
🖐 프록시 패턴에 대해 잠깐 설명
구체적인 인터페이스를 사용하고 실행 시킬 클래스에 대한 객체가 들어갈 자리에 대리인의 객체를 대신 투입하게 하여 동작한다.
코드 구현
구체적인 인터페이스 생성
public interface Subject {
public String doAction();
}
인터페이스를 구현한 클래스 생성
public class RealSubject implements Subject{
@Override
public String doAction() {
return "Action";
}
}
프록시 클래스 생성
public class Proxy implements Subject {
private Subject proxy;
@Override
public String doAction() {
if (proxy == null) {
proxy = new RealSubject();
}
return proxy.doAction() + " Proxy";
}
}
클라이언트 입장에서 RealSubject 클래스를 사용하지 않고 Porxy 클래스 개게를 생성해서 RealSubject 클래스의 일을 대신 할 수 있다.
😭 그러므로 프록시가 내부 메서드에 있는 @Transactional 까지는 가져와서 사용하지 못한다는 얘기이다.
개별적으로 트랜잭션을 묶던가 별도로 트랜잭션 클래스를 만들어야 한다.
격리 수준
DEFAULT
- 데이터베이스의 격리 수준을 따른다(의존한다)
- mysql 기본 격리 수준
동작 테스트
트랜잭션의 한 단위 이므로 member 를 save() 하는 시점엔 insert 쿼리가 지연된다. db 반영은 안된다.
review를 save() 한 다음 커밋이 발생한다.
커밋이 발생되면 지연된 insert 쿼리가 발생하며 db에 반영이 된다.
READ_UNCOMMITTED (level 0)
- 다른 commit되지 않은 트랜잭션에 의해 변경된 데이터를 볼 수 있다
해당 트랜잭션에서 저장된 데이터를 조회한다. 이때 별도의 트랜잭션을 만들어 데이터 변경을 해보자.
mysql> start transaction;
mysql> update members set name='이기철';
다음으로 변경한 데이터를 다시 한번 조회한다.
변경이 된 데이터를 볼 수 있다.
하지만 아직 커밋이 되지 않아 데이터가 db에 반영이 안될 것이다. 마지막 스텝까지 넘어가서 조회를 해보면 반영이 안되어 조회가 된다.
이처럼 READ_UNCOMMIT 옵션은 커밋 되지 않은 데이터를 읽을 수 있게 된다. 이것을 더티 리드라고 한다.
만약 중간 로직에 데이터를 추가하거나 수정한다고 하면 더티 리드로 인해 데이터의 정확성을 해친다.
READ_COMMITTED (level 1)
- read_uncommitted의 더티 리드를 보완
- 커밋된 내용만 읽는다.
위의 테스트 한 내용에서 더티 리드를 방지하게 되어 commit된 내용만 읽게 된다. 그러므로 rollback을 하게 되면 업데이트한 내용은 반영이 되지 않는다.
하지만 조회만 하는 트랜잭션에서 다른 트랜잭션의 수정 작업으로 인해 조회 값이 달라질 수 있다. 이러한 상태를 unrepeatable read 라고 한다.
REPEATABLE_READ (level 2)
- unrepeatable read를 보안
- 반복 조회를 하는 트랜잭션에서 다른 트랜잭션의 수정 작업으로 인해 조회 값이 변경되더라도 항상 초기값 즉, 변경 되기 전의 값을 리턴해준다.
- 영속성 컨텍스트의 스냅샷과 비슷한 원리이다.
- 초기 값을 스냅샷으로 저장 하여 실제 db 데이터를 리턴해주는 것이 아니라 스냅샷의 데이터를 리턴 해 준다.
- commit 이 완료 되면 db의 반영된 데이터 값을 리턴 해 준다.
이 격리 레벨에서도 데이터의 정확성에 문제점이 있다. 한 트랜잭션 단위에서 다른 트랜잭션이 데이터를 수정하거나 추가를 하고 commit을 하면 기존 트랜잭션 내에서 Phantom Read 가 발생되고 다른 트랜잭션의 데이터 정보를 반영하게 된다.
다른 트랜잭션에서 insert를 해보자.
insert가 성공이 되면 커밋을 하여 다음 스텝을 확인해보자.
추가된 데이터가 조회 되었다.
SERIALIZABLE (level 3)
- 완벽한 읽기 모드를 제공한다.
- 키밋이 일어나지 않은 트랜잭션이 존재 하게 되면 select 하는 문장에서 lock을 걸어 놓는다.
다른 트랜잭션에서 insert 작업을 한다.
조회 스텝으로 넘어가서 확인해보자.
조회 쿼리에 lock 이 발생하는 것을 확인 할 수 있다.
이러한 결과를 보면 격리 레벨 마지막 단계는 다른 트랜잭션과는 완전히 격리하는 수준의 성격을 가진다.
참조 :
http://wiki.gurubee.net/pages/viewpage.action?pageId=21200923
https://www.baeldung.com/spring-transactional-propagation-isolation
'JPA' 카테고리의 다른 글
[Jpa] 단일 테이블 전략의 상속 관계 매핑 이슈 (0) | 2022.08.20 |
---|---|
[Jpa] 대댓글 계층구조 연관관계 메소드 이슈 (0) | 2022.08.12 |
[Jpa] Cascade (0) | 2022.08.07 |
[Jpa] Transaction Propagation (0) | 2022.08.06 |
[Jpa] Deprecated 된 getById() 대안 getReferenceById() (0) | 2022.08.02 |
[Jpa] Dirty Checking (0) | 2022.07.19 |
[Jpa] Projections (0) | 2022.07.18 |
[Spring JPA] 트랜잭션은 언체크, 체크 예외에 대해 어떻게 커밋과 롤백을 처리할까? (1) | 2022.05.18 |