spring/JPA

[JPA] OSIV와 성능 최적화 (Open Session in View란?)

대기업 가고 싶은 공돌이 2024. 8. 27. 03:27

OSIV(Open Session In View) 란?

OSIV란 영속성 컨텍스트를 View 영역까지 열어두는 기능이다.

즉, View 레이어에서도 지연로딩과 같은 영속성 컨텍스트의 특징을 사용할 수 있다.

 

Spring boot에서 OSIV는 default가 true기 때문에 따로 설정을 바꿔주지 않는 한 항상 실행된다.

 

데이터베이스 커넥션

JPA는 언제 데이터베이스 커넥션을 가져오고 언제 커넥션을 반환할까?

 

JPA에서 영속성 컨텍스트를 사용하기 위해선 당연히 데이터베이스 커넥션을 사용해야한다.

 

기본적으로 데이터베이스 트랜잭션을 시작할 때 영속성 컨텍스트가 데이터베이스 커넥션을 가져온다.

다음과 같이 보통 Service 단에서 트랜잭션이 시작하니 위의 트랜잭션 코드가 실행될 때 데이터베이스 커넥션이 만들어지는 것이다.

 

 

그렇다면 데이터베이스 커넥션은 언제 DB에 반환될까?

 

OSIV가 true로 설정돼있으면 이 데이터베이스 커넥션은 한 트랜잭션이 모두 끝나고 컨트롤러에서 코드를 돌 때까지 반환이 되지 않는다.

 

그 이유는 api를 살펴봤을 때 만약 객체가 레이지 로딩이라면 객체에 접근 할 때마다
영속성 컨텍스트에서 프록시 객체를 초기화 시키고 데이터를 가져와야하니 데이터베이스 커넥션을 반환하지 않는 것이다.

 

api가 호출 됐다면 api가 반활될 때까지, 화면인 경우엔 뷰 템플릿이 렌더링 될 때까지 데이터베이스 커넥션이 반환되지 않는다.

 

하지만 이 전략엔 치명적인 단점이 있다.

 

OSIV 단점 

너무 오랜 시간 데이터베이스 커넥션을 물고있기 때문에, 실시간 트래픽이 중요한 서비스에선

데이터베이스 커넥션을 할당받지 못해 장애로 이어지는 것이다.

 

극단적인 예로 api에서 또 다른 외부 api를 호출할 때 외부 api가 3초가 걸린다고 하면

데이터베이스 커넥션은 그 3초가 넘는 시간 동안 해당 호출에 묶여있는 것이다.

 

OSIV를 OFF 하면?

OSIV를 off로 설정하면 딱 트랜잭션의 주기 동안만 데이터베이스 커넥션이 유지가 되고,

영속성 컨텍스트도 트랜잭션 동안만 살아있는다.

 

데이터베이스 커넥션이 짧은 시간 동안만 유지된다는 장점으로도 이어질 수 있겠지만,

컨트롤러 단에서 지연로딩을 사용하지 못 하니, 한 트랜잭션안에 모든 지연로딩 로직을 다 

밀어 넣어야 한다는 단점이 있다.

 

OSIV를 false로 바꾸고 api에서 지연로딩을 실행하면 다음과 같이 no session 즉, 연결이 없다는 에러를 볼 수 있다.

 

해결 방법은 트랜잭션 안에서 미리 lazy 로딩 대상을 다 초기화를 시켜 두거나,

fetch join을 사용해서 미리 다 로딩을 해놓거나 두 가지 방법이 있다.

 

해결 방법

@GetMapping("/api/v3")
public List<OrderDto> ordersV3(){
	List<Order> orders = orderRepository.findAllWithItem();
    
    List<OrderDto> result = orders.stream().map(o -> new OrderDto(o)).collect(toList());
    
    return result;
}

예를 들어 위와 같은 api가 있다고 해보자.

 

여기서 문제가 되는 것은 orderRepository에서 order를 가져온 후 

result를 채워 넣을 때 lazy 로딩이 발생한다는 것이다. 

 

이를 해결하기 위해 

	List<Order> orders = orderRepository.findAllWithItem();
    
    List<OrderDto> result = orders.stream().map(o -> new OrderDto(o)).collect(toList());
    
    return result;

해당 코드를 다른 Query용 Service를 만들어 해당 서비스 안에 전부 넣어버리는 것이다.

 

Service단에 트랜잭션을 걸고 위의 코드를 전부 집어넣은 메소드를 하나 만든 후

 

이렇게 api를 수정해주면 간단하게 문제를 해결할 수 있다.

 

커맨드와 쿼리를 분리하자

보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다.

 

그런데 복잡한 화면을 출력하기 위한 조회 쿼리는 성능을 최적화 하는 것이 중요하다.

하지만 복잡성에 비해 핵심 비즈니스에 큰 영향을 주지는 않기 때문에,

 

이 둘의 관심사를 분리하는 선택은 유지보수 관점에서 충분히 의미있다.

 

단순하게 설명해서 다음처럼 분리하는 것이다.

 

  • OrderService
    • OrderService: 핵심 비즈니스 로직
    • OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션)
  • 애플리케이션이 작다면 OrderSerivce 하나만 유지하고, 크다면 분리하는 것이 좋다.

결론

고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는

OSIV를 켠다.

 

참고: 김영한 실전! 스프링 부트와 JPA 활용2