Spring/JPA

@Transactional은 조회만 할 때 있어야할까?

묠니르묘묘 2022. 8. 10. 00:24
스프링 부트 프로젝트를 진행하면서 트랜잭션 어노테이션을 적지 않은곳에서 조회가 잘되고 있었다.
하지만 어느 곳에서는 읽기 전용 트랜잭션인 `@Transactional(readOnly = true)`를 적어 성능 최적화를 하고 있었다.
여기서 든 의문이 트랜잭션이 애초에 없었으면 읽기 전용으로 안만들어도 되는 것이 아닌가? 였다.

📚 그렇다면 한번 살펴보자

SpringBoot JPA를 사용하고 있고, 단순 조회하는 서비스(Service) 계층이 있다고 가정한다.

 

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용하고 있다.

이것은 말 그대로 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 의미이다.

  • 트랜잭션 시작 → 영속성 컨텍스트 생성
  • 트랜잭션 끝 → 영속성 컨텍스트 종료

이런 전략 때문에 보통 비즈니스 로직을 시작하는 서비스 계층에 `@Transactional` 을 붙이는 것이다.

이것을 붙임으로써 서비스 계층에서 조회한 Entity(엔티티)는 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다는 의미이기도 하다.

 

 

📚`@Transactional`을 적었을 때

@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 연관관계를 가지고 있다. 이 때 회원은 지연 로딩(Lazy Loading) 방식이다.

위 코드에서는 게시글을 전체 조회하여 1. 게시글 정보만 출력  2. 게시글과 연관된 회원의 로그인 출력 을 진행하고 있다.

서비스 계층에 `@Transactional`을 적었을 때는 회원의 아이디가 출력이 잘 되었다.

 

 

📚`@Transactional`을 적지 않았을 때

트랜잭션이 없는 상태에서 지연 로딩 할 때

게시글 엔티티는 조회가 되었지만 지연로딩인 회원은 예외가 발생하게 된다.

트랜잭션이 없는 서비스 계층이므로 조회된 엔티티는 준영속 상태가 된다.

준영속 상태이므로 변경 감지(Dirty Checking)와 지연 로딩이 동작하지 않게 된다.

준영속 상태에서 지연 로딩을 하게 될 시에는 위 코드처럼 `LazyInitializationException`이 발생하게 된다.

준영속 상태에서 지연로딩을 사용하면 문제가 발생한다.
하지만 JPA 표준에 어떤 문제가 발생하는지 정의하지 않아서 구현체마다 다르게 동작한다.

 

 

🧐 그렇다면 지연 로딩을 해결하는 방법은 무엇이 있을까?

  • 필요한 엔티티를 미리 로딩한다
  • OSIV 사용하여 엔티티를 영속 상태로 유지한다

 

 

📚 필요한 엔티티를 미리 로딩한다

말 그대로 영속성 컨텍스트가 있을 때 필요한 엔티티를 미리 로딩하거나 초기화해서 반환하는 방법이다.

이 방법도 어디서 로딩하느냐에 따라 달라지게 된다.

  • 글로벌 페치 전략 수정 : 지연 로딩에서 즉시 로딩으로 변경
    • 사용하지 않는 엔티티를 로딩할 수 있음
    • N+1 문제가 발생할 수 있음
  • JPQL 페치 조인(Fetch Join) : JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있음
    • N+1 문제가 발생하지 않음 (연관된 엔티티를 미리 로딩)
    • 화면에 맞춘 Repository 메소드가 증가할 가능성이 있음
  • 강제 초기화 : 영속성 컨텍스트가 살아있을 때 강제 초기화하여 반환하는 방법

위 방법들을 사용해도 결국 모든 문제는 엔티티가 준영속 상태이기 때문에 발생하는 것이다.

그럼 엔티티를 영속 상태로 만들 수 없을까?

영속 상태로 만들려면 영속성 컨텍스트를 열어두면 된다. 이러면 지연 로딩을 사용할 수 있는데 이것이 OSIV이다.

 

 

