티스토리 뷰
1. @Transactional
트랜잭션이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업 단위이다. 스프링 프레임워크로 개발하는 경우 보통 Business Layer에 @Transactional을 사용한다.
기본적으로 @Transactional은 CRUD 시에 사용할 수 있다. 즉, Query와 Command를 구분하지 않고 사용할 수 있다.
예를 들어, @Transactional을 사용하여 save() 메서드에서 생성된 member 객체를 데이터베이스에 저장할 수 있다. @Transactional을 사용하면 트랜잭션 AOP가 동작하면서 메서드 시작과 끝에 transactional.begin()과 transactional.commit()을 추가하기 때문이다. 단, 중간에 예외가 발생하면 transactional.rollback()이 발생한다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public Member save(MemberCreateRequest request) {
Member member = new Member();
member.updateUsername(request.getUsername());
return memberRepository.save(member);
}
}
2. JPA에서 @Transactional과 1차 캐시
ORM(Object-Relation Mapping) 기술인 JPA는 1차 캐시를 사용한다. 트랜잭션 내에서 조회된 엔티티는 1차 캐시에 저장되고, 이후에 동일 트랜잭션 내에서 재조회 시 데이터베이스로부터 조회하는 것이 아닌 1차 캐시에 저장된 엔티티를 반환한다.
@Transactional을 사용하는 경우, 트랜잭션 내에서 조회된 엔티티는 1차 캐시와 스냅샷에 저장된다.
- 1차 캐시 : 트랜잭션 내에서 조회된 엔티티를 저장하는 공간이다.
- 스냅샷 : 커밋 시점에 1차 캐시와 다른 부분을 찾아 쿼리를 발생시킨다.
(1)번 시점에 조회된 member 객체가 1차 캐시와 스냅샷에 저장되고, (2)번 시점에 1차 캐시에 저장된 member 엔티티의 데이터를 변경한다. 마지막으로 (3)번 시점에 트랜잭션 커밋을 하게 되는데 이때 1차 캐시와 스냅샷을 비교하여 다른 부분이 있다면 쿼리를 발생시킨다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final EntityManager em;
@Transactional
public Member updateUsername(MemberUpdateRequest request) {
// (1) 조회한 엔티티는 1차 캐시와 스냅샷에 저장된다.
Member member = memberRepository.findById(1L)
.orElseThrow(() -> new EntityNotFoundException());
// (2) 1차 캐시에 저장된 member 엔티티의 데이터가 변경된다.
member.updateUsername(request.getUsername());
// (3) 트랜잭션 커밋 시점에 flush가 발생한다.
return member;
}
}
따라서 (1)번 시점에 select 쿼리가 발생하고, (2)번 시점에 update 쿼리가 발생한다.
3. @Transactional(readOnly=true)을 사용하면 스냅샷이 생성되지 않는다.
@Transactional 애너테이션에 readOnly 속성을 사용하면, 해당 트랜잭션은 읽기 전용 트랜잭션이 된다. 읽기 전용 트랜잭션이란 트랜잭션 내에서 CUD(Create, Update, Delete) 작업이 일어나지 않음을 보장하는 것을 의미한다.
JPA에서는 기본적으로 1차 캐시와 스냅샷을 사용하여 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)를 수행하고, 이 과정에서 변경된 사항이 있으면 데이터베이스에 변경 쿼리를 전송한다. 하지만 읽기 전용 트랜잭션에서는 CUD 작업이 발생하지 않기 때문에 변경 감지가 실행되지 않는다.
좀 더 자세히 말하자면, 읽기 전용 트랜잭션에서는 스냅샷이 생성되지 않는다. JPA는 1차 캐시에 저장된 엔티티와 스냅샷을 비교하여 변경 사항을 감지하지만, 읽기 전용 트랜잭션에서는 스냅샷 자체가 생성되지 않으므로 변경 감지가 발생하지 않는다.
또한, 읽기 전용 트랜잭션은 트랜잭션 내에서 변경 사항이 없음을 보장하므로, 트랜잭션이 커밋될 때 flush도 일어나지 않는다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final EntityManager em;
@Transactional(readOnly = true)
public Member findUser(Long userId) {
/**
* 조회한 엔티티를 1차 캐시에 저장한다.
* 같은 트랜잭션 내에서 동일한 엔티티를 조회하는 경우 DB에서 다시 조회 쿼리를 날리지 않고, 1차 캐시에 저장된 엔티티를 반환한다.
* 읽기 전용 트랜잭션이므로 스냅샷을 생성하지 않는다.
* 스냅샨은 JPA에서 엔티티의 변경 사항을 추적하기 위해 원본 상태를 저장하는 메커니즘이다.
*/
Member member = memberRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException());
Member findMember = em.find(Member.class, userId);
boolean isContainsInCache = em.contains(findMember);
System.out.println("isContainsInCache = " + isContainsInCache);
// 1차 캐시에 저장된 엔티티의 데이터가 변경된다.
member.updateUsername("이름 수정");
/**
* 트랜잭션 커밋 시점에 flush가 발생하지 않는다.
* flush는 1차 캐시에 저장된 변경 사항을 DB에 반영하는 작업이다.
* 읽기 전용 트랜잭션이므로 CUD 작업이 발생하지 않으므로, 굳이 flush 할 필요가 없다.
*/
return member;
}
}
DB에서 엔티티를 조회하는 순간 select 쿼리가 발생하며, 조회된 엔티티는 1차 캐시에 저장된다. 이후 동일한 트랜잭션 내에서 동일한 엔티티를 조회할 때는 데이터베이스가 아닌 1차 캐시에 저장된 엔티티가 반환된다. 또한, EntityManger를 사용하여 조회한 엔티티가 1차 캐시에 저장되어 있는지 확인한 결과 true가 반환되는 것을 확인할 수 있다.
이후 updateUsername() 메서드를 통해 엔티티의 이름을 변경했지만, 트랜잭션 커밋 시점에 flush가 발생하지 않기 때문에 update 쿼리가 발생하지 않는 것을 확인할 수 있다. 이는 트랜잭션이 읽기 전용 상태에서 변경 작업이 무시되는 결과이다.
4. 결론
@Transactional(readOnly=true)를 통해 읽기 전용 트랜잭션을 사용하면, 스냅샷이 생성되지 않으므로 변경 감지가 발생하지 않으며, 이로 인해 트랜잭션 커밋 시 flush가 일어나지 않아 성능상 이점이 있다.