출처: https://blog.iroot.kr/341 [RootKR] 출처: https://blog.iroot.kr/341 [RootKR]

스프링이나 J2EE 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 그리고 트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다. 

 

 따라서 조회한 엔티티가 서비스와 레포지토리가 아닌 컨트롤러나 프리젠테이션 계층에서는 준영속 상태가 된다. 즉 트랜잭션이 없는 프리젠테이션 계층에서는 병경 감지와 지연 로딩이 동작하지 않는다. 

@RestController
@RequiredArgsConstructor
public class OrderController {
   private final OrderRepository orderRepository;
  
   public String view(Long orderId) {
       Order order = orderRepository.findOne(orderId);

       // 준 영속상태의 member. 아직 lazy 발동안됨
       Member member = order.getMember();
       member.getName(); // 사용시 Lazy 발동. 예외 발생
      
       return member.toString();
   }
}

 

준영속 상태와 변경 감지

 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않는다. 보통 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생한다. (근데 Spring Boot OSIV에선 컨트롤러까지 영속성 트랜잭션이 살아있다...)

 

 비즈니스 로직은 서비스 계층에서 끝내고 프리젠테이션 계층은 데이터를 보여주는 데 집중해야 한다. 변경 감지 기능이 프리젠테이션 계층에서 동작하면 유지보수가 어려워질 수 있다. 

 

준영속 상태와 지연로딩

 준영속 상태에서 지연 로딩이 동작하지 않아 지연로딩 객체 조회 시 프록시 객체가 사용된다. 프록시는 실제 데이터가 아니가 때문에 프록시 객체를 사용하면 실제 데이터를 불러오려고 초기화를 시도한다. 준영속 상태에선 지연로딩에 문제가 발생하므로, LazyInitialaztionException 예외가 발생한다. 

 

 준영속 상태의 지연로딩 문제를 해결하는 방법은 크게 2가지다. 

  • 뷰가 필요한 엔티티를 미리 로딩해 두는 방법
  • OSIV를 사용해 엔티티를 항상 영속 상태로 유지하는 방법

 뷰가 필요한 엔티티를 미리 로딩하는 방법을 살펴보자. 이 방법은 영속성 컨텍스트가 살아 있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해 반환하는 방법이다. 따라서 엔티티가 준영속 상태로 변해도 연관된 엔티티를 이미 다 로딩해 두어 지연 로딩이 발생하지 않는다. 

 

 뷰가 필요한 엔티티를 미리 로딩해 두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 있다. 

 

1. 글로벌 페치 전략 수정

 가장 간단한 방법은 지연 로딩에서 즉시 로딩으로 변경하는 것이다. 

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "member_id")
private Member member;

 fetch 전략을 EAGER로 하면 다음과 같이 프리젠테이션 계층에서 사용할 수 있다. 

public String view(Long orderId) {
   Order order = orderRepository.findOne(orderId);
  
   Member member = order.getMember();
   member.getName(); // 이미 다 로딩되서 오류 안남

   return member.toString();
}

 위의 방법은 편리하기는 하나 다음 단점이 있다. 

  • 사용하지 않는 엔티티를 로딩한다. 
    • 위 코드에서 만약 나는 Order만 조회하고 싶었는데, 쓸데없는 Member도 조회하는 꼴이 된다. 
  • N+1 문제가 발생한다. 
    • em.find()를 실행(엔티티 매니저)하면 EAGER로 설정 시 SQL은 Order와 Member를 left outer join 해서 한꺼번에 가져온다. 
    • 문제는 jpql에서 발생하는데, JPA가 jpql을 분석해 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 jpql 자체만 사용한다. 
    • 만약 조회한 Order 엔티티가 10개라면 select * from Order 쿼리가 한번 날아가고, Order마다 해당하는 Member를 조회하려 10번의 select * from Member where id = ? 쿼리가 날아가는 문제가 생긴다.
    • 위 문제는 jpql 패치 조인으로 해결할 수 있다. 

2. JPQL 페치 조인

 페치 조인을 사용하면 SQL Join을 사용해 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 해결된다. 하지만 이 역시 단점이 존재한다. 

  • 무분별하게 사용하면 화면에 맞춘 리포지토리에 메서드가 증가할 수 있다. 즉 Order만 필요한 화면이 있고 Order와 Member 둘 다 필요한 화면이 있다고 할 때 api를 Order 전용과 Order를 Member와 페치 조인한 api가 2개나 생겨 버린다. 
  • 실무에서 나도 이 페치 조인 때문에 api 함수 개수가 늘어나 관리가 힘들었다...ㅠㅠ

 

3. 강제로 초기화

 강제로 초기화는 영속서 켄텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해 반환하는 방법이다. 

@Transactional(readOnly = true)
public Order findOrder(Long id) {
   Order order = orderRepository.findOne(id);
   order.getMember().getName();  // getMember()후 그 속성을 읽으면 초기화가 된다. 
  
   return order;
}

 

 글로벌 페치 전략으로 즉시 로딩하면 연관된 엔티티가 실제 엔티티가 아닌 프록시 객체로 조회된다. 프록시 객체는 사용하는 시점(getName())에 초기화된다. 위 예제처럼 프리젠 테이션 계층에 필요한 프록시 객체를 영속성 컨텍스트가 살아있을 때 강제로 초기화해 반환하면 이미 초기화했으므로 준영속 상태에서도 사용할 수 있게 된다. 

 위의 문제는 서비스 계층이 프리젠테이션 계층을 위한 프록시 초기화 역할을 하게 되어 계층을 침범하는 문제가 생기는데, 이는 FACADE 계층이 담당하면 된다. 

 

FACADE 계층 추가

 서비스 계층과 프리젠테이션 계층 사이에 FACADE 계층을 하나 더 두는 방법이다. 뷰를 위한 프록시 초기화는 이곳에서 담당하면 된다. 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 FACADE에서 트랜잭션을 시작해야 한다. 

 

FACADE 계층의 역할과 특징

  • 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리
  • 프리젠테이션 계층에서 필요한 프록시 객체를 초기화함
  • 서비스 계층을 호출해 비즈니스 로직을 실행
  • 리포지토리를 직접 호출해 뷰가 요구하는 엔티티를 찾음
@RequiredArgsConstructor
public class OrderFacade {
   private final OrderService orderService;
  
   public Order findOrder(Long id) {
       Order order = orderService.findOrder(id);
       order.getMember().getName(); // 프록시 초기화
      
       return order;
   }
}

 

 서비스 계층과 프리젠테이션 계층 간 논리적 의존관계를 제거했지만, 실용적 관점에서 볼 때 FACADE의 단점은 중간에 계층이 하나 더 끼어들었다는 점이다. 

 

'개발 이야기 > JPA' 카테고리의 다른 글

[개발이야기 - JPA] OSIV를 알아보자  (0) 2020.05.12
[개발이야기 - JPA] 병합 - merge()  (0) 2020.05.02

+ Recent posts