@Transactional, 영속성 컨텍스트
2024. 3. 23. 19:37ㆍBackend 취업준비/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/
'Backend 취업준비 > Spring' 카테고리의 다른 글
AOP를 활용한 api 중복 요청 방지 (따닥 방지) (0) | 2024.07.22 |
---|---|
@WebMvcTest를 사용한 Controller 단위 테스트 (1) | 2024.07.15 |
엔티티와 @RequestBody를 쓰는 DTO에서 기본 생성자, getter/setter의 필요성 (리플랙션) (0) | 2024.03.22 |
스프링 시큐리티 (0) | 2024.03.20 |
Servlet Filter, Spring Intercepter (0) | 2024.03.19 |