꿈꾸는 새벽하늘

13장. 웹 애플리케이션과 영속성 관리 본문

🌿 Spring & Spring Boot/📗 자바 ORM 표준 JPA 프로그래밍

13장. 웹 애플리케이션과 영속성 관리

rovemin 2023. 8. 21. 23:52

1. 트랜잭션 범위의 영속성 컨텍스트

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

이 전략은 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같다는 것을 의미한다.

즉, 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.

 

스프링 프레임워크를 사용하면 보통 비즈니스를 시작하는 서비스 계층에 @Transaction 어노테이션을 선언해서 트랜잭션을 시작한다. 외부에서는 단순히 서비스 계층의 메소드를 호출하는 것처럼 보이지만 이 어노테이션이 있으면 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다.

스프링 트랜잭션 AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다. 이때 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후 데이터베이스 트랜잭션을 커밋하므로 영속성 컨텍스트의 변경 내용이 데이터베이스에 정상 반영되게 된다. 만약 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다.

 

  • 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다. 따라서 다양한 위치에서 엔티티 매니저를 주입받아 사용하더라도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
  • 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다. 따라서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.

2. 준영속 상태와 지연 로딩

준영속 상태에서는 지연 로딩 기능이 동작하지 않는다. 이 문제를 해결하는 방법으로는 2가지가 있다.

  • 뷰가 필요한 엔티티를 미리 로딩해두는 방법
  • OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법

 

뷰가 필요한 엔티티를 미리 로딩해두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 있다.

  • 글로벌 페치 전략 수정
  • JPQL 페치 조인
  • 강제로 초기화

(1) 글로벌 페치 전략 수정

글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하는 방법이다.

글로벌 페치 전략에 즉시 로딩을 사용하면 사용하지 않는 엔티티를 로딩하고, N+1 문제가 발생한다. N+1 문제는 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것이다. N+1 문제는 JPQL 페치 조인으로 해결할 수 있다.

(2) JPQL 페치 조인

JPQL 페치 조인은 JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인이다.

페치 조인은 조인 명령어 마지막에 fetch를 넣어주면 된다.

페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회하므로 N+1 문제가 발생하지 않는다.

그러나 페치 조인을 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있고, 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범하게 된다는 단점이 있다.

(3) 강제로 초기화

강제로 초기화하기는 영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.

3. OSIV

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 것을 의미한다.

영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지된다. 따라서 뷰에서도 지연 로딩을 사용할 수 있다.

 

과거의 OSIV는 요청 당 트랜잭션 방식이었다.

OSIV의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이므로 가장 단순한 구현 방법은 클라이언트의 요청이 들어오자마자 서블릿 필터가 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다.

그러나 요청 당 트랜잭션 방식의 OSIV는 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 문제점이 있었다. 이를 해결하기 위해 엔티티를 읽기 전용 인터페이스로 제공하고, 엔티티를 레핑하고, DTO만 반환하는 방법이 있으나 이 방법들은 코드량이 상당히 증가한다는 단점이 있다.

 

스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV다.

동작원리는 아래와 같다.

  1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단, 이때 트랜잭션은 시작하지 않는다.
  2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
  3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다.
  4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
  5. 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.

스프링 OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다. 따라서 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다. 그리고 엔티티 수정은 트랜잭션이 있는 계층에서만 동작하며 트랜잭션이 없는 프리젠테이션 계층은 지연 로딩을 포함해서 조회만 할 수 있다.