트랜잭션 범위의 영속성 컨텍스트
스프링 컨테이너의 기본 전략
- 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
- 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.
- 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
- 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
- 따라서 엔티티 매니저는 달라도 같은 영속성 컨텍스트를 사용한다.
- 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
- 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
- 스프링이나 J2EE 컨테이너의 가장 큰 장점은 트랜잭션과 복잡한 멀티 스레드 상황을 컨테이너가 처리해준다는 점이다.
준영속 상태와 지연 로딩
- 트랜잭션이 없는 프리젠테이션 계층에서 엔티티는 준영속 상태다. 따라서 변경 감지와 지연 로딩이 동작하지 않는다.
- 하이버네이트를 구현체로 사용하고
LazyInitializerException
예외가 발생한다.
- 하이버네이트를 구현체로 사용하고
- 뷰를 렌더링할 때 영관된 엔티티도 함께 사용해야 하는데 연관된 엔티티가 준영속 상태이면 지연 로딩이 되지 않아 문제가 될 수 있다. 해결 방법은 크게 2가지가 있다.
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
- 뷰가 필요한 엔티티를 미리 로딩하는 방법은 3가지 방법이 있다.
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제로 초기화
글로벌 페치 전략 수정
- 글로벌 페치 전략에 즉시 로딩 사용 시 단점
- 사용하지 않는 엔티티를 로딩한다.
- N+1 문제가 발생한다.
- N+1 문제는 JQL 페치 조인으로 해결할 수 있다.
JPQL 페치 조인
- 페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다.
- 따라서 N+1 문제가 발생하지 않는다.
- JPQL 페치 조인의 단점
- 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있다.
- 결국 프레젠테이션 계층이 데이터 접근 게층을 침범하는 것이다.
- 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가할 수 있다.
강제로 초기화
- 강제로 초기화 단점
- 프리젠테이션 계층이 서비스 게층을 침범하는 상황이 된다.
FACADE 계층 추가
- ‘강제로 초기화’의 단점을 해결하기 위해서, 프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 추가한다.
- FACADE 계층에서 뷰를 위한 프록시 초기화를 담당한다.
- FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리한다.
- 프리젠테이션 계층과 필요한 프록시 객체를 초기화한다.
- 서비스 계층을 호출해서 비즈니스 로직을 실행한다.
- 리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.
- 하지만 실용적인 관점으로 볼 때 FACADE의 최대 단점은 중간에 계층이 하나 더 끼어들어 더 많은 코드를 작성해야 하는 것이다.
준영속 상태와 지연 로딩의 문제점
- 결국 이 문제들은 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다.
- OSIV: 영속성 컨텍스트를 뷰까지 살아있게 열어둔다.
- 이러면 뷰에서도 지연로딩을 사용할 수 있다.
OSIV
- OSIV: 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
- OSIV는 하이버네이트에서 사용하는 용어다. JPA에서는 OEIV(Open EntityManager in View) 라한다. 하지만 관례상 모두 OSIV로 부른다.
과거 OSIV: 요청 당 트랜잭션
- 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다.
- 문제점: 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점이다.
- 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법들은 다음과 같다.
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 레핑
- DTO만 반환
- 엔티티를 읽기 전용 인터페이스로 제공
- 위 방법 모두 코드량이 상당히 증가한다는 단점이 있다.
- 최근에는 이런 문제점을 어느정도 보완해서 비즈니스 게층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다.
스프링 OSIV: 비즈니스 계층 트랜잭션
- 스프링 프레임워크가 제공하는 OSIV 라이브러리
- 스프링 프레임워크가 제공하는 OSIV는 “비즈니스 게층에서 트랜잭션을 사용하는 OSIV"다.
- 클라이언트 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스틀르 생성한다. 단 이때 트랜잭션을 시작하지 않는다.
- 서비스 게층에서
@Transactional
로 트랜잭션을 시작할때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다. - 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝내지면 영속성 컨텍사트는 종료 하지 않는다.
- 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
- 서비스 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.
- 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 트랜잭션 없이 읽기라 한다.
- 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
- 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. 이것을 트랜잭션 없이 읽기라 한다.
- 스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV는 다음과 같은 특징이 있다.
- 영속성 컨텍스트를 프리젠테이션 계층까지 유지한다.
- 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정할 수 없다.
- 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩을 할 수 있다.
- 스프링 OSIV 주의사항
- 프리젠테이션 계층에서 엔티티를 수정하면 반영하지 않지만, 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 게층을 호출하면 플러시가 된다.
OSIV 정리
- 스프링 OSIV의 단점
- 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다. 특히 트랜잭션 롤백 시 주의해야 한다.
- 프리젠테이션 계층에서 엔티티를 수정하고나서 비즈니스 로직을 수행함녀 엔티티가 수정될 수 있다.
- 프리젠테이션 게층에서 지연 로딩에 의한 SQL이 실행된다. 따라서 성능 튜닝 시에 확인해야 할 부분이 넓다.
- OSIV를 사용하는 방법은 만능이 아니다
- 복잡한 통계 화면은 엔티티로 조회하기보다는 처음부터 통계 데이터를 구상하기 위한 JPLQ을 작성해서 DTO로 조회하는 것이 효과적이다.
- 수많은 테이블을 조인해서 보여주어야 하는 복잡한 관리자 화면도 객체 그래프로 표현하기 어려운 경우가 많다. 이때도 엔티티를 직접 조회하기 보다는 JPQL로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 더 나은 해결책일 수 있다.
- OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다.
- JSON이나 XML을 생성할 때는 지여 로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다.
- 외부 API: 타팀과 협업하기 위한 API, 타 기업과 협업하는 API
- 엔티티를 직접 노출하기 보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다.
- 내부 API: 같은 프로젝트에 있는 화면을 구성하기 위한 AJAX 호출
- 엔티티를 변경해도 클라이언트와 서버를 동시에 수정할 수 있어서 실용적인 관점에서 엔티티를 직접 노출하는 방법도 괜찮다고 생각한다.
너무 엄격한 계층
- OSIV를 사용하면서 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 , 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무런 문제가 없다.
- 과거 EJB 시절에는 프리젠테이션 계층이 엔티티를 직접 반환하면 여러 가지 문제가 발생했다.
- 따라서 대부분 DTO를 만들어서 반환했고 엔티티가 계층을 뛰어넘는 것은 어려운 일이었다.
- OSIV를 사용하면 설명한 것처럼 좀 더 유연하고 실용적인 관점으로 접근하는 것도 좋은 방법이라 생각한다.