////
Search
Duplicate
🕌

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

스프링이나 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를 사용하면 해결할 수 있다.