'스프링 부트와 AWS로 혼자 구현하는 웹 서비스'를 공부하던 중 궁금한 점이 생겨 이를 정리하고자 한다.
@Transactional(readOnly = true)
public List<PostsListReponseDto> findAllDesc(){
return postsRepository.findAllDesc().stream()// postsRepository의 결과로 넘어온 Posts의 stream을
.map(PostsListReponseDto::new) // map을 통해 PostsListResponseDto로 변환하고
.collect(Collectors.toList()); // 이를 List로 반환하는 메소드
}
위의 코드를 보면, DB에서 posts들을 Select하는 메서드이다. 하지만 @Transactional(readOnly = true)을 사용하고 있다. '애초에 트랜잭션을 안 쓰면 readOnly로 설정할 필요가 없지 않나?', '수정과 삭제를 하지 않는데 왜 트랜잭션을 사용하는 것인가?'라는 의문이 생겨 이에 대한 내용을 찾아보았다.
먼저 트랜잭션에 대해 알아보자.
트랜잭션은 DB의 상태를 변경하는 작업 또는 한번에 수행되어야 하는 연산들을 의미한다.
트랜잭션을 사용하기 위해서 클래스나 메서드에 @Transactional 어노테이션을 붙여주면 된다.
그렇게 되면 해당 범위는 트랜잭션이 보장되게 되고, 영속성 컨텍스트가 생성된다.
Transaction이 보장되게 되면 연산 수행 중 오류가 발생하게 되면 자동적으로 롤백을 해준다.
그렇기 때문에 @Transactional 어노테이션은 Create, Update, Delete처럼 DB내의 데이터를 조작해야하는 경우에 오류를 대비하여 사용되어야 한다.
크게 3가지 이유로 Select에도 @Transactional을 사용한다.
1. 속도의 최적화
트랜잭션에 readOnly 옵션을 주면 강제로 플러시(영속성 컨텍스트의 변경 내용을 DB에 반영)를 호출하지 않는 한 플러시가 일어나지 않는다.
그렇기에 아무리 영속성 컨텍스트에 있는 엔티티를 수정한다 해도 플러시를 실행하지 않으니 엔티티의 등록, 수정, 삭제는 당연히 동작하지 않는다.
플러시 할 때 일어나는 스냅샷 비교와 같은 무거운 로직들을 수행하지 않으므로 성능이 향상된다.
※ 주의할 점이 있다. H2 데이터베이스에서는 위의 옵션이 적용되지 않는다. 하지만 mySql은 적용되기에 테스트는 H2를, 배포는 mySql을 사용하면 되겠다.
2. 지연 로딩 가능
@Service
@Transactional
@RequiredArgsConstructor
public class TestService {
private final PostRepository postRepository;
public void test() {
List<Post> posts = postRepository.findAll();
for (Post post : posts) {
System.out.println("post 조회 = " + post.toString()); // 1번
System.out.println("member 아이디 조회 = " + post.getMember().getLoginId()); // 2번
}
}
}
위의 코드를 보면 Post와 Member는 N:1 관계를 맺고 있다. Member 1명이 여러개의 Post를 등록할 수 있는 것이다.
이런 상황에서 회원은 지연 로딩(Lazy Loading) 방식으로 불러오게 된다.
1번 코드인 Post를 조회한 후에 2번 코드인 게시글과 연관된 Member의 Id를 조회하는 것이다.
위 코드처럼 Service 계층에 @Transactional을 적었을 때는 Member의 Id가 출력이 잘 되었다.
하지만 Service 계층에 @Transactional을 적지 않았을 때는 Post의 조회는 잘 되지만 Member의 Id가 출력이 되지 않고, LazyInitializationException이 발생한다. 준영속 상태에서 지연 로딩을 시도했기 때문이다.
준영속 상태에서 Dirty Checking과 Lazy Loading은 동작하지 않는다. 그렇기 때문에 위의 상황처럼 지연 로딩이 필요하다면 @Transactional을 명시하여 영속성 상태로 변경해야한다.
3. 쿼리 결과의 정합성 보장
트랜잭션 내에서 발생하는 Select와 밖에서 발생하는 Select는 Read Commited(커밋된 읽기) 격리 수준에서는 커밋이 완료된 데이터만 조회 가능하기에 차이가 없다.
하지만 Repeatable Read(반복 가능한 읽기) 격리 수준에서는 차이가 발생한다.
Repeatable Read 격리 수준에서는 트랜잭션이 시작되기 전에 커밋된 내용에 관해서만 조회할 수 있기 때문에 트랜잭션 동안에 아무리 다른 트랜잭션에서 데이터를 변경하고 커밋하더라도 같은 결과를 조회하게 된다.
그래서 트랜잭션 내에서의 Select는 항상 같은 결과를 보장해야 한다는 Repeatable Read 정합성을 보장 받을 수 있다.
반면에 트랜잭션 밖의 Select의 경우에는 같은 메서드 안에 여러 개의 Select가 있을 때, 다른 트랜잭션이 데이터를 변경하게 되면 Select마다 다른 결과가 나타날 수 있다. 정합성을 보장받지 못하는 것이다.
참고
https://khdscor.tistory.com/m/20
https://ssdragon.tistory.com/116
https://hojak99.tistory.com/602
'개발 > 스프링 개념' 카테고리의 다른 글
[Spring] Spring JPA 복합키 (0) | 2023.02.10 |
---|---|
[Spring] Session이란? (0) | 2023.01.25 |
[Spring] Spring 웹 계층 구조 (0) | 2023.01.20 |
[Spring] 의존성 주입이란? (0) | 2023.01.13 |
[Spring] 영속성 컨텍스트와 @Transactional 어노테이션 (0) | 2022.11.19 |