스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 이 전략은 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다. 즉, 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
스프링 프레임워크를 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional
어노테이션을 선언해서 트랜잭션을 시작한다.
트랜잭션 범위의 영속성 컨텐스트
트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 종료된다. 따라서 조회한 엔터티가 서비스와 레포지토리 계층에서는 영속 상태를 유지하지만 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다.
트랜잭션 범위의 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프레젠테이션 계층에서 엔터티는 준영속 상태다. 따라서 변경 감지와 지연 로딩이 동작하지 않는다.
변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층(트랜잭션 범위)까지만 동작한다. 단순히 데이터를 보여주기만 하는 프레젠테이션 계층에서 데이터를 수정할 일은 없기 때문에 문제되지 않는다.
준영속 상태의 가장 큰 문제는 지연 로딩 기능이 동작하지 않는다는 점이다. 이를 해결하는 방법은 크게 2가지가 있다.
글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하면 된다.
페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 발생하지 않는다.
JPQL 페치 조인을 무분별하게 사용하면 화면에 맞춘 레포지토리 메소드가 증가할 수 있어 프레젠테이션 계층이 데이터 접근 계층을 침범하게 된다.
따라서 무분별한 최적화로 프레젠테이션 계층과 데이터 접근 계층 간의 의존관계가 급격하게 증가하는 것보다는 적절한 선에서 타협점을 찾는 것이 합리적이다.
강제로 초기화하기는 영속성 컨텍스트가 살아있을 때 프레젠테이션 계층이 필요한 엔터티를 강제로 초기화해서 반환하는 방법이다.
이는 은근 슬쩍 프레젠테이션 계층이 서비스 계층을 침범하는 상황이므로 비즈니스 로직을 담당하는 서비스 계층에서 프렌젠체이션 계층을 위한 프록시 초기화 역할을 분리해야 한다. FACADE 계층이 그 역할을 담당해줄 것이다.
아래 그림은 프레젠테이션 계층과 서비스 계층 사이에 뷰를 위한 프록시 초기화를 담당하는 FACADE 계층을 하나 더 두는 방법이다.
뷰를 개발할 때 필요한 엔터티를 미리 초기화하는 방법은 생각보다 오류가 발생할 가능성이 높다. FACADE를 사용해서 어느 정도 해소할 수는 있지만 상당히 번거롭다.
따라서 모든 문제는 엔터티가 프레젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 영속성 컨텍스트를 뷰까지 살아있게 열어두어 뷰에서도 지연 로딩을 사용할 수 있게 하는 것이 OSIV이다.
OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔터티는 영속 상태로 유지되므로 뷰에서도 지연 로딩을 사용할 수 있다.
OSIV의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다. 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것을 요청 당 트랜잭션 방식의 OSIV라고 한다.
요청 당 트랜잭션 방식의 OSIV는 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔터티를 변경할 수 있다는 문제가 있다. 이런 문제를 해결하려면 프레젠테이션 계층에서 엔터티를 수정하지 못하게 막으면 된다.
프레젠테이션 계층에서 엔터티를 수정하지 못하게 막는 방법은 다음과 같다.
엔터티를 읽기 전용 인터페이스로 제공
엔터티 레핑 : 엔터티의 읽기 전용 메소드만 가지고 있는 엔터티를 감싼 객체를 만들어 프레젠테이션 계층에 반환
DTO만 반환 : 프레젠테이션 계층에 엔터티 대신 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환(OSIV의 장점을 살릴 수 없고, 엔터티를 거의 복사한 DTO 클래스를 하나 더 만들어야 한다.)
지금까지 설명한 방법 모두 코드량이 상당히 증가한다는 단점이 있다. 이러한 문제점들로 인해 요청 당 트랜잭션 방식의 OSIV는 최근에는 거의 사용하지 않는다.
스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서만 트랜잭션을 유지하는 방식이다.
동작 원리
영속성 컨텍스트는 트랜잭션 범위 안에서 엔터티를 조회하고 수정하 수 있는데, 트랜잭션 범위 밖에서 엔터티를 조회만 할 수 있는 것을 트랜잭션 없이 읽기라고 한다.
스프링 OSIV를 사용하면 프레젠테이션 계층에서 엔터티를 수정해도 데이터베이스에 반영되지 않지만, 엔터티 수정 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.
이런 문제를 해결하려면 단순히 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔터티를 변경하면 된다.
스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있어 이런 문제가 발생한다. OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같으므로 이런 문제가 발생하지 않는다.