@Transactional, 영속성 컨텍스트

2024. 3. 23. 19:37Backend 취업준비/Spring

@Transactional

  • 데이터 접근 기술마다 데이터 베이스 접근 방법이 다름

  • 하지만 실제로 하는 일은 비슷함 => 추상화 가능
  • @Transactional은 Spring AOP로 구현되어 있다.
  • 빈 생성시 @Transactional애노테이션이 붙어있다면 프록시 객체가 빈으로 등록된다.

=> @Transactional를 Service 클래스에 선언하면 해당 클래스에서 비지니스 로직을 수행할 때 Service의 프록시 객체가 빈으로 등록되어 수행

 

스프링에서 @Transactional 을 이용하여 트랜잭션 처리를 하는 방법

  • DB와 관련된, 트랜잭션이 필요한 서비스 클래스 혹은 메서드에 @Transactional 어노테이션을 달아주어야 한다
  • 클래스, 메서드 모두에 @Transactional 어노테이션을 붙이면 메서드 레벨의 @Transactional 선언이 우선 적용된다

 

  • org.springframework.transaction.annotation.Transactional 를 사용하자.
  • 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋. 만약 런타임 예외가 발생하면 롤백 (컴파일 체크 예외는 별도의 설정을 하지 않는다면 롤백하지 않는다. => 비지니스 상황에서 문제가 되어서 꼭 확인이 필요한 경우를 고려할 수 있도록 만들어짐.)
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다
  • 주로 비지니스 로직이 담기는 서비스 계층에서 활용한다

 

테스트 환경에서의 @Transactional 동작

 

  • @Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테
    스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다
  • org.springframework.test.context.transaction.TestTransaction 클래스를 이용해 알아보자.

 

TestTransaction

  • TestTransaction 클래스는 @Transactional 이 적용된 테스트 메서드를 감싸고 있는 트랜잭션과 상호 작용할 수 있는 기능을 가지는 유틸리티 클래스이다.
  • TestTransaction.isActive() => 현재 동작 중인 트랜잭션이 있는지 확인
  • TestTransaction.isFlaggedForRollback() => 롤백 옵션이 적용되어 있는지 확인할 수 있다. 디폴트로 롤백 옵션이 적용
  • 롤백 옵션을 바꾸고 싶다면
    • TestTransaction.flagForCommit(); 
    • TestTransaction.flagForRollback();

 

주의할점

 

롤백되지 않는 경우

WebEnvironment의 RANDOM_PORT, DEFINED_PORT를 사용하면 실제 테스트 서버는 별도의 스레드에서 테스트를 수행하기 때문에 트랜잭션이 롤백되지 않는다.

 

 

id는 롤백되지 않는다.

  • 주로 insert 작업을 할 때 id(식별자)가 자동으로 증가하도록 Auto Increment 옵션을 적용하곤 하는데, 트랜잭션에 포함된 insert 작업으로 인해 증가한 id는 트랜잭션이 롤백되어도 다시 감소하지 않는다.
  • Auto Increment 옵션은 트랜잭션의 범위 밖에서 동작하기 때문이다.
  • 트랜잭션 범위 안에서 동작하면 같이 롤백되고 id도 순서대로 부여할 수 있기 때문에 편할 것 같은데 왜 그럴까? 바로 동시성 때문이다.
  • 여러 사람이 동시에 한 사이트에 회원가입을 하는 상황을 생각해보자.
  • 각각 insert 문이 포함된 트랜잭션이 진행되는데, 중복된 아이디 혹은 올바르지 않은 양식의 아이디 입력 등 여러 요인으로 인해 트랜잭션이 실패할 수도 있고, 성공할 수도 있다.
  • 각 트랜잭션이 다른 사람의 회원 가입 트랜잭션 성공 여부를 기다렸다가 id를 부여받기엔 얼마나 기다려야 될 지 모르는 일이다.
  • 따라서 Auto Increment 는 트랜잭션과 별개로 동작

 

영속성 컨텍스트

 

  • 해당 메서드에서는 repository에서 데이터를 가져와  Article 객체에 넣고, 그 객체를 수정한 후, 다시 repository에 저장하지 않고 update 메서드를 끝냈다
  • 그럼에도 DB에 수정한 값이 잘 반영 된다
  • 이는 영속성 컨텍스트의 개념 때문

 

영속성 컨텍스트란?

  • 엔티티를 저장하고 관리하는 저장소
  • 엔티티 매니저가 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
  • Spring Data JPA 에서 사용하는 save, findById 등의 안을 살펴보면, 엔티티 매니저가 영속성 컨텍스트에 저장하고 조회하는 내용이 구현되어 있다. (Spring Data JPA  에서는 영속성을 자동으로 관리해 준다)
  • 영속성 컨텍스트에 저장된 상태를 영속 상태, 저장되었다가 분리된 상태를 준영속 상태라고 한다.

 

