스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 트랜잭션과 영속성 컨텍스트를 관리해 주기때문에 개발을 손쉽게 할수 있다. 하지만 내부 동작에 대해 잘 모른채로 개발을 해 발생할수 있는 문제에 대해 다뤄보겠다.
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 사용한다.
→ 트랜잭션 범위 = 영속성 컨텍스트 범위
스프링 프레임워크 사용시 비즈니스 로직을 시작하는 서비스 계층에 @Transcational 어노테이션을 선언해 트랙잭션을 시작한다. 이는 위의 그림의 Controller 와 Service단의 트랜잭션 범위를 의미한다.
트랜잭션과 영속성 컨텍스트는 같은 범위를 유지하며 보통 service 계층과 repository의 계층에 존재한다.
웹계층에서 컨트롤러에 요청을 하는 과정을 살펴보면
☝️ 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용
→ 엔티티메니저가 달라도 같은 트랙잭션 안이라면 같은 영속성 컨텍스트를 공유함
☝️ 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용
보통 트랜잭션은 서비스 계층에서 시작되므로 컨트롤러 계층은 트랜잭션의 범위에서 벗어난다. 따라서 트랜잭션 범위에서 조회된 엔티티는 준영속 상태가 된다.
ex)
class OrderController {
public String view(Long orderId) {
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName()//지연 로딩시 예외 발생
}
}
가장 간단한 방법은 글로벌 페치 전략을 Eager로 변경하면 된다.
@ManyToOne(fetch = FetchType.EAGER)
‼️ 글로벌 페치 전략에 즉시로딩 사용시 단점
사용하지 않는 엔티티 로딩
→ 이는 간단하게 연관된 엔티티가 즉시 로딩이 되면서 사용하지 않더라도 조회가 된다.
N+1 문제 발생
→ JPA가 JPQL을 분석해서 SQL을 사용할때는 글로벌 페치 전략을 참고하지 않고 JPQL 자체만 사용한다. 만약 조회한 order엔티티가 10개면 이름 참조하는 member 도 10개가 조회된다. 따라서 상당히 많은 SQL문이 호출되면서 조회성능에 치명적이다.
글로벌 페치 전략을 즉시로딩으로 설정시 애플리케이션 전체에 영향을 주게 된다.
N+1을 해결하기위해 JPQL만 페치 조인을 사용하게 수정해보자
select o
from Order o
join fetch o.member
‼️ 단점
이는 프리젠테이션 계층과 서비스 계층 사이에 FACADE계층을 추가해 뷰를 위한 프록시 초기화를 담당하는 계층을 만들어준다. → 논리적 의존성 분리 가능
특징
💁 위의 모든 문제들은 결국 엔티티가 프리젠테이션 계층에서 준영속이기에 발생하는 문제들이다.
osiv란 영속성 컨텍스트를 뷰 까지 열어 둔다는 뜻이다.
Member member = memberSerivce.getMember(id);
member.setName("xxx");
model.addAttribute("Member", member);
이를 막기위해 다음과 같은 방법이 있다.
‼️ 이방법들은 코드가 상당히 증가한다는 단점이 있다.
이전 요청당 트랜잭션은 프리젠테이션 계층에서 데이터를 변경할수 있다는 단점이 있었다. 이 방법은 문제들을 어느정도 해결한 방법이다.
동작 원리
특징
‼️ 문제점
Member member = memberService.getMember(id);
member.setName("xxx");
memberService.biz();
위 코드의 문제점은 무엇일까?
비즈니스 계층 트랜잭션의 특징을 살펴보면 영속성 컨텍스트의 생명주기와 트랜잭션의 생명주기가 다르다.
즉 영속성 컨텍스트가 종료되기 이전에 다시 트랜잭션을 살린다면 값이 변경 될수 있다.