스프링 컨테이너는 트랜잭션 범위와 영속성 컨테이너의 라이프사이클이 동일한 트랜잭션 범위의 영속성 컨텍스트 전략을 기본 전략으로 채택하고 있습니다.
영속성 컨텍스트 및 생명주기는 이전 포스트를 참조해주세요.
스프링에서는 서비스 레이어의 메소드(종종 클래스)에 @Transactional을 표기하여 트랜잭션을 시작 및 단위로 나누게 됩니다.
내부적으로는 @Transactional을 만나면 서비스 메소드를 실행하기 전에 스프링 AOP가 먼저 동작하여 트랜잭션을 시작하게 됩니다. 그리고 메소드가 종료되면 트랜잭션을 커밋하면서 종료하게 됩니다.
트랜잭션을 커밋하는 시점에 JPA는 영속성 컨텍스트의 변경 내용을 DB에 반영하고 트랜잭션을 커밋하게 됩니다. 만약 오류가 생기면 롤백을 수행하게 됩니다.
트랜잭션 범위의 영속성 컨텍스트 전략은 동일한 트랜잭션 내에서는 항상 동일한 영속성 컨텍스트를 사용하고, 서로 다른 트랜잭션에서는 서로 다른 영속성 컨텍스트를 사용합니다.
위에서 등장한 동작 그림을 보면 서비스와 리포지토리는 동일한 영속성 컨텍스트로 관리됩니다. 그러나 컨트롤러(+ 뷰)는 트랜잭션 범위에 포함되지 않기 때문에 준영속 상태가 됩니다.
준영속 상태에 대한 이해가 필요한 내용이므로 잘 모르신다면 준영속 상태에 대한 내용을 먼저 보고 오시는 것을 권장드립니다.
준영속 상태에서는 지연 로딩 및 Dirty Check가 동작하지 않기 때문에 컨트롤러 레이어에서 엔티티를 변경하고자 하면 예외가 발생합니다.
준영속 상태의 Dirty Check 같은 경우 책임 분리 문제나 유지보수성 문제 등으로 인해 컨트롤러 또는 뷰에서 데이터가 변경되는 것이 권장되지 않기도 하면서 컨트롤러에서 데이터가 변경될 일이 거의 없기에 Dirty Check가 컨트롤러 레이어에서 동작하지 않는 것은 큰 문제가 되지 않습니다.
문제는 지연 로딩인데 지연 로딩으로 설정한 경우 연관 엔티티는 프록시 객체로 로딩이 됩니다. 뷰에서 어떤 동작을 통해 프록시 객체를 로딩하고자 할 때 실제 엔티티를 가져오기 위해 초기화를 수행하게 됩니다. 이때 준영속 상태는 영속성 컨텍스트가 존재하지 않는 상태이기 때문에 데이터를 가져올 수 없는 문제가 발생합니다.
다시말해, 프록시 객체는 영속성 컨텍스트로 실제 엔티티를 조회하게 되는데 준영속 상태에서는 영속성 컨텍스트가 존재하지 않아서 지연 로딩을 할 수 없다라는 것입니다.
따라서 뷰가 필요로하는 엔티티를 미리 로딩하는 방법과 OSIV 사용이라는 두 가지 방법을 통해 준영속 상태의 지연 로딩 사용 불가능 문제를 해결합니다.
뷰에서 필요한 엔티티를 미리 로딩하는 방법는 다시 세 가지 방법으로 구현됩니다.
글로벌 페치 전략 수정은 가장 간단한 방법으로 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하는 방법입니다.
가장 간단하지만 다음 두 가지 단점이 있습니다.
N+1 문제
해당 항목 참조. JPA에서 가장 주의해야하는 성능 문제로 하나의 쿼리로 부모 엔티티를 조회할 때 각 부모 엔티티의 연관된 자식 엔티티까지 N번의 쿼리로 조회하여 총 N+1번의 쿼리가 실행되는 성능 저하 문제
사용하지 않는 엔티티까지 조회
특정 뷰에서 엔티티 A와 그 연관 관계에 있는 B를 필요로해서 즉시 로딩으로 변경하는 경우 엔티티 A만을 필요로하는 뷰에서도 B 엔티티를 조회하게 되는 불필요한 조회가 발생합니다.
글로벌 페치 전략 수정은 간단하지만 애플리케이션 전체에 영향을 주게 됩니다. 그래서 JPQL이 호출되는 시점에 함께 로딩할 엔티티를 선택하게 되는 JPQL 페치 전략을 이용합니다.
다음과 같이 SELECT로 조회할 때 JOIN FETCH 구문을 삽입해서 필요한 엔티티를 추가로 조회합니다.
SELECT DISTINCT m FROM Member m JOIN FETCH m.orders
자세한 사용법은 이 포스트를 참조해주세요.
이 방법으로는 N+1 문제도 해결하면서 뷰에서 사용할 엔티티를 미리 로딩하는 가장 좋은 방법이기도 합니다.
가장 좋은 방법이라고는 했지만 뷰가 필요로하는 엔티티의 형식에 따라 리포지토리에 정의해야하는 메소드가 늘어난다는 단점이 있습니다. 서비스를 위한 메소드가 아니라 뷰를 위한 메소드이기 때문에 레이어 간의 경계가 흐릿해진다는 문제를 불러일으키는 것 입니다.
강제 초기화는 영속성 컨텍스트가 살아있는 시점에 뷰에서 필요로하는 엔티티를 강제로 초기화시켜서 반환하는 방식입니다. 영속성 컨텍스트가 생존해 있을 때 초기화 했으므로 준영속 상태에서도 사용할 수 있게 됩니다.
강제 초기화를 위해 프록시 초기화 여부를 확인하고 프록시를 초기화해야하는 과정이 필요합니다. 그런데 이 과정을 서비스 레이어에 작성하면 뷰(컨트롤러)와 서비스가 밀접해지기 때문에 이 사이에 Facade(façade 퍼사드)라는 레이어를 추가하여 프록시를 초기화하는 전용 계층을 도입하게 됩니다.
퍼사드는 컨트롤러와 서비스/리포지토리의 분리, 프록시 객체 초기화, 서비스 레이어 호출, 뷰가 요청한 엔티티를 리포지토리에서 직접 호출 등의 업무를 맡게 됩니다.
레이어간 의존성이 잘 분리된다는 장점이 있지만 퍼사드를 따로 정의해야하는 번거로움 등의 문제가 발생합니다.
OSIV (Open Session In View)는 영속성 컨텍스트를 뷰까지 열어두는 기술입니다.
Hibernate 구현체 OSIV 클래스에 대한 자세한 내용은 공식 API 문서를 참조해주세요.
스프링 프레임워크의 OSIV는 비즈니스 레이어(서비스와 리포지토리)에서만 트랜잭션을 사용합니다.
스프링 프레임워크 OSIV는 다음과 같이 동작합니다.

스프링 인터셉터(또는 서블릿 필터)는 영속성 컨텍스트를 생성. 단, 트랜잭션은 시작하지 않음@Transactional을 만나면 1번에서 만들어진 영속성 컨텍스트를 가져와 트랜잭션 시작스프링 인터셉터에 요청이 돌아오면 영속성 컨텍스트를 종료 (flush() 호출 X)영속성 컨텍스트는 트랜잭션 범위 안에서만 읽기/쓰기가 가능하며 트랜잭션 범위 밖에서는 읽기만 가능합니다.
지연 로딩의 프록시 초기화 또한 읽기(조회) 기능이므로 트랜잭션 범위 밖에서 수행될 수 있습니다. 이러한 동작을Nontransactional Read라고 합니다. 이를 이용해스프링 프레임워크 OSIV는 컨트롤러 또는 뷰에서 엔티티를 수정할 수 없게만들면서지연 로딩을 사용할 수 있게 만들어줍니다.