"JPA와 DB는 도대체 어떻게 동기화되는 걸까?"이 질문에 대한 답을 찾기 위해,
이번 글에서는 @Transactional 어노테이션을 통해 JPA의 트랜잭션이 종료되는 시점에
어떤 일이 벌어지는지 하나씩 파헤쳐 보려고 합니다.
트랜잭션 흐름
트랜잭션의 종료 직전에 벌어지는 일을 다루기 전에, 트랜잭션의 전체 흐름을 간단하게 살펴보겠습니다.
다음은 @Transactional을 사용하는 간단한 코드 예제입니다.
@Service
@RequriedArgsConstructor
class PostService {
private final PostRepository;
@Transactional
public void updatePost(Long postId, String content) {
// 게시물이 존재하는지 확인
PostEntity postEntity = postRepository.findById(postId).orElseThrow(() -> new IllegalException("Post not found"));
// 게시물을 내용 업데이트
postEntity.updateContent(content);
}
}
이 코드는 JpaRepository를 상속받은 PostRepository를 사용해 게시물의 내용을 업데이트하는 간단한 로직입니다.
(단, 파라미터의 postId의 값은 1, content 값은 "내용을 변경합니다."라고 가정합니다.)
이제 각 단계별로 살펴보겠습니다.
1. @Transactional 메서드 호출
@Transactional이 붙은 메서드가 호출되면, Spring에서 TransactionManger를 통해서 트랜잭션을 시작합니다.
트랜잭션이 시작되면, JPA에선 영속성 컨텍스트를 생성 및 초기화를 하게 됩니다. 그 다음으로 DB와 연결되는 Connection 객체가 생성되고, 데이터베이스 드라이버를 통해서 데이터베이스 트랜잭션도 시작하게 됩니다.
2. postRepository.findById(postId) 호출
Post 엔티티를 조회하기 위해서 사용한 findById 메서드를 호출하게 되면, 먼저 영속성 컨텍스트(1차 캐시)에 조회하는 엔티티가 존재하는 지 확인을 하고, 존재하지 않는 경우엔 DB에 즉시 SELECT 쿼리문을 날려 데이터를 가져오게 됩니다. 가져온 데이터는 영속성 컨텍스트(1차 캐시)에 저장이 되고, 이후 postEntity라는 변수에 담기게 됩니다.
만약 값이 존재하지 않으면 orElseThrow를 통해 예외가 발생하고, 트랜잭션이 롤백됩니다.
3. postEntity.updateContent(content) 호출
현재 PostEntity는 이전 조회 코드로 인해 영속성 컨텍스트에서 관리 대상입니다. 즉, 영속 상태입니다.
JPA가 가지는 특징 중 하나는 영속성 컨텍스트의 스냅샷을 통해 더티 체킹이 일어난다는 점입니다. 쉽게 말해, 직접 update쿼리를 날려서 변경된 데이터를 수정해주지 않아도 알아서 update쿼리를 생성해 줍니다. 이때, 쿼리는 DB에 바로 반영이 되지 않고, 영속성 컨텍스트에 기록됩니다.
"이전 (3)번 로직을 호출할때, update 쿼리를 날려서 DB의 데이터를 수정해준게 아니였나?" 라고 생각하실 수 있습니다.
JPA가 가지는 특중 또 다른 하나는 트랜잭션내에 발생하는 SELECT를 제외한 INSERT/UPDATE/DELTE 쿼리문은 저장소에 따로 저장을 해놓았다가 트랜잭션 종료 직전에 flush()가 호출되면, 한번에 쿼리를 실행한다는 특징을 갖고 있습니다.
4. 트랜잭션의 종료 직전
@Transactional내부의 모든 로직이 실행되고, 종료 시점엔 JPA는 flush()를 호출해 영속성 컨텍스트의 변경 사항을 데이터베이스에 동기화 합니다. 즉, 이 시점에 UPDATE 등의 SQL 쿼리가 실행됩니다.
현재 상황으로는 아래와 같은 쿼리문이 작성됩니다.
UPDATE post SET content = "내용을 변경합니다." WHERE id = 1;
그렇다면, flush() 메서드 호출을 통해 DB에서 쿼리를 실행하고 실행한 결과가 바로 반영되게 될까요?
사실, flush()메서드는 단순히 저장소에 저장되어 있는 쿼리를 실행하는 작업의 역할만 맡고 있습니다.
"쿼리를 실행하는게 DB에 반영이 되는거 아닌가요?"라고 생각하실 수 있습니다. 하지만, 잊지 말아야 할 사실은 현재 DB도 트랜잭션이 진행중에 있다는 사실입니다. DB내부에서도 최종적으로 flush()로 실행된 쿼리문의 결과가 반영되기 위해선 COMMIT을 해주어야 합니다.
그럼, 어떻게 DB는 쿼리를 실행해도 DB에 바로 반영하지 않고, COMMIT명령어를 기다리는 것일까요?
사실 이 시점에서 DB는 해당 쿼리를 수행하고, 변경된 사항을 임시로 저장하게 됩니다.
즉, 임시로 저장을 해놓고, COMMIT 명령이 올때까지 대기하고 있는 것입니다.
그럼 COMMIT 명령어는 누가 내리는 걸까요?
COMMIT 명령은 성공적으로 트랜잭션이 완료되면,JPA가 데이터베이스 드라이버를 통해서 COMMIT 명령어를 보내게 됩니다.
COMMIT;
COMMIT 명령어를 전달받은 DB는 이전에 flush()로 인해 임시 저장되어 대기중인 결과를 확정짓게 됩니다.
즉, DB에 반영을 하게됩니다. COMMIT을 수행한 DB는 DB내부의 Transaction을 종료하게 됩니다.
그럼, JPA 트랜잭션 내부 로직을 실행하던 중 에러가 발생하면 ROLLBACK이 어떻게 적용될까요?
COMMIT과는 조금 다른 맥락으로 동작합니다. 예외 발생으로 인해 ROLLBACK이 되는 경우엔 flush()를 호출하지 않습니다.
정리
흐름을 간단히 나타내면 다음과 같습니다. (JPA 트랜잭션 내부의 중간과정은 생략하였습니다.)
COMMIT 상황
JPA 트랜잭션 시작 → DB 트랜잭션 시작 → JPA가 변경사항을 DB로 전달 (flush() 호출) → DB가 쿼리 실행 후 임시 저장소에 기록 → COMMIT 명령 시 변경사항 확정 ✅ → DB 트랜잭션 종료 → JPA 트랜잭션 종료
ROLLBACK 상황
JPA 트랜잭션 시작 → DB 트랜잭션 시작 → ROLLBACK 발생 → DB 트랜잭션 종료 → JPA 트랜잭션 종료
'JPA' 카테고리의 다른 글
동시성 문제 해결방법(Update Query편) (0) | 2025.01.29 |
---|---|
save()와 SELECT의 관계 (0) | 2025.01.26 |
🙆🏻♂️ 너, Fetch Join 만능이야? (0) | 2024.09.24 |
🙆🏻♂️ 너, Fetch Join 좀 알고 싶다? (0) | 2024.09.22 |
@DataJapTest를 사용하는 이유 (0) | 2024.07.17 |