지금까지 스타디를 진행하며, JPA를 스프링,J2EE(J2EE는 자바 기술로 기업환경의 어플리케이션을 만드는데 필요한 스펙들을 모아둔 스펙 집합) 환경에서 동작 보다는 순수 JPA 자체를 학습 했습니다.
이번 시간에는 스프링이나 J2EE환경에서 JPA를 사용하며 컨테이너 환경에서의 트랜잭션, 영속성 컨텍스트를 관리 하는 동작과 연관지어 컨터이너 환경에서의 JPA의 내부동작과 집중적으로 준영속 상태와 지연 로딩에 대해 알아보겠습니다.
스프링 컨테이너에서의 JPA의 사용에 대해 알기전에 먼저 간단하게 기억 해야 할 사항을 정리하겠습니다.
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 사용합니다.
즉 트랜잭션 범위와 컨텍스트 생존 범위가 같다는 말입니다.
트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다. 반대로 말하면 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
앞에서 말한 바와 같이 보통 트랜잭션은 서비스 계층에서 시작하고 종료 되는 시점에 트랜잭션이 종료되므로 영속성 컨텍스트도 함께 종료됩니다.
즉 조회한 엔티티가 서비스, 리포지토리 계층에서는 영속 상태이지만 뷰,컨트롤러(프레젠테이션 계층)에서 준영속 상태가 됩니다.
class Controller {
public String view(Long id){
여기서 만약 프록시 객체를 초기화 할 시 문제 발생
즉 지연 로딩 시 예외 발생
}
}
위와 같이 만약 컨트롤러에서 즉 트랜잭션이 없는 계층에서는 준영속 상태이므로 당연히 지연 로딩, 변경 감지 기능이 동작하지 않습니다. 그렇기 때문에 위코드는 예외가 터집니다.
여기서 잠깐 변경 감지에 대해 말하자면 위의 내용과는 별개로 단순히 데이터를 보여주는 프레젠테이션 계층에서 변경 감지에 의한 데이터가 수정되는 것은 불필요합니다.
오히려 프레젠테이션 계층에서 수정이 일어난다면 유지보수나 디버깅 측면서에서 매우 불리할겁니다.
비지니스 로직은 서비스 계층에서 끝내고 단순 컨트롤러에서는 데이터를 보여주는 일에 최선을 다하는 방향에서 생각해 봤을때도 변경 감지는 프레젠테이션 계층에서 동작하지 않는 것은 크게 문제가 되지않습니다.
다시 돌아와 트랜잭션이 없는 프레젠테이션 계층에서 준영속 상태의 엔티티를 지연 로딩 할수 없기 때문에 뷰를 랜더링할 때 엔티티를 사용하면 예외가 발생합니다.
이처럼 준영속 상태의 지연 로딩 문제를 해결하는 방법이 2가지가 있습니다.
먼저 뷰가 필요한 엔티티를 미리 로딩하는 방법에 대해 알아보겠습니다.
이 방법은 영속성 컨텍스트가 생존해 있는 즉 영속상태에 있는 서비스, 리포지토리 계층에서 미리 프레젠테이션 계층에 필요한 데이터를 미리 로딩하거나 지연 로딩시 프록시 객체를 미리 초기화 해서 반환하는 방법입니다. 따라서 이 방법을 사용시 프로젠테이션 계층에서 엔티티가 준영속 상태가 되어도 이미 프록시 객체가 초기화 되어 지연 로딩시 문제가 발생하지 않습니다.
뷰가 필요한 엔티티를 미리 로딩하는 3가지 방법.
가장 간단한 방법입니다. fetch 전략을 LAZY에서 -> EAAGER로 바꾸는 전략입니다.
즉 즉시 로딩으로 바꾸는 전략입니다.
이런 전략을 취할시 엔티티를 조회시 연관 엔티티도 항상 함께 로딩 됩니다.
즉 미리 다 로딩하기 때문에 프록시를 사용하지않고 준영속 상태에서도 엔티티를 모두 사용가능합니다.
하지만 큰 문제점이 두가지 있습니다.
당연하게도 필요없는 엔티티를 가져오기 때문에 자원 낭비가 존재합니다.
사실 즉시로딩을 사용한다고 해서 큰 문제가 있는것은 아니지만 보통 필수적으로 사용하는 Spring data jpa를 사용 할 경우 data jpa의 쿼리 메서드기능을 많이 사용할 겁니다. 이 때 쿼리메서드는 JPQL을 생성해 동작하기 때문에 이 때 문제가 발생합니다. 엄밀히 만하면 JPQL을 사용시 문제가 발생합니다.
List<Order> orders =
em.createQuery = "select o from Order o ", Order.class)
.getResultList();
우외 같이 JPQL을 사용시 JPA가 JPQL을 분석해 sql문을 생성하게되고 이는 글로패치전략(지연,즉시)인지 구분하지 않고 JPQL만 참고해 사용합니다.
order가 member와 관계가 있을시 order 앤티티 10개를 조회하면 member도 10번 조회하는 sql 문이 나갑니다.
즉 select * from Order //JPQL로 실행된 sql문이 한번 나갈 시
10번의 member를 select 하는 sql문이 나갑니다.
이처럼 N+1문제가 생깁니다.
위의 즉시로딩 문제를 해결 하는 방법으로 페치조인을 하는 방법이있습니다.
select o from Order o
select o from Order o joint fetch o.member
첫 번째 분장을 두번째와 같이 바꿉니다. 이경우 실제 sql문에 join이 나가게되고
(order를 조회하며 member에 join을 겁니다)
join 대상을 함께 조회하기 때문에 N+1문제가 발생하지않습니다.
정리하자면 JPQL의 fech join 기능을써서 글로벌 패치 전략과 무관하게 필요한 엔티티를 가져옵니다.
하지만 이방법도 문제가 존재합니다. 뷰를 위해 리포지토리가 논리적 의존 관계가 생길수 있습니다. 뷰의 데이터 최적화를 위해 의존성이 생길수있습니다.
프레젠테이션 계층에 필요한 엔티티를 강제로 초기화합니다. 즉 서비스, 리포지토리 레이어에서 지연 로딩을 사용시 프록시를 강제로 초기화해 return합니다.
이럴시 프레젠테이션 계층의 준영속 상태에서도 엔티티를 사용 가능합니다.
하지만 이방법또한 서비스 계층에서 뷰에 필요한 엔티티를 위해 프록시를 초기화 하는 로직이 들어가게되고 이또한 의존성이 생겨 좋지않습니다. 따라서 서비스 계층은 비지니스 로직만 담당하고 프록시 초기화를 해주는 계층이 필요합니다.
이를 위해 해결책이 FACADE계층 입니다.
이 처럼 프록시 초기화를 담당하는 계층을 하나 두는 방식입니다. 주의할 점은 프록시 초기화를 위한 영속성 컨텍스트가 필요하므로 트랜잭션을 FACADE에서 시작합니다.
분리하는 점은 좋지만 단순 초기화를 위해 서비스 계층에서 FACADE에게 위임하는 코드가 많고 이는 많은 코드를 유발할 것입니다.
뷰에 필요한 데이터를 미리 가져오는 방법들에 대해 알아봤습니다. 하지만 사실 위의 3가지 방법들은 각 문제점들 때문에 사실 사용하기는 번거롭습니다. 그렇다면 최종적인 해결책은 무엇인가? 사실 지금까지 프레젠테이션에서 엔티티의 프록시 객체 초기화가 가장큰 문제였습니다.
그렇기 때문에 뷰에 보여질 데이터를 만드는 것에 어려움을 겪었던 것이고요. 이문제를 해결 하기위해서는 사실 심플합니다. 프레젠테이션의 준영속 상태를 영속 상태로 바꿔주는 것입니다! 즉 영속성 컨텍스트의 생존범위를 프레젠테이션 까지 늘려주는 것입니다. 그렇다면 우리는 지연로딩을 뷰에서도 사용할 수 있게 됩니다. 이것을 가능하게 하는 기술이 osiv입니다.
프레젠테이션 계층에서 엔티티를 지연 로딩하며 조회하기 할 때 osiv를 사용하는 방법이 있습니다.
하지만 이또한 마법은 아니고 OSIV,FACADE,DTO등 사용자에게 뷰를 노출 할 때 각각 장점을 살려 사용하는게 좋습니다.
물론 DTO나,FACADE의 지루한 코드를 반복하는 경우도 있지만 복잡한 통계형 데이터를 노출시에는 JPQL로 통계형쿼리를 조회해 DTO로 만들어 반환하는 것이 더 나은 해결 책일수 있습니다.
또한 엔티티를 직접 프레젠테이션에 노출 시키는 방법은 좋지 않은 방법이기 때문에 내부 API에서는 괜찮은 방법일 수 있지만. 외부 클라이언트에 노출 시키는 방법은 좋지 않을 수 있습니다.