•
스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해주므로 어플리케이션을 쉽게 개발할 수 있다.
•
그러나 컨테이너 환경에서 동작하는 JPA의 내부 동작 방식을 이해하지 못하면 문제가 발생했을 때 해결하기가 쉽지 않다.
•
이번 장에는 컨테이너 환경에서 JPA가 동작하는 내부 동작 방식을 이해하고 컨테이너 환경에서 웹 어플리케이션을 개발할 때 발생할 수 있는 문제점과 해결 방안을 알아보자.
1. 트랜잭션 범위의 영속성 컨텍스트
•
순수한 J2SE 환경에서 JPA를 사용하면 개발자가 직접 엔티티 매니저를 생성하고 트랜잭션도 관리해야 한다.
•
스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 제공하는 전략을 따라야 한다.
1. 스프링 컨테이너의 기본 전략
•
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
◦
트랜잭션의 범위와 영속성 컨텍스트의 생명 주기가 같다는 뜻이다.
•
보통 스프링 프레임워크를 사용하면 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다.
◦
이는 스프링의 트랜잭션 AOP가 동작하여 프록시 패턴 형태로 구현된다.
•
따라서 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용하고 다르면 다른 영속성 컨텍스트를 사용한다.
◦
엔티티 매니저에 영향을 받지 않는다.
2. 준영속 상태와 지연 로딩
•
트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다.
•
따라서 조회한 엔티티가 서비스 즉 응용의 하위 계층에서는 영속성 컨텍스트에 의해 관리되면서 영속 상태를 유지하지만 표현 계층에서는 준영속 상태가 된다.
•
변경 감지 기능은 영속성 컨텍스트가 살아 있는 계층까지만 동작하고 영속성 컨텍스트가 종료된 표현 계층에서는 동작하지 않는다.
•
준영속 상태의 가장 골치 아픈 문제는 지연 로딩 기능이 동작하지 않는다는 점이다.
◦
뷰를 렌더링할때 연관된 엔티티도 함께 사용해야 하는데, 연관된 엔티티를 지연 로딩으로 조회하여 프록시 객체가 조회되었을 경우 문제가 발생할 수 있다.
•
뷰가 필요한 엔티티를 미리 로딩해두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 정도 방법이 있다.
1. 글로벌 페치 전략 수정
•
글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하는 방법이다.
•
이 경우 즉시 로딩의 단점을 가져가게 되며 다음과 같다.
◦
사용하지 않는 엔티티를 로딩한다.
◦
N+1 문제가 발생한다.
▪
JPQL을 사용하는 경우, JPA가 이를 분석하여 SQL을 생성할 때, 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용하기 때문이다.
2. JPQL 페치 조인
•
JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인을 사용한다.
// 페치 조인 사용 전
JPQL:
select o
from Order o
SQL:
select *
from Order
// 페치 조인 사용 후
JPQL:
select o
from Order o
join fetch o.member
SQL:
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID = m.MEMBER_ID
Java
복사
•
무분별하게 사용하는 경우, 표현 계층에 직접 의존하는 데이터 접근 계층이 만들어질 수 있다.
3. 강제로 초기화
•
영속성 컨텍스트가 살아있을 때 표현 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.
•
이를 서비스 계층에서 수행하는 것은, 응용 계층이 표현 계층을 위한 작업을 수행하는 것이므로 부적절하며 이를 파사드 게층으로 분리한다.
4. FACADE 계층 추가
•
표현 계층와 응용 계층 사이에 뷰를 위한 프록시 초기화를 담당하는 계층을 하나 더 두는 것이다.
•
표현 계층과 도메인 모델 계층 간의 논리적 의존성을 분리해주며 필요한 프록시 객체를 초기화한다.
•
서비스 계층을 호출하여 비즈니스 로직을 실행하고 레포지터리를 직접 호출하여 뷰가 요구하는 엔티티를 찾는다.
5. 준영속 상태와 지연 로딩의 문제점
•
이는 복잡도를 상승시켜 파사드 계층에서 문제를 발생할 여지를 만든다.
•
결국 모든 문제의 원인은 엔티티가 표현 계층에서 준영속 상태이기 때문에 발생한다. 영속성 컨텍스트가 표현 계층까지 살아있게 하여 지연 로딩을 사용할 수 있도록 하는 것이 OSIV다.
3. OSIV
•
Open Session In View로 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
1. 과거 OSIV: 요청 당 트랜잭션
•
OSIV의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것으로 가장 단순한 구현 방법은 클라이언트의 요청마다 서블릿 필터가 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다.
•
이 경우, 컨트롤러나 뷰 같은 표현 계층에서 엔티티에 대한 조작이 가능하므로 유지보수가 어려워질 가능성이 있다.
◦
이를 방지하기 위해 엔티티를 읽기 전용 인터페이스로 제공한다거나 엔티티 래핑, DTO만 반환하는 방법들이 존재한다.
2. 스프링 OSIV: 비즈니스 계층 트랜잭션
•
스프링 프레임워크는 다양한 OSIV 라이브러리를 가지고 있다.
◦
하이버네이트 OSIV 서블릿 필터, 스프링 인터셉터, JPA OEIV 서블릿 필터, 스프링 인터셉터
•
스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV로 트랜잭션을 비즈니스 계층에서만 사용하겠다는 의도다.
•
영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서만 이루어져야 한다. 트랜잭션 없이 엔티티를 변경하고 플러시하면 TranscatinoRequiredException 예외가 발생한다.
3. OSIV 정리
•
스프링 OSIV의 특징
◦
OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다.
▪
따라서 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다.
◦
엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다. 트랜잭션이 없는 표현 계층은 조회만 가능하다.
•
단점
◦
OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다.
▪
특히 트랜잭션 롤백을 주의해야 한다.
◦
표현 계층에서 엔티티를 수정하고 비즈니스 로직을 수행하면 엔티티 수정이 반영될 수 있다.
◦
표현 게층에서 지연 로딩에 의한 SQL이 실행되므로 성능 튜닝 시 확인해야할 부분이 많다.
•
OSIV vs FACADE vs DTO
◦
어떤 방법을 사용하든 준영속 상태가 되기 전 프록시를 초기화해야 한다.
•
OSIV를 사용하는 방법이 만능은 아니다.
◦
OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다.
◦
통계와 같이 복잡한 데이터를 요하는 경우 JPQL로 필요한 데이터만 조회해서 DTO로 반환하는 것이 더 나을 수 있다.
•
엔티티는 생각보다 자주 변경되므로 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티는 표현 계층에 과하게 의존하게 된다.
•
따라서 외부 API는 엔티티를 직접 노출하기보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다.
5. 정리
•
스프링이나 J2EE 컨테이너 환경에서 JPA는 트랜잭션 범위의 영속성 컨텍스트 전략이 적용된다.
•
이 전략은 트랜잭션의 범위와 영속성 컨텍스트의 생명 주기가 같고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
•
이 전략의 유일한 단점인 엔티티가 준영속 상태일때 발생할 수 있는 문제들을 OSIV를 사용하면 해결할 수 있다.