개발을 하면 Service 단에 일단 @Transactional 을 붙이고 시작하는 경향이 있다.
이번에는 이 @Transactional에 대해서 알아보자.
▶ 트랜잭션이란?
@Transactional 어노테이션에 대해 알아보기 전에 트랜잭션이 무엇일까?
트랜잭션은 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위 이다.
여기서 데이터베이스의 상태를 변화시킨다는 것
= SQL을 이용하여 DB에 접근하는 것을 의미한다.(SELECT, INSERT, DELETE, UPDATE)
작업 단위에는 이러한 SQL 명령문들이 여러개 존재할 수 있다.
예를 들어서 일기 웹서비스를 예로 들어보자.
- 1. 일기를 쓰고 작성하기 버튼을 누른다.
- 2. 일기 작성이 처리되고 자동으로 일기 목록으로 돌아온다.
- 3. 일기 목록에서 직전에 쓴 일기가 포함된, 업데이트 된 일기 목록들을 볼 수 있다.
이것을 DB 작업으로 옮기면
- 1. 일기 작성 -> INSERT
- 2. 일기 목록 불러오기 -> SELECT
이렇게 된다.
이 기능에서의 트랜잭션은 INSERT문과 SELECT를 합친것이다.
즉, 여러 작업들이 모여서 하나의 트랜잭션이 되는것이다!
▷ 트랜잭션의 특성
트랜잭션은 아래 4가지의 특성을 만족해야한다.
1. 원자성(Atomicity)
- 트랜잭션 내의 모든 작업이 완벽히 수행되거나, 전혀 수행되지 않아야한다.
- 만약 중간에 실패하면, 모든 작업이 원래 상태로 돌아가야한다.
2. 일관성(Consistency)
- 트랜잭션이 완료된 후 DB가 일관성이 있는 상태를 유지해야 한다.
- 논리적 오류나 불일치 없이 무결성을 유지해야한다.
- 여기서 "일관성" 이라는 것은 "논리적으로 맞는 상태"를 유지해야 하는것을 의미한다.
- 송금으로 예를 들어보면 A가 B에게 100만원을 보낸다고 해보자. 그러면 A의 계좌에서는 100만 원이 빠져야 하고 B의 계좌에는 100만 원이 추가 되어야한다. 이것이 제대로 일어나게 보장해주는 것이 일관성이다.
3. 격리성(Isolation)
- 각각의 트랜잭션은 독립적으로 수행되어야하며, 다른 트랜잭션의 중간 상태를 볼수 없어야한다.
- 동시에 수행되는 트랜잭션들은 서로 간섭하지 않아야한다. = 동시에 여러 트랜잭션이 실행되더라도 서로의 결과에 영향을 미치지 않도록 해야한다.
4. 지속성(Durability)
- 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 반영되어야 한다.
- 시스템 장애가 발생하더라도 완료된 트랜잭션의 결과는 보존되어야 한다.
▷ 트랜잭션의 상태
위에서 말한걸 다시 정리해보자.
트랜잭션 = DB의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위
트랜잭션의 과정을 도식화해보면 이렇게 할 수 있다.
- 활성
- 트랜잭션이 정상적으로 실행중인 상태
- 작업 성공
- 부분 완료
- 트랜잭션의 마지막까지 실행되었고, Commit 연산 실행 직전 상태
- 완료
- 트랜잭션이 성공하여 Commit 연산을 실행한 후의 상태
- 부분 완료
- 작업 실패
- 실패
- 트랜잭션 실행에 오류가 발생하여 중단된 상태
- 철회
- 트랜잭션이 비정상적으로 종료되어 Rollback 연산을 수행한 상태
- 실패
▶ @Transactional 어노테이션
@Transactional 은 해당 메서드나 클래스가 "트랜잭션 관리 하에 실행되도록 지정"하는 스프링 어노테이션이다.
이를 통해 트랜잭션의 시작과 끝을 자동으로 관리할 수 있다!
이를 선언적 트랜잭션이라고도 하는데, 직접 객체를 만들 필요 없이 선언만으로도 관리를 용이하게 해주기 때문이다.
그렇다면 @Transcational 이 붙은 메서드를 호출 할 경우, 우리의 코드에는 어떤 일이 벌어지는 걸까??
▷ @Transactional 의 작동
1. 프록시 생성
- @Transactional 어노테이션이 적용된 클래스나 메서드는 프록시(proxy) 객체에 의해 감싸진다.
- 이 프록시는 실제 메서드를 호출하기 전,후에 트랜잭션 관련 로직을 추가로 수행한다.
2. 프록시 메서드 호출 전
- 프록시 객체는 트랜잭션이 시작되어야 하는 메서드를 호출하기 전에 트랜잭션 매니저를 통해 새로운 트랜잭션을 시작하거나 기존 트랜잭션을 가져온다.
3. 트랜잭션 시작
- 트랜잭션 매니저는 데이터베이스 연결을 설정하고, 필요한 경우 새로운 트랜잭션을 시작한다.
- 트랜잭션 속성(propagation, isolation, timeout, readOnly, rollbaskFor, noRollbackFor)을 설정한다.
4. 실제 메서드 호출
- 프록시 객체는 실제 비즈니스 로직을 포함한 메서드를 호출한다.
- 비즈니스 로직은 트랜잭션 내에서 실행된다.
5. 메서드 실행 후 처리
- 메서드가 성공적으로 완료되면 프록시 객체는 트랜잭션을 커밋한다.
- 메서드 실행 중 예외가 발생하면 프록시 객체는 트랜잭션을 롤백한다.
6. 트랜잭션 종료
- 트랜잭션 매니저는 트랜잭션을 종료하고, 데이터베이스 연결을 해제한다.
위와 같은 방식으로 동작한다.
@Transcational이 클래스 또는 메서드에 붙을 때, Spring은 해당 메서드에 대한 프록시를 만든다.
(프록시는 실제 객체에 대한 접근을 제어하거나 추가적인 기능을 제공하기 위해 그 객체를 감싸는 대리 객체임)
프록시는 원래 객체와 동일한 인터페이스를 구현하거나 상속받아서 원래 객체처럼 사용할 수 있도록 한다.
트랜잭션의 경우, 트랜잭션의 시작과 연산 종료시에 커밋 과정이 필요하다.
그러므로, 프록시를 생성하여 해당 메서드의 앞과 뒤에 트랜잭션의 시작과 끝을 추가하는 것이다!!
또한, 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
이게 무슨 말이냐??
서비스 클래스에서 @Transactional을 사용할 경우,
해당 코드 내의 메서드를 호출할 때 영속성 컨텍스트가 생긴다는것이다.
영속성 컨텍스트는 트랜잭션 프록시 객체가 트랜잭션을 시작할 때 생성되고,
메서드가 종료되어 트랜잭션 프록시 객체가 트랜잭션을 커밋할 경우
영속성 컨텍스트가 flush 되면서 해당 내용이 반영된다.
이후 영속성 컨텍스트 또한 종료된다.
이러한 방식으로 영속성 컨텍스트를 관리해주기 때문에, @Transcational을 사용할경우
트랜잭션의 원칙을 정확하게 지킬 수 있다.
또한 아래의 원칙도 유의해야 한다.
- 만약 같은 트랜잭션 내에서 여러 EntityManager를 쓰더라도, 이는 같은 영속성 컨텍스트를 사용한다.
- 같은 EntityManager를 쓰더라도, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
▶ EntityManager와 영속성 컨텍스트
EntityManager가 뭐길래 뭘 같이 쓰고 말고 어쩌고 저쩌고 유의하라는거야??
엔티티 매니저와 영속성 컨텍스트는 JPA의 핵심 개념이다.
두 개념은 아주아주 밀접하게 연관되어있다. 이 둘의 관계와 각각의 역할에 대해서 알아보자!
▷ 엔티티 매니저(EntityManager)
엔티티 매니저는
JPA에서 DB와 상호작용하고, 엔티티의 생명주기를 관리하는 인터페이스이다.
엔티티 매니저는
DB작업(CURD 연산, 쿼리 실행 등)을 수행하며, 이를 위해 영속성 컨텍스트를 사용한다.
엔티티 매니저는 다음과 같은 주요 기능을 제공한다.
1. 엔티티 저장 : 새로운 엔티티를 영속성 컨텍스트에 추가
2. 엔티티 조회 : 영속성 컨텍스트 또는 DB에서 엔티티를 조회
3. 엔티티 병합 : 준영속 상태의 엔티티를 영속 상태로 병합
4. 엔티티 삭제 : 영속성 컨텍스트와 DB에서 엔티티를 삭제
5. 트랜잭션 관리 : 트랜잭션을 시작하고 종료
▷ 영속성 컨텍스트(Persistence Context)
영속성 컨텍스트는
엔티티 매니저에 의해 관리되는 일종의 1차 캐시로, 엔티티 인스턴스를 영속 상태로 관리한다.
영속성 컨텍스트는
엔티티의 생명주기를 관리하며, 트랜잭션 내에서 엔티티의 상태 변화를 추적하고 DB에 반영한다.
영속성 컨텍스트는 아래와 같은 특징을 가지고 있다.
1. 1차 캐시
- 동일한 트랜잭션 내에서 동일한 엔티티를 여러번 조회할 때 DB를 재조회하지 않고 캐시에 저장된 엔티티를 반환함
2. 더티 체킹
- 영속성 컨텍스트에서 관리되는 엔티티의 변경 사항을 자동으로 감지하고 트랜잭션이 커밋될 때 DB에 반영함
3. 엔티티 생명주기 관리
- 엔티티의 상태를 관리함.
Entity의 생명 주기
1. 비영속
- 영속성 컨텍스트와 무관한 새로운 데이터
- ex) 클라이언트에서 넘어 온 데이터를 영속화 하기 전의 상태
2. 영속
- 영속성 컨텍스트에 주입 또는 관리되고 있는 경우
- ex) 클라이언트에서 넘어 온 데이터를 영속화 함
3. 준영속
- 영속화 되었다가 분리된 경우(또는 식별자는 있지만 영속성 컨텍스트에 없는 객체)
- ex) 데이터를 수정할 때, 클라이언트 측에서 넘어온 id를 갖고 있는 객체
4. 삭제
- 영속성 컨텍스트에서 아예 삭제된 데이터
4. 지연 로딩(Lazy Loading)
- 트랜잭션이 끝날 때까지 DB에 변경사항을 모아서 한번에 반영함.
▷ 엔티티 매니저와 영속성 컨텍스트의 관계
엔티티 매니저 = 영속성 컨텍스트를 생성하고 관리하는 주체
즉, 영속성 컨텍스트는 엔티티 매니저의 내부에서 동작하며, 엔티티 매니저를 통해 접근 가능함.
엔티티 매니저가 하는 대부분의 작업은 실제로 영속성 컨텍스트에서 일어난다.
- 엔티티 매니저는 영속성 컨텍스트를 생성하고 관리한다.
- 엔티티 매니저는 영속성 컨텍스트를 생성한다.
- 이는 일반적으로 트랜잭션이 시작될 때 생성, 종료될 때 삭제된다.
- 엔티티 매니저를 통한 엔티티 작업은 영속성 컨텍스트에서 처리한다.
- 엔티티 매니저는 영속성 컨텍스트를 통해 엔티티의 상태를 관리한다.
- ex) em.persist(entity); → 엔티티를 영속성 컨텍스트에 저장
- 엔티티 매니저는 영속성 컨텍스트를 통해 1차 캐시 기능을 제공한다.
- 엔티티 매니저는 동일한 트랜잭션 내에서 동일한 엔티티를 여러번 조회할 때 영속성 컨텍스트의 1차 캐시를 사용함.
▶ 더티 체킹(Dirty Checking)
더티체킹은 JPA의 중요한 기능 중 하나이다.
영속성 컨텍스트에서 관리되는 엔티티의 변경 사항을 자동으로 감지하여 DB에 반영하는 메커니즘이다!
더티체킹은 트랜잭션이 커밋되는 시점에 발생한다.
즉, 트랜잭션이 끝날 때 엔티티의 상태가 변경되었는지 확인하고,
변경사항이 있을 경우 자동으로 UPDATE 쿼리를 생성하여 DB에 반영한다.
▷ 더티 체킹의 작동 방식
더티 체킹은 다음과 같은 순서로 작동한다.
영속성 컨텍스트에 엔티티 저장 -> 엔티티의 상태 변경 -> 트랜잭션 커밋 시점에 더티 체킹 수행
1. 영속성 컨텍스트에 엔티티 저장
- 엔티티 매니저를 통해 엔티티를 조회하거나 저장하면 해당 객체는 영속성 컨텍스트에 의해 관리되는 영속 객체 상태가 된다.
- ex) User user = entityManager.find(User.class, userId);
- -> user 객체는 엔티티 매니저를 통해 관리되는 영속성 객체이다(repository를 통해서 조회하거나 저장해도 마찬가지).
2. 엔티티의 상태 변경
- 영속 상태의 엔티티 객체를 UPDATE하면, 이 변경 사항은 영속성 컨텍스트에 의해 자동으로 추적된다.
- ex) user.updateName(newName);
3. 트랜잭션 커밋 시점에 더티 체킹 수행
- 트랜잭션 커밋시에 JPA는 영속성 컨텍스트에서 관리되는 모든 엔티티의 상태를 검사한다.
- 이 과정에서 변경된 엔티티가 있다면, 해당 엔티티에 대한 UPDATE 쿼리가 실행된다.
▷ UPDATE 후 save() 메서드를 호출하지 않는 이유
그렇다! 위 내용을 정리해보면
JPA에서는 엔티티의 상태 변화를 감지하고
자동으로 DB에 반영한다는 것을 알 수 있다!
그러므로 save() 메서드를 명시적으로 호출할 필요가 없다.
그 이유는 아래와 같다.
1. 트랜잭션 종료 시 변경된 영속성 객체를 자동으로 감지(더티체킹)하여 DB에 commit함.
2. 불필요한 조회 방지
- 이미 영속성 컨텍스트에 의해 관리되는 Entity의 경우 추가적인 조회 없이 변경 사항을 자동으로 반영할 수 있다.
- 그런데 save() 메서드를 호출하면 JPA는 해당 Entity를 저장하기 위해서
- 해당 Entity가 새로운 Entity인지, 아니면 기존의 Entity인지
- 즉, INSERT 쿼리를 날릴지 아니면 UPDATE 쿼리를 날릴지 확인하기 위해서 추가적인 데이터베이스 조회를 수행한다.
- 즉, 불필요한 조회가 추가적으로 발생한다.
따라서, 트랜잭션으로 묶여있는 클래스나 메서드에서 조회하거나 저장한 Entity은
엔티티매니저의 영속성 컨텍스트에 의해 관리되는 Entity이므로
객체에 변경이 감지되면 더티체킹으로 UPDATE 쿼리가 실행되므로 명시적으로 save()를 호출 할 필요가 없는것이다!