[JPA] 13장 웹 애플리케이션과 영속성 관리

diveintoo·2022년 4월 16일
0

본 글은 김영한님의 <자바 ORM 표준 JPA 프로그래밍>을 읽고 공부한 내용을 정리한 글입니다.

컨테이너 환경에서 JPA가 동작하는 내부 동작 방식을 이해한다.
컨테이너 환경에서 웹 애플리케이션을 개발할 때 발생할 수 있는 다양한 문제점과 해결 방안을 탐구한다.

1. 트랜잭션 범위의 영속성 컨텍스트

1.1 스프링 컨테이너의 기본 전략

  • 스프링은 트랜잭션 범위의 영속성 컨텍스트를 기본 전략으로 사용한다.

  • 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 동일하며, 트랜잭션 종료 시, 영속성 컨텍스트도 동일하게 종료하게 된다.

  • 스프링 어플리케이션에서는 @Transactional을 사용하여 트랜잭션을 시작하게 되는데, 단순 호출처럼 보이는 부분도 사실 스프링의 트랜잭션 AOP가 먼저 작동하게 된다.

  • 트랜잭션이 같을 경우, 같은 영속성 컨텍스트를 사용한다.
    다양한 위치에서 엔티티 매니저(EntityManager)를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용하게 된다.

  • 트랜잭션이 다를 경우, 다른 영속성 컨텍스트를 사용한다.
    여러 스레드에서 동시에 요청에 올 경우, 같은 엔티티 매니저를 사용한다고 하더라도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 달라진다.

1.2 준영속 상태와 지연 로딩

  • 조회한 엔티티가 트랜잭션 범위인 서비스, 레파지토리 레이어에서는 영속성 컨텍스트에 의해 관리가 되기 때문에 영속 상태를 유지하지만, 이 밖의 레이어에서는 준영속 상태가 된다.

  • 영속성 컨텍스트의 관리 밖에서는 지연 로딩 및 변경 감지가 동작하지 않는다.

    • 필요한 엔티티를 미리 로딩함으로써 지연 로딩을 사용할 수 있다.
      • 글로벌 페치 전략 수정
      • JPQL 페치 조인
      • 강제로 초기화
      • FACADE 계층
    • OSIV를 사용하여 엔티티를 항상 영속 상태로 유지

글로벌 페치 전략 수정

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch=FetchType.EAGER)	// 즉시 로딩
    private Member member;
}
  • 사용하지 않는 엔티티를 로딩한다.

  • N+1 문제가 발생한다.

JPQL 페치 조인

  • JPQL 페치 조인은 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있다.

  • JPQL 페치조인을 사용하도록 변경하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 발생하지 않는다.

  • 가장 현실적인 대안

  • 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가한다.

    • 화면 A를 위해 order만 조회하는 repository.findOrder() 메소드
    • 화면 B를 위해 order와 연관된 member를 페치 조인으로 조회하는 repository.findOrderWithMember() 메소드
      -> 뷰와 리포지토리 간에 논리적인 의존관계가 발생한다.

강제로 초기화

class OrderService{
    @Transactional
    public Order findOrder(id){
        Order order = orderRepository.findOrder(id);
        order.getMember().getName();	// 프록시 객체를 강제로 초기화한다.
        return order;
    }
}
  • 트랜잭션 안에서 강제로 초기화해서 반환하여 준영속 상태에서도 사용할 수 있도록 한다.

  • 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 하므로 프리젠테이션 계층이 서비스 계층을 침범하는 상황이 발생한다.

FACADE 계층

  • 프리젠테이션 계층에서 필요한 프록시 객체를 초기화한다.

  • 중간에 계층이 하나 더 끼어들기 때문에 더 많은 코드를 작성해야 한다.

모든 문제는 엔티티가 프레젠테이션 계층에서 준영속 상태이기 때문에 발생한다.

2. OSIV

-> Open Session In View
영속성 컨텍스트를 프레젠테이션 레이어까지 열어둔다.

2.1 과거 OSIV: 요청 당 트랜잭션

  • 서블릿 필터 혹은 스프링 인터셉터에서 영속성 컨텍스트 생성

  • 이때부터 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료한다.

  • 지연로딩을 처리하기 위해 프록시 객체를 초기화해야 하는 과정이 필요없다.

  • 비즈니스 레이어가 아닌 곳에서 데이터 변경이 발생하는 문제가 생길 수 있다.
    -> 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법

    • 엔티티를 읽기 전용 인터페이스로 제공
    • 엔티티 레핑
    • DTO만 반환

2.2 스프링 OSIV: 비즈니스 계층 트랜잭션

  • 클라이언트의 요청이 들어오면 서블릿 필터 혹은 인터셉터에서 영속성 컨텍스트를 생성한다.(트랜잭션 X)

  • 서비스 계층에서 트랜잭션을 시작할 때, 앞에서 생성한 영속성 컨텍스트에 트랜잭션을 시작한다.

  • 서비스 계층이 종료되면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러쉬한다. 트랜잭션만 종료한다.(영속성 컨텍스트 X)

  • 요청이 끝날 때 영속성 컨텍스트를 종료한다.

트랜잭션 없는 읽기

-> 변경은 반드시 트랜잭션 내에서 이뤄져야 하는데, 조회의 경우 트랜잭션 없이도 가능하다.

프록시 객체를 초기화하는 지연 로딩 역시 조회이므로 트랜잭션 없이 읽기가 가능하다.

주의점

컨트롤러에서 엔티티를 일부 값을 수정한 뒤, 바로 뷰로 데이터를 반환하지 않고 또 다른 서비스 메서드를 호출하여 트랜잭션을 시작할 경우 문제가 발생하게 된다.

컨트롤러에서 변경한 값이 트랜잭션 커밋에 의해 실제 데이터베이스에 반영되기 때문이다.

-> 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 된다.

OSIV를 사용하는 방법이 만능은 아니다.

  • OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다.

  • 복잡한 화면을 구성할 때는 이 방법이 효과적이지 않는 경우가 많다.

    • 복잡한 통계 화면은 엔티티로 조회하기보다는 처음부터 통계 데이터를 구상하기 위한 JPQL을 작성해서 DTO로 조회하는 것이 더 효과적이다.

0개의 댓글