본 글은 김영한님의 <자바 ORM 표준 JPA 프로그래밍>을 읽고 공부한 내용을 정리한 글입니다.
컨테이너 환경에서 JPA가 동작하는 내부 동작 방식을 이해한다.
컨테이너 환경에서 웹 애플리케이션을 개발할 때 발생할 수 있는 다양한 문제점과 해결 방안을 탐구한다.
스프링은 트랜잭션 범위의 영속성 컨텍스트를 기본 전략으로 사용한다.
트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 동일하며, 트랜잭션 종료 시, 영속성 컨텍스트도 동일하게 종료하게 된다.
스프링 어플리케이션에서는 @Transactional을 사용하여 트랜잭션을 시작하게 되는데, 단순 호출처럼 보이는 부분도 사실 스프링의 트랜잭션 AOP가 먼저 작동하게 된다.
트랜잭션이 같을 경우, 같은 영속성 컨텍스트를 사용한다.
다양한 위치에서 엔티티 매니저(EntityManager)를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용하게 된다.
트랜잭션이 다를 경우, 다른 영속성 컨텍스트를 사용한다.
여러 스레드에서 동시에 요청에 올 경우, 같은 엔티티 매니저를 사용한다고 하더라도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 달라진다.
조회한 엔티티가 트랜잭션 범위인 서비스, 레파지토리 레이어에서는 영속성 컨텍스트에 의해 관리가 되기 때문에 영속 상태를 유지하지만, 이 밖의 레이어에서는 준영속 상태가 된다.
영속성 컨텍스트의 관리 밖에서는 지연 로딩 및 변경 감지가 동작하지 않는다.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch=FetchType.EAGER) // 즉시 로딩
private Member member;
}
사용하지 않는 엔티티를 로딩한다.
N+1 문제가 발생한다.
JPQL 페치 조인은 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있다.
JPQL 페치조인을 사용하도록 변경하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 발생하지 않는다.
가장 현실적인 대안
무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가한다.
class OrderService{
@Transactional
public Order findOrder(id){
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 프록시 객체를 강제로 초기화한다.
return order;
}
}
트랜잭션 안에서 강제로 초기화해서 반환하여 준영속 상태에서도 사용할 수 있도록 한다.
뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 하므로 프리젠테이션 계층이 서비스 계층을 침범하는 상황이 발생한다.
프리젠테이션 계층에서 필요한 프록시 객체를 초기화한다.
중간에 계층이 하나 더 끼어들기 때문에 더 많은 코드를 작성해야 한다.
모든 문제는 엔티티가 프레젠테이션 계층에서 준영속 상태이기 때문에 발생한다.
-> Open Session In View
영속성 컨텍스트를 프레젠테이션 레이어까지 열어둔다.
서블릿 필터 혹은 스프링 인터셉터에서 영속성 컨텍스트 생성
이때부터 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료한다.
지연로딩을 처리하기 위해 프록시 객체를 초기화해야 하는 과정이 필요없다.
비즈니스 레이어가 아닌 곳에서 데이터 변경이 발생하는 문제가 생길 수 있다.
-> 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법
클라이언트의 요청이 들어오면 서블릿 필터 혹은 인터셉터에서 영속성 컨텍스트를 생성한다.(트랜잭션 X)
서비스 계층에서 트랜잭션을 시작할 때, 앞에서 생성한 영속성 컨텍스트에 트랜잭션을 시작한다.
서비스 계층이 종료되면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러쉬한다. 트랜잭션만 종료한다.(영속성 컨텍스트 X)
요청이 끝날 때 영속성 컨텍스트를 종료한다.
-> 변경은 반드시 트랜잭션 내에서 이뤄져야 하는데, 조회의 경우 트랜잭션 없이도 가능하다.
프록시 객체를 초기화하는 지연 로딩 역시 조회이므로 트랜잭션 없이 읽기가 가능하다.
컨트롤러에서 엔티티를 일부 값을 수정한 뒤, 바로 뷰로 데이터를 반환하지 않고 또 다른 서비스 메서드를 호출하여 트랜잭션을 시작할 경우 문제가 발생하게 된다.
컨트롤러에서 변경한 값이 트랜잭션 커밋에 의해 실제 데이터베이스에 반영되기 때문이다.
-> 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 된다.
OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다.
복잡한 화면을 구성할 때는 이 방법이 효과적이지 않는 경우가 많다.