본문 바로가기
Spring/JPA

OSIV ( Open Session In View ) 에 대해 알아보자.

by YoonJong 2023. 3. 18.
728x90

김영한님의 JPA 활용 2편을 들으면서 OSIV 라는 개념에 대해서 정리한 내용입니다.

 

JPA 를 프로젝트에 적용하고 리팩토링 하는 과정에 들은 강의인데, 이전 학습할 때 한번도 듣지 못한 용어를 보게 되어 더욱 집중해서 보았던 것 같습니다.

현재 리팩토링을 진행하고 있는 프로젝트에서도 API 만 구현하고 있지만 ADMIN 페이지는 아니기 때문에, false 로 변경하였습니다.


스프링부트 프로젝트를 생성하고 바로 실행시키면, 콘솔창에 아래와 같은 WARN 표시가 생성되는 것을 볼 수 있습니다.

해당 표시는 ERROR 표시가 아니면서 실행에 아무런 영향이 없기 때문에 크게 신경쓰지 않고 넘어갔던 것 같습니다.

properties 또는 yml 에서 설정이 가능

먼저, Spring.jpa.open-in-view 는 true , false 로 지정해줄 수 있습니다.

 

OSIV 가 디폴트값으로(true) 되어 있으면 어떤 일이 일어날까요?

true 와 false 의 차이는 영속성 컨텍스트 생존 범위 의 차이가 있습니다.

true 일 때 생존범위

위의 그림을 보면, 영속성 컨텍스트는 요청에서 부터 Repository 끝까지 생존하고 있음을 알 수 있습니다.

쉽게 얘기하면, 요청이 들어오는 순간 생성되고 사용자에게 응답이 나가기 전까지 살아있으며 응답이 나가야 영속성 컨텍스트가 사라집니다.

 

동작과정을 보면 아래와 같습니다.

1. 요청이 들어오면 영속성 컨텍스트를 생성합니다. 트랜잭션은 아직 시작하지 않습니다.

2. 서비스에서 생성되어있던 영속성 컨텍스트를 가지고 트랜잭션을 시작합니다.

3. Repository 에서 영속성 컨텍스트에 flush 를 하고 트랜잭션을 끝냅니다. 

4. 영속성 컨텍스트는 열려있는 상태이므로 뷰나 컨트롤러에서 사용할 수 있습니다.

 

영속성 컨텍스트가 요청부터 응답까지 살아있다는 것은 큰 장점입니다.

지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 DB와 관계를 맺고 있어야 하기 때문입니다.

 

하지만 너무 오래 영속성 컨텍스트를 가지고 있게 되어 커넥션이 부족하게 될 수 있습니다.

이것은 장애로 이루어 질 수 있기 때문에 고려해야할 사항입니다.


OSIV 를 false 로 설정하면 어떻게 될까요?

false 일 때, 생존범위

그림처럼 영속성 컨텍스트는 트랜잭션 범위로 제한되게 됩니다.

모든 지연로딩은 트랜잭션 안에서 처리해야 하는데, 만약 Controller에서 지연로딩 로직이 있다면, 트랜잭션 안에 있는 Service 나 Repository 로 리팩토링이 필요합니다.

 

만약 Controller 에서 지연로딩 로직이 있다면 View 템플릿은 동작하지 않게 됩니다.

따라서, 트랜잭션이 끝나기 전 지연 로딩을 호출을 시켜놔야 합니다.

 

OSIV 설정이 필요한 이유는 무엇일까요?

트래픽이 많은 환경에서는 OSIV 에 관련한 에러가 많이 발생한다고 합니다.

커넥션은 무한하지 않기 때문에, 이를 관리할 수 있는 방법이 필요합니다.

시간이 오래걸리거나 실시간 API 는 오픈 API를 꺼야 에러가 나지 않으며, 사용자가 적은 ADMIN 페이지 같은 경우에는 따로 끄지 않는것이 좋은 방안이라고 합니다.

 

 

제가 들었던 강의에서는 이러한 문제를 해결하기 위해 커멘드와 쿼리를 분리하라고 추천합니다.

 

OSIV 뿐만 아니라 유지보수 관점에 있어서도 충분히 고려할만한 부분입니다.

단순하게 말하면, Controller 와 Service 를 분리하는 것 입니다.

 

예시를 보기 위해 코드를 참고하겠습니다.

현재 yml 에서 jpa.open-in-view: false 로 한 상태입니다.

 

아래의 코드는 강의 한부분입니다.

현재 영속성 컨텍스트의 범위는 트랜잭션 안에서만 생존할 수 있지만, 아래 코드는 Controller 에서 Repository 에 접근하고 있기 때문에 트랜잭션 범위의 밖에서 진행되고 있습니다. 

따라서, 해당 API 를 실행했을 때, 500에러가 발생하며, 프록시를 초기화할 수 없다고 메시지를 보여주게 됩니다.

@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    List<OrderDto> result = orders.stream().map(o -> new OrderDto(o))
            .collect(toList());

    return result;
}

 

이제 컨트롤러와 서비스를 분리해보겠습니다.

리포지토리에서 구현해도 되지만, 컨트롤러-서비스-리포지토리 구성이 가장 효율적이라고 생각해 서비스 클래스를 따로 생성했습니다.

@RestController
@RequiredArgsConstructor
public class OrderApiController {

 	private final OrderServiceTest orderServiceTest;

	@GetMapping("/api/v2/orders")
	public List<OrderDto> ordersV2() {
   	List<OrderDto> result = orderServiceTest.orderSerchTest();
   	return result;
	}
}


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

    private final OrderRepository orderRepository;

    public List<OrderApiController.OrderDto> orderSerchTest() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<OrderApiController.OrderDto> result = orders.stream().map(o -> new OrderApiController.OrderDto(o))
                .collect(toList());

        return result;
    }
}

위에는 컨트롤러이며 , 아래는 서비스입니다.

서비스 부분을보면 트랜잭션 어노테이션을 부여해서 지연로딩에 대한 문제를 해결해주었습니다.

이제 트랜잭션안에서 필요한 데이터들을 로딩해주었습니다.

 

다시 한번 API를 호출해보면 정상적으로 작동되는 것을 볼 수 있습니다.

728x90

댓글