📚 OSIV 사용하여 엔티티를 영속 상태로 유지한다

Open Session In View의 약자로써 영속성 컨텍스트를 뷰까지 열어둔다는 의미이다.

영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지되어서 지연 로딩을 사용할 수 있다.

이것은 요청당 트랜잭션 방식의 OSIV가 있고, 스프링 프레임워크가 제공하는 비즈니스 계층에서만 트랜잭션을 유지하는 OSIV가 있다.

후자를 줄여서 스프링 OSIV라고 하겠다.

 

요청당 트랜잭션 방식의 OSIV는 프레젠테이션 계층에서 데이터를 변경할 수 있는 문제점이 있다.

(트랜잭션 범위가 영속성 컨텍스트와 같은 문제)

하지만 스프링 OSIV는 이런 문제를 해결했는데, 바로 비즈니스 계층에서만 트랜잭션이 사용되면서 영속성 컨텍스트는 뷰까지 제공하는 것이다.

 

스프링 OSIV를 사용할 때 클라이언트 요청이 들어오면 영속성 컨텍스트를 생성하게 된다.

이 영속성 컨텍스트는 서비스 계층에서도 유지되어 있기에 지연로딩인 엔티티를 조회할 수 있다.

클라이언트 요청 시 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다.

 

 

📚 트랜잭션 없이 읽기 (Nontransactional Reads)

말 그대로 단순 조회만 할 때 사용하는 방법으로 트랜잭션 없이 엔티티를 조회하는 것이다.

  • 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티 조회 및 수정 가능
  • 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티 조회만 가능

이렇기 때문에 `@Transactional`이 없어도 영속성 컨텍스트만 있다면 지연로딩을 사용할 수 있게 된다.

이 부분을 조금만 더 자세히 말하자면 최초 DB 커넥션 시작 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 DB 커넥션을 유지하기 때문에 영속 상태인 엔티티에서 지연 로딩도 조회가 가능한 것이다.

즉, 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하다.

 

만약 OSIV를 끄게 된다면 트랜잭션 종료할 때 영속성 컨텍스트를 닫고 DB커넥션도 반환하여 커넥션 리소스를 낭비하지 않게 되는데, 이러면 지연로딩은 트랜잭션 안에서 처리해야 한다.

 

 

🧐 어? 그럼 영속상태니까 변경감지도 되나요?

영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 한다.

OSIV를 켜놓고 `@Transactional`을 쓰지 않는다면 트랜잭션을 시작도 안한 것이다.

따라서 변경 감지는 동작하지 않는다.

 

 

📝 결론

읽기 전용 트랜잭션은 플러시를 작동하지 않게 해서 성능을 향상시킨다.

하지만 "애초부터 트랜잭션이 없었다면 읽기 전용으로 만들지않아도 되고 읽기 전용으로 만든것처럼 성능 최적화가 되지 않을까?"라는 의문이 생길 수 있다.

위 두 방법 전부 영속성 컨텍스트를 플러시 하지 않음으로써 스냅샷 비교와 같은 무거운 로직을 수행하지 않아 성능이 향상된다.

물론 트랜잭션이 아예 없으면 트랜잭션 시작, 로직 수행, 트랜잭션 커밋 과정조차 사라지지만 그 정도 성능은 미비할 것으로 생각된다.

 

따라서 단순 조회만 하는 서비스 계층이고, 연관관계가 지연로딩인 엔티티도 조회하는 경우가 있을 때는 OSIV를 켜놓고 `@Transactional` 없이 조회해도 된다.

 

하지만 동시성 이슈를 생각하지 않아도 되는 단순 조회만 하는 서비스 계층은 많이 없을테고, 확장성과 유지보수를 생각한다면 대부분 Service단에 @Transactional을 적는 것이 옳다고 생각된다.

OSIV 설정 정보

Spring Boot(스프링부트)에서 OSIV라는 설정을 자동으로 true로 해놓는다.
이처럼 스프링부트의 자동 설정은 다양하게 수행되고 있다.