트랜잭션 범위의 영속성 컨텍스트
스프링 컨테이너의 기본 전략
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다
트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 의미이다
즉, 트랜잭션이 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다
@Transactional이 붙은 메소드를 실행하기 전에 스프링 트랜잭션 AOP가 먼저 동작한다
스프링 트랜잭션 AOP는 대상 메소드를 호출하기 전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다
트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해 변경 내용을 데이터베이스에 반영한 후 데이터베이스 트랜잭션을 커밋한다
예외가 발생하는 경우 트랜잭션을 롤백하고 종료하는데 이때 플러시는 호출하지 않는다
트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용하고, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다
따라서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다
스프링 컨테이너는 스레드마다 다른 트랜잭션을 할당한다
같은 엔티티 매니저를 사용하더라도 트랜잭션이 달라 접근하는 영속성 컨텍스트가 다르므로 멀티 스레드 상황에 안전하다
준영속 상태와 지연 로딩
트랜잭션은 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 종료되고 영속성 컨텍스트도 함께 종료된다
조회한 엔티티가 서비스와 리포지토리 계층에서는 영속성 컨텍스트에 관리되며 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태가 된다
이때 지연 로딩으로 설정한 엔티티를 프레젠테이션 계층에서 접근하면 해당 엔티티는 준영속 상태이므로 예외가 발생한다
-> → LazyInitializationException 예외가 발생한다
준영속 상태의 지연 로딩 문제를 해결하는 방법
1. 뷰가 필요한 엔티티를 미리 로딩해두는 방법
영속성 컨텍스트가 살아 있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해서 반환하는 방법이다
어디서 미리 로딩하느냐에 따라 3가지 방법이 있다
- 글로벌 페치 전략 수정, JPQL 페치 조인, 강제로 초기화
2. OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
글로벌 페치 전략 수정
글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하면 된다
단점
1. 사용하지 않는 엔티티를 로딩한다
2. N+1 문제가 발생한다
JPA가 JPQL을 분석해 SQL을 생성하는 경우 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용해 문제가 발생한다
JPQL을 사용해 Order을 조회하는데 Order와 연관된 Member도 함께 조회하는 경우,
먼저 Order을 조회하고 조회한 Order의 수 만큼 다시 Member를 조회하는 쿼리를 실행한다
처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1 문제라 한다
SQL이 상당히 많이 호출되므로 조회 성능에 치명적이다
JPQL 페치 조인
JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있다
select o from Order o join fetch o.member
페치 조인은 조인 명령어 마지막에 fetch를 넣어주면 된다
페치 조인을 사용하면 SQL join을 사용해 페치 조인 대상까지 조회한다
-> N+1 문제가 발생하지 않는다
단점
페치 조인을 무분별하게 사용해 화면에 맞춘 리포지토리 메소드를 증가시키는 것은 프레젠테이션 계층이 데이터 접근 계층을 침범하는 것이다
예를 들어 Order만 필요한 화면과 Order와 Member 모두 필요한 화면 각각을 위해 메소드를 만들면 최적화는 가능하지만 뷰와 리포지토리 간에 논리적인 의존관계가 발생한다
최적화와 의존관계 최소화 사이의 타협점을 찾아야 한다
강제로 초기화
class OrderService {
@Transactional
public Order findOrder(id) {
Order order = orderRepository.findOrder(id);
order.getMember().getName(); //프록시 객체를 강제로 초기화한다
return order
}
}
영속성 컨텍스트가 살아있을 때 프레젠테이션 계층에 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다
org.hibernate.Hibernate.initialize(order.getMember());
하이버네이트를 사용하면 initialize() 메소드를 사용해서 프록시를 강제로 초기화할 수 있다
JPA 표준에는 프록시 초기화 메소드가 없고 초기화 여부만 확인할 수 있다
프록시 초기화 역할을 서비스 계층에서 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 한다
이는 프레젠테이션 계층이 서비스 계층을 침범하는 상황으로 서로 논리적으로 의존하게 되며 번거롭고 실수하기 쉽다
'스터디 > JPA' 카테고리의 다른 글
컬렉션과 부가 기능 - (1) (0) | 2025.03.27 |
---|---|
웹 애플리케이션과 영속성 관리 - (2) (0) | 2025.03.21 |
스프링 데이터 JPA - (2) (0) | 2025.02.20 |
스프링 데이터 JPA - (1) (0) | 2025.02.20 |
객체지향 쿼리 언어 - (7) (1) | 2025.02.19 |