JPA(Java Persistence API)

[JPA] OSIV (Open Session In View)

cwchoiit 2023. 11. 16. 09:56
728x90
반응형
SMALL

JPA를 사용할 때 "Open Session In View"라는 게 있다. 이 녀석이 은근 골치가 아픈 녀석인데 Spring Boot와 JPA를 같이 사용할 때 서버를 실행해 보면 (OSIV 관련 어떠한 작업도 하지 않았을 때) 이런 경고 문구가 노출된다.

2023-11-16T08:31:26.467+09:00  WARN 76673 --- [  restartedMain] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning

 

그러니까 이 옵션은 기본값이 "true"인데 얘가 true이면 화면이 렌더링되는 시점에도 데이터베이스에 쿼리가 날아갈 수 있다는 경고 문구를 날리고 있다. 이 속성값은 application.yml 파일에서 spring.jpa.open-in-view 키로 값을 줄 수 있다.

 

728x90
반응형
SMALL

이 OSIV가 그래서 정확히 어떻게 동작을 하느냐? 아래 컨트롤러 코드를 보자.

@GetMapping("members")
public Result<List<MemberDto>> members() {
    List<Member> findMembers = memberService.findMembers();

    List<MemberDto> collect = findMembers
            .stream()
            .map(m -> new MemberDto(m.getUsername()))
            .toList();

    return new Result<>(collect.size(), collect);
}

 

코드를 보면 memberService.findMember()를 호출하고 있다. 이 MemberService를 들어가 보면 이렇게 생겼다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {

    private final MemberRepository memberRepository;

	public List<Member> findMembers() {
        return memberRepository.findAll();
    }
}

 

클래스 레벨에 @Transactional() 어노테이션이 달려있고 findMembers() 메소드는 데이터베이스에서 멤버를 전부 가져온다. 이때 OSIV 속성이 true라면 이 클래스의 메소드가 시작하고 끝나는 시점에 트랜잭션이 커밋이 되거나 영속 컨텍스트가 닫히지 않는다. 이 메소드를 호출한 컨트롤러도 트랜잭션 안에서 동작하게 된다. 이게 OSIV 속성이 true일 때 일어나는 현상이다.

 

즉, 트랜잭션이 활성화된 메서드가 끝나도 트랜잭션은 닫히지 않고 그 메서드를 호출한 녀석도 역시 트랜잭션이 활성화된 상태로 남아있는다. 그렇기 때문에 위 경고 문구처럼 화면을 뿌리는 컨트롤러가 화면을 렌더링 하는 시점에도 역시 데이터베이스 쿼리가 날아갈 수 있다는 경고 문구를 하고 있는 것.

 

그럼 OSIV 속성은 항상 false로 설정해야 할까?

 

OSIV가 활성화된 상태일 때 장단점

장점은 다음과 같다.

  • 트랜잭션 어노테이션이 붙어 있는 곳이 아니더라도 그것을 호출한 곳도 트랜잭션이 활성화 된 상태이다 보니 지연로딩을 초기화하는데 좀 더 유연하다. 즉, 서비스에서 데이터베이스 쿼리를 하고 받은 데이터를 가지고 컨트롤러에서 지연 로딩 관련 데이터를 초기화할 수 있다는 얘기다.

단점은 다음과 같다.

  • 트랜잭션이 예상하는 것보다 오래 살아 있기 때문에 네트워크 상황에 따라 트랜잭션이 부족할 수 있다. 즉, 컨트롤러에서 서비스를 호출해서 데이터베이스에 쿼리를 했고 받은 데이터를 컨트롤러가 받을 때 네트워크 상황이 좋지 않아 컨트롤러가 재빨리 응답을 줄 수 없는 경우 그 시간 동안 내내 트랜잭션은 유효하게 된다. 그러면 트래픽이 많은 애플리케이션은 이러한 경우 때문에 커넥션풀이 부족할 수 있고 부족하게 되는 경우 장애로 이어질 수 있다.

OSIV가 비활성화된 상태일 때 장단점

장점은 다음과 같다.

  • 트랜잭션이 시작하고 끝나는 시점이 명확하고 짧아지기 때문에 커넥션 풀에 여유가 생긴다.

단점은 다음과 같다.

  • 트랜잭션 하나가 영속 컨텍스트 하나와 매핑되기 때문에 트랜잭션이 끝나는 동시에 지연로딩을 초기화할 수 없고 그렇게 되면 지연 로딩으로 데이터를 가져올 때 언제나 트랜잭션 안에서 모든 지연로딩을 초기화한 상태로 돌려주어야 한다.

 

OSIV를 비활성화했을 때 단점을 극복하는 방법

만약, OSIV를 비활성화한 경우엔 모든 지연 로딩 엔티티를 트랜잭션 안에서 처리해야 하므로 다음과 같이 해결할 수 있을 것이다.

  • 페치 조인을 사용해 데이터를 쿼리 하는 시점에 모두 받아온다.
  • 페치 조인으로 모두 받아오지 못할 경우 지연 로딩을 초기화하는 새로운 트랜잭션 안에서 처리한다.
  • 하나의 트랜잭션에서 모든 지연 로딩 엔티티를 초기화한다.

 

