JPA

[Jpa] Transaction Scope and Isolation

kkkkkkkkkkkk 2022. 8. 5. 17:57

트랜잭션은 어떤 작업을 하는데의 단위 묶음이라 한다. 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

dnjscksdn98

https://www.baeldung.com/spring-transactional-propagation-isolation