소스
https://github.com/rkwhr0010/jpa
스프링에서 JPA를 사용하면 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해준다.
컨테이너 환경에서 JPA가 동작하는 내부 동작 방식을 이해해야 문제 발생 시 해결할 수 있다.
트랜잭션 범위의 영속성 컨텍스트
스프링 컨테이너의 기본 전략
트랜잭션 범위의 영속성 컨텍스트 전략이 기본
영속성 컨텍스트 생존 범위 == 트랜잭션 범위
트랜잭션 안에서 런타임 예외 발생으로 롤백 시 트랜잭션은 커밋을 하지 않는다.
이때 영속성 컨텍스트도 플러시가 되지 않아 DB에 변경사항이 반영되지 않는다.
- 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
엔티티 매니저가 달라도 같은 영속성 컨텍스트를 사용한다 - 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다
같은 엔티티 매니저를 사용해도 트랜잭션이 다르면 영속성 컨텍스트도 다르다
준영속 상태와 지연 로딩
대부분 서비스 계층에서 트랜잭션을 시작하므로 컨트롤러 계층에서는 엔티티는 준영속 상태가 된다.
따라서 지연로딩 전략을 취하면, 컨트롤러에서 처음으로 준영속 상태 엔티티 값을 꺼낼 때 예외가 발생하게 된다.
지연로딩이 아닌 준영속 엔티티를 컨트롤러에서 조작을 해도 변경감지 기능이 동작하지 않는다.
준영속 상태 지연 로딩 문제 해결법
- 엔티티를 미리 로딩하기
- OSIV 사용해서 엔티티를 영속 상태로 유지하기
엔티티 미리 로딩하기
- 글로벌 패치 전략 변경
- JPQL 페치 조인
- 강제로 초기화
글로벌 패치 전략 즉시 로딩 단점
- 사용하지 않는 엔티티를 로딩
- N+1 문제가 발생
직접이던, 스프링 데이터 JPA가 생성해 주던 JPQL로 조회를 할 때 엔티티를 결과로 받는다면, 즉시로딩의 경우 연관된 엔티티를 전부 조회하게 된다.
위 예제의 경우 Member 5번, Product 5번의 추가 쿼리가 수행된다.
JPQL 페치 조인
글로벌 페치 조인은 애플리케이션 전역에 영향을 주기에 적절한 방법이 아니다.
페치 조인을 하게 되면 N+1 문제도 발생하지 않는다.
조인을 사용하기 때문에 SQL 한 번으로 다 조회하기 때문이다.
N+1 문제를 해결할 가장 현실적인 방법
JPQL 페치 조인 단점
가장 간편하고 현실적인 N+1 해결 방법이기에 너무 무분별하게 사용할 수가 있다.
프리젠테이션 계층에서 데이터를 보여줄 목적으로 페치조인을 사용한다는 것부터가 프리젠테이션 계층이 데이터 접근 계층에 직접적으로 의존하게 된다.
보여줄 화면 마다 페치 조인을 사용하게 된다면, 리포지토리에 많은 페치조인 메소드가 증가할 수 있다.
강제로 초기화
프리젠테이션 계층에 넘기기 전에 프록시 객체를 강제로 초기화하는 것을 말한다.
말은 거창하지만, 프록시 객체에 어느 메서드나 한 번 호출하면 초기화가 이루어지므로 아무 게터 메서드나 호출한 후 엔티티를 반환하면 된다.
프리젠테이션에서 보여줄 데이터 때문에 이렇게 엔티티를 미리 로딩한다는 것은 서비스 계층이 프리젠테이션 계층에 의존하는 것과 같다. 프리젠테이션 계층의 변화가 서비스 계층에 영향을 미치게 된다.
그래서 사이에 FACADE 계층을 추가해 이 일을 위임한다.
FACADE 계층
프리젠테이션 계층과 서비스 계층 사이에 추가 계층을 두어 뷰를 위한 프록시 초기화만 담당한다.
서비스 계층은 더 이상 뷰에서 사용할 프록시 객체를 초기화하지 않아도 된다. 즉, 의존성이 제거된다.
준영속 상태와 지연 로딩의 문제점
뷰를 개발할 때마다 필요한 엔티티를 초기화하는 것은 오류가 발생할 가능성이 높다.
아무리 중간에 FACADE 같은 별도의 계층으로 처리를 한다고 해도 초기화를 빼먹을 수도 있다.
또한 FACADE로 코드 레벨의 물리적인 의존성은 제거했지만, 논리적인 의존성을 제거하지 못한다. 즉, 완전한 해결책이 되지 못한다.
초기화 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 뷰에서도 지연 로딩을 가능하게 할 수 있는데 이를 OSIV라 한다
OSIV
Open Session In View
여기서 세션은 영속성 컨텍스트를 의미한다.
OSIV 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다.
OSIV 요청 당 트랜잭션
요청이 들어오면, 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서 트랜잭션을 시작하고 요청이 끝나면 트랜잭션과 영속성 컨텍스트를 종료한다.
요청 당 트랜잭션 방식 OSIV 문제점
컨트롤러나 뷰에서 엔티티를 변경할 수 있다.
예를 들어 뷰에서 개인정보를 마스킹해서 보여줘야해서 컨트롤러 계층에서 엔티티에 마스킹 처리를 하면, 실제로 마스킹된 결과가 DB에 반영된다.
프리젠테이션 계층에서 엔티티를 수정못하게 하는 방법
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 레핑
- DTO만 반환
엔티티를 읽기 전용 인터페이스로 제공
엔티티 래핑
읽기 메서드만 있는 메서드를 정의한 클래스로 엔티티를 감싼다.
DTO만 반환
엔티티 대신 별도 데이터 전용 DTO를 정의하고, DTO를 생성해 반환한다.
위 세 가지 방법은 전부 코드량이 증가하고, 번거롭다는 단점이 있다.
그래서 요청 당 트랜잭션 방식 OSIV는 잘 사용하지 않는다.
스프링 OSIV: 비즈니스 계층 트랜잭션
스프링 프레임워크가 제공하는 OSIV
스프링 프레임워크가 제공하는 OSIV 라이브러리
서블릿 필터, 스프링 인터셉터 어디서 적용할지에 따라 필요한 클래스가 달라진다.
- 하이버네이트
- 스프링
OSIV 는 하이버네이트에서 부르는 명칭
JPA 정식 표현은 OEIV
OSIV == OEIV는 같은 뜻이다.
스프링 OSIV 분석
요청 당 트랜잭션 방식의 OSIV는 프리젠테이션 계층에서 데이터 변경을 막을 수 없다는 치명적인 문제가 있다.
스프링 프레임워크가 제공하는 OSIV는 이 문제를 어느 정도 해결했다.
- 요청이 들어오면 설정한 클래스에 따라 필터나, 인터셉터에서 영속성 컨텍스트를 생성한다.
- 서비스 계층에서 생성된 영속성 컨텍스트에 트랜잭션을 시작한다.
@Transactional 이 붙은 클래스에 한함
- 서비스 계층 작업이 끝나면, 트랜잭션을 커밋하고, 영속성 컨텍스트를 플러시 한다.
- 필터나, 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 하지 않는다.
최종적으로 프리젠테이션 계층인 컨트롤러, 뷰에서 영속성 컨텍스트가 살아있고, 이후 영속성 컨텍스트가 소멸될 때 플러시를 호출하지 않으므로 지연로딩 문제와 엔티티 수정 문제에서 자유로워 진다.
트랜잭션 없이 읽기
영속성 컨텍스트 속 모든 변경 사항은 트랜잭션 안에서 이뤄져야 한다.
트랜잭션 종료 후 플러시를 하면 jakarta.persistence.TransactionRequiredException 예외가 발생한다.
만약 엔티티 변경이 없이 단순히 조회만 한다면 트랜잭션이 필요없다. 이를 트랜잭션 없이 읽기라 한다.
스프링이 제공하는 OSIV 영속성 컨텍스트가 프리젠테이션 계층까지 유지된다. 단, 트랜잭션은 종료된 상태다. 즉, 트랜잭션 없이 읽기가 가능한 상태다.
강제로 플러시를 해봤자 예외만 발생한다.
스프링 OSIV 주의사항
서비스 계층를 통해 조회해온 엔티티를 수정하고, 다른 서비스 계층 레이어를 조회하면, 문제가 된다.
영속성 컨텍스트는 살아 있고, 트랜잭션이 종료됐는데 다시 트랜잭션이 시작된다. 다시 시작된 트랜잭션이 종료될 때, 커밋이 이루어지며 플러시가 발생한다. 이전에 안전하다고 생각했던 엔티티 변경 사항이 DB에 반영된다.
일반적인 해결책은 트랜잭션이 걸린 비즈니스 로직을 모두 호출 후 뷰에 보여줄 엔티티를 수정하는 것이다.
스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있어 이런 문제가 발생한다.
OSIV 정리
- 스프링 OSIV 특징
영속성 컨텍스트는 요청 시작부터 종료까지 유지된다.
이때 트랜잭션은 @Transactional이 붙은 서비스 레이어를 호출할 때 시작하고, 종료된다. 즉, 생명주기가 더 짧다.
영속성 컨텍스트 종료 시 플러시를 호출하지 않으므로 프리젠테이션 레이어에서 영속 상태 엔티티를 수정해도 DB에 반영되지 않는다.
- 스프링 OSIV 단점
하나의 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다.
프리젠테이션 계층에서 의도하지 않은 플러시가 발생할 수 있다.
- 그럼에도 OSIV
FACADE, DTO 등을 사용하면 결국 신규로 작성해야하는 코드가 많아진다.
프록시의 경우 강제로 초기화해서 넘겨야하는데 빼먹는 다면 예외가 발생한다.
- OSIV는 만능이 아니다
프리젠테이션 계층에서 출력할 엔티티를 자유롭게 사용할 수 있지만, 복잡한 뷰에선 엔티티 조회가 아닌 통계를 위한 별도 JPQL이나 SQL로 조회하는 것이 효과적이다. 이땐 DTO를 사용하게 된다
- OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다
외부에서 호출하는 클라이언트의 경우 결국 JSON이나 XML로 전송해야한다.
이 경우 엔티티를 JSON 시켜 외부 API로 노출해야 한다.
내부 API 경우 클라이언트 - 서버를 전부 제어가능하지만 외부 API는 불가능하다
그래서 엔티티 정의를 바꾸기 어려워 진다.
엔티티는 생각보다 자주 변경되므로 외부 API의 경우 엔티티를 직접 JSON으로 변환시키보다 중간 완충제로 DTO를 두어 노출하는 것이 안전하다.
너무 엄격한 계층
OSIV를 사용하기 전엔 프리젠테이션 계층에서 지연 로딩된 엔티티를 여러 방법을 통해 미리 초기화해야 했다.
OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션까지 살아있으므로 지연 로딩 엔티티를 초기화할 필요가 없다. 따라서 단순 조회용 엔티티의 경우 굳이 서비스 레이어를 거치지 않고 리포지토리에 직접 접근해 엔티티를 조회해도 문제가 없다.
정리
스프링 JPA를 사용하면 트랜잭션 범위의 영속성 컨텍스트 전략이 기본 적용된다.
같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
이 전략의 단점은 프리젠테이션 계층에서 준영속 엔티티를 다루게 되어 지연 로딩 엔티티를 사용하지 못한다.
OSIV로 이런 문제를 해결할 수 있다.
'개발 > JPA' 카테고리의 다른 글
12 스프링 데이터 JPA (1) | 2024.06.03 |
---|---|
10 객체지향 쿼리 언어 (0) | 2024.05.27 |
09 값 타입 (0) | 2024.05.20 |
08 프록시와 연관관계 관리 (1) | 2024.05.13 |
07 고급 매핑 (0) | 2024.05.06 |