페치 조인으로 모든 데이터를 전부 한 번에 로딩

@GetMapping("orders")
public List<OrderDto> orders() {
    List<Order> orders = orderRepository.findAllWithItem();
    return orders.stream().map(OrderDto::new).toList();
}

 

위 코드는 컨트롤러에서 주문 정보를 모두 가져와서 DTO로 반환 후 JSON으로 데이터를 돌려준다. 여기서 OrderDto 클래스는 지연로딩 엔티티를 받아 처리한다. 다음 코드를 보자.

@Data
static class OrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemDto> orderItems;

    public OrderDto(Order order) {
        orderId = order.getId();
        name = order.getMember().getUsername();
        orderDate = order.getOrderDate();
        orderStatus = order.getOrderStatus();
        address = order.getDelivery().getAddress();

        orderItems = order.getOrderItems().stream().map(OrderItemDto::new).toList();
    }
}

@Data
static class OrderItemDto {

    private String itemName;
    private int orderPrice;
    private int count;

    public OrderItemDto(OrderItem orderItem) {
        itemName = orderItem.getItem().getName();
        orderPrice = orderItem.getOrderPrice();
        count = orderItem.getCount();
    }
}

 

OrderDto의 생성자에서 OrderItems를 처리한다. OrderItem은 지연 로딩 엔티티이다. 그래서 OSIV가 비활성화된 경우 위 컨트롤러에서 지연로딩을 처리할 수 없는 상태이다. 그러나, 페치 조인으로 지연 로딩 데이터를 모두 한 번의 쿼리로 로딩해 온다면 가능하다.

public List<Order> findAllWithItem() {
    return em.createQuery("SELECT DISTINCT o " +
            "FROM Order o " +
            "JOIN FETCH o.member m " +
            "JOIN FETCH o.delivery d " +
            "JOIN FETCH o.orderItems oi " +
            "JOIN FETCH oi.item i", Order.class).getResultList();
}

 

 

지연 로딩을 초기화하는 새로운 트랜잭션에서 처리

다른 방법으로는 새로운 트랜잭션을 받아오면 된다. 지금 문제가 되는 부분은 레포지토리에서 받아온 주문 데이터들을 DTO로 변환하는 이 코드이다.

return orders.stream().map(OrderDto::new).toList();

 

이 부분을 처리할 때 DTO 클래스가 생성자에서 지연 로딩 엔티티를 처리하기 때문에 문제가 발생하는데 이 코드 처리를 새로운 트랜잭션에서 하면 된다.

package com.example.jpabook.service;

import com.example.jpabook.domain.Order;
import com.example.jpabook.domain.OrderDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
public class OrderQueryService {

    public List<OrderDto> transferOrderDto(List<Order> orders) {
        return orders.stream().map(OrderDto::new).toList();
    }
}

 

새로운 트랜잭션을 가지는 OrderQueryService 클래스를 가지고 처리하는 로직을 둔다. 그리고 이 클래스의 메서드 transferOrderDto()를 컨트롤러에서 호출 후 반환한다.

@GetMapping("orders")
public List<OrderDto> orders() {
    List<Order> orders = orderRepository.findAllWithItem();
    return orderQueryService.transferOrderDto(orders);
}

 

 

하나의 트랜잭션에서 모든 것을 처리

즉, 서비스 또는 레포지토리에서 모든것을 다 처리한 후 컨트롤러는 받기만 하고 돌려주는 것을 말한다.

나는 비즈니스 로직을 레포지토리에 넣는 것을 매우 싫어하므로 (그래서도 안되고 항상 참조 방향은 다음과 같아야 한다).

컨트롤러 -> 서비스 -> 레포지토리, 엔티티 

역방향이 되면 안 된다. 유지보수에 굉장히 큰 타격을 주므로. 

아무튼 지금 코드에서 레포지토리에서 모든 로직을 넣지 않을 것이다. 근데 만약 서비스를 호출해서 처리하는 경우라면 서비스 안에서 DTO로 변환까지 같이 해준 데이터를 컨트롤러가 받으면 된다.

 

 

결론

OSIV를 활성화 또는 비활성화는 Trade-off가 있다. 그래서 뭐가 더 좋고 나쁘다가 아니라 상황에 맞게 사용하면 된다. 예를 들어 트래픽이 많이 필요 없는 애플리케이션은 활성화 상태로 두면 코드의 복잡도가 떨어지는 장점을 가지면서 애플리케이션에 장애 없이 서비스를 할 수 있을 것이다. 그러나 트래픽이 많은 애플리케이션은 이 옵션을 활성화해서는 안된다. 트랜잭션이 부족할 가능성이 농후하기 때문에. 따라서 상황에 맞게 사용하는 것이 중요하다고 본다.

 

728x90
반응형
LIST

'JPA(Java Persistence API)' 카테고리의 다른 글

[JPA] 페치 조인의 한계와 한계 돌파  (0) 2023.11.15
[JPA] 변경감지와 Merge  (0) 2023.11.12
[JPA] Part 18. 벌크 연산  (0) 2023.10.30
[JPA] Part 17. Named Query  (2) 2023.10.30
[JPA] Part 16. Fetch JOIN (JPQL)  (0) 2023.10.30