Backend
home
🔎

@Transactional 어노테이션 + dirty checking 이해하기

생성 일시
2025/01/22 06:16
태그
SpringBoot
게시일
2025/01/22
최종 편집 일시
2025/02/09 13:26

@Transactional

스프링 프레임워크에서 특정 메서드 또는 클래스에서 수행되는 ‘트랜잭션’과 관련하여 관리를 위해 사용되는 어노테이션이다.
메서드 또는 클래스에 해당 어노테이션을 선언하면 코드 실행 중 트랜잭션 오류 발생 시 트랜잭션이 ‘롤백’ 되고 변경사항이 모두 취소된다.
여러 개의 데이터베이스 작업을 하나의 트랜잭션으로 처리해야 할 때 유용하다.
한 메소드에서 여러 개의 업데이트나 삽입 작업을 수행하고, 이 작업들이 모두 성공해야 데이터베이스에 반영하고 싶은 경우 @Transactional을 사용할 수 있다.

트랜잭션

데이터베이스 작업을 ‘원자적으로 처리’ 하기 위한 메커니즘
여러 개의 데이터베이스 작업을 하나의 논리적 단위로 묶어서 실행하고 모든 작업이 성공적으로 완료되면 ‘커밋’하고 실패할 경우 ‘롤백’하는 기능을 제공한다.
원자적으로 처리한다?
트랜잭션 내의 모든 데이터베이스 작업이 모두 성공하거나 모두 실패할 때까지 어떤 작업도 부분적으로 적용되지 않는 것을 의미한다.
다시 말해, 트랜잭션은 모든 작업이 완전히 수행되기 전까지 어떤 변화도 발생하지 않도록 보장한다.

@Transaction(readOnly = true)를 왜 붙이는가?

우리는 스프링의 AOP를 통해 @Transactional 어노테이션만으로 손쉽게 Service Layer에서 트랜잭션을 걸 수 있다.
일반적으로, 조회용 메서드에 대해서는 @Transactional(readOnly = true) 를 설정함으로써 성능상 이점을 얻을 수 있다고 한다.
JPA 관련
이는 JPA의 영속성 컨텍스트(Persistence Context)가 수행하는 변경 감지(Dirty Checking)와 관련이 있다. 영속성 컨텍스트는 Entity 조회 시 초기 상태에 대한 Snapshot을 저장한다. 트랜잭션이 Commit 될 때, 초기 상태의 정보를 가지는 Snapshot과 Entity의 상태를 비교하여 변경된 내용에 대해 update query를 생성해 쓰기 지연 저장소에 저장한다.
그 후, 일괄적으로 쓰기 지연 저장소에 저장되어 있는 SQL query를 flush 하고 데이터베이스의 트랜잭션을 Commit 함으로써 우리가 update와 같은 메서드를 사용하지 않고도 Entity의 수정이 이루어진다. 이를 변경 감지(Dirty Checking) 라고 한다.
이 때, readOnly = true를 설정하게 되면 스프링 프레임워크는 JPA의 세션 플러시 모드를 MANUAL로 설정한다.
MANUAL 모드는 트랜잭션 내에서 사용자가 수동으로 flush를 호출하지 않으면 flush가 자동으로 수행되지 않는 모드이다.
즉, 트랜잭션 내에서 강제로 flush()를 호출하지 않는 한, 수정 내역에 대해 DB에 적용되지 않는다.
이로 인해 트랜잭션 Commit 시 영속성 컨텍스트가 자동으로 flush 되지 않으므로 조회용으로 가져온 Entity의 예상치 못한 수정을 방지할 수 있다.
또한, readOnly = true를 설정하게 되면 JPA는 해당 트랜잭션 내에서 조회하는 Entity는 조회용임을 인식하고 변경 감지를 위한 Snapshot을 따로 보관하지 않으므로 메모리가 절약되는 성능상 이점 역시 존재한다.
데이터 무결성 보장
해당 속성을 true로 설정함으로써 트랜잭션이 데이터베이스에 대한 변경을 수행하지 않도록 함으로써 데이터 무결성을 보장하는데 도움이 된다. 복잡한 비즈니스 로직을 가진 애플리케이션에서 유용하며, 의도치 않은 데이터 변경을 방지할 수 있다.
코드의 가독성
위에서 아래로 코드를 읽게 되었을 때 가장 먼저 눈에 띄는 것은 어노테이션이다.이 때, readOnly = true라는 옵션이 붙어있다면 직관적으로 '이 메서드는 조회용 메서드구나' 라고 알 수 있다. 즉, 성능상의 이점 뿐 아니라 readOnly = true를 붙임으로써 직관적으로 해당 메서드가 조회용 메서드임을 알 수 있어 가독성 측면에서도 이점을 가진다.
// 예시를 위해 간단하게 작성했다. 실제 구현 시에는 DTO로 Entity를 변환하는 것이 조타 // 1 @Transactional public Member getMember(Long memberId) { Optional<Member> member = memberRepository.findById(memberId); ... .. .. return member; } // 2 @Transactional(readOnly = true) public Member getMember(Long memberId) { Optional<Member> member = memberRepository.findById(memberId); ... .. .. return member; }
Java
복사

@Transactional 사용 시 save가 필요 없는 이유 (Dirty Checking)

dirty checking의 단점
JPA에서 업데이트 방식은 dirty checking 으로, 모든 행에 대해서 하나씩 update 쿼리를 날려서 수정한다. 그러므로 성능에 악영향을 줄 수 있다.
Entity에 @DynamicUpdate 어노테이션을 사용하면 수정된 데이터만 동적으로 update query를 날릴 수 있다.