영속성 컨텍스트의 특징

  • 식별자 값
    • 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 기본키(PK)와 매핑된 값)으로 구분한다.
    • 영속 상태가 되기 위해서는 반드시 식별자가 존재해야 한다.
  • 데이터베이스 저장
    • 플러시(flush) : 트랜잭션을 커밋하는 순간에 영속성 컨텍스트에 새로 저장된 엔티티가 데이터베이스에 반영된다.

 

엔티티의 생명 주기

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속(managed) : 영속성 컨텍스트에 관리되는 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(remove) : 삭제된 상태

 

비영속

객체를 생성한 상태

Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

 

영속

EntityManager em = EntityManagerFactory.createEntityManager();
em.getTransaction().begin();

Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 객체를 영속성 컨텍스트에 저장(영속)
em.persist(member);

 

준영속

// member 엔티티를 영속성 컨텍스트에서 분리(준영속)
em.detach(member);

 

삭제

// 객체를 삭제한 상태(삭제)
em.remove(member);

 

 

 

영속성 컨텍스트의 기능

 

1차 캐시

  • 영속성 컨텍스트는 내부에 1차 캐시를 갖고 있다. 1차 캐시를 영속성 컨텍스트 라고 봐도 무리가 없다.
  • Map형태, key => id, value => entity 
  • em.persist(member) → member 엔티티가 영속성 컨텍스트 안에 있는 1차 캐시에 저장된다.
  • em.find() → 1차 캐시에 찾는 엔티티가 있으면 DB에 SQL을 수행하지 않아도, 1차 캐시를 통해 엔티티를 조회할 수 있다.

  • 만약 1차 캐시에 찾는 엔티티가 없으면, DB를 조회해서 엔티티를 생성하고 1차 캐시에 저장 후, 영속 상태의 엔티티를 반환.

  • 보통 트랜잭션 단위로 엔티티 매니저가 생성되고, 트랜잭션이 끝날 때 종료된다. => 1차 캐시도 다 날아감. 트랜잭션을 사용하는 메서드 내에서 사용할 때 아주 조금의 이득이 있을 수 있겠다. 아주 복잡한 비지니스 로직에서는 이점이 있을수도 (애플리케이션 전체에서 공유하는 캐쉬는 JPA나 Hibernate 에서는 2차 캐쉬라고 따로 존재)

 

 

동일성 보장

  • 하나의 트랜잭션 안에 존재하는 Id가 같은 엔티티는 동일성이 보장된다. → 1차 캐시에 있는 같은 엔티티 인스턴스를 반환 (마치 컬렉션에서 꺼내서 같은 참조값을 가진 것 같은 효)
  • 동일성 비교 : ==
  • 동등성 비교 : equals

트랜잭션을 지원하는 쓰기 지연(transactional wirte-behind)

  • 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 SQL을 모아두고, 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스로 보낸다.

변경 감지(dirty checking)

  • 1차 캐시에 들어있는 엔티티가 변경되면, 그 변경 내용을 감지해서 트랜잭션 커밋 시점에 변경된 내용을 데이터베이스에 반영.
  • 엔티티가 영속성 컨텍스트에 저장되면, 최초 상태를 저장해둔다. → 스냅샷
  • 플러시(flush) 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾아 DB에 반영한다.
  • 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용.

지연 로딩(lazy loading)

  • 연관 관계 매핑이 되어 있는 엔티티를 조회할 때, 조회 시점에는 연관된 엔티티를 실제 객체 대신 프록시 객체로 로딩한다.
    • 프록시 객체 : 실제 엔티티 객체 대신 사용되는 객체. 실제 객체의 참조 값을 지닌다.
    • 기본적으로는 LAZY 속성이 아니어서 해당 다대일 관계를 매핑할 때@ManyToOne(fetch = FetchType.LAZY)어노테이션을 사용해 줘야 한다
  • 연관 매핑된 엔티티의 해당 객체를 실제 사용할 때 비로소 영속성 컨텍스트를 통해 데이터를 불러온다. → 데이터가 진짜 필요할 때 쿼리를 보낸다. 

 

결론적으로 1차 캐시와 변경 감지 기능 덕분에 repository에서 가져온 엔티티의 데이터를 수정하면, 트랜잭션이 종료될 때(해당 메서드가 끝날 때) 변경된 엔티티의 값이 그대로 데이터베이스에 반영

 

 

출처

https://www.youtube.com/watch?v=cc4M-GS9DoY

https://tecoble.techcourse.co.kr/post/2021-05-25-transactional/

https://velog.io/@kimdy0915/Transactional-%EC%82%AC%EC%9A%A9%EA%B3%BC-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8persistence-context

https://ittrue.tistory.com/254