OSIV
의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다. 가장 단순한 구현 방법은 클라이언트의 요청이 들어오자마자 서블릿 필터나 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다. 이것을 요청당 트랜잭션 방식의 OSIV
이라고 한다.
많은 사용자들이 사용하는 서비스에서 요청당 트랜잭션 방식의 OSIV
방식으로 서비스를 서빙한다면, 데이터베이스 커넥션을 너무 오래 들고 있어서 실시간성이 중요한 어플리케이션에서는 문제가 생긴다.
만약 컨트롤러 계층에서 데이터를 가공하기 위해 외부 API를 호출한다고 가정한다. 그 외부 API 호출하는데에서 3초가 걸려버리면, 데이터베이스 커넥션을 물고 계속 기다리게 된다. 데이터베이스의 커넥션과 상관이 없음에도!
또한, 요청당 트랜잭션 방식의 OSIV
는 컨트롤러 계층까지 데이터베이스 커넥션이 살아있음으로서 발생하는 문제점도 있다. 만약 컨트롤러 단에서 사용자의 이름을 노출하지 않기 위해 "XXX" 로 바꾸는 로직이 존재한다면 컨트롤러에서 바꾼 데이터가 더티체킹(Dirty checking)
으로 인해 데이터베이스 내의 데이터가 변경되는 문제가 발생하게 된다.
그렇기에 컨트롤러 계층(혹은 프레젠테이션 계층) 에서 엔티티를 수정하지 못하게 막으면 된다. 컨트롤러 계층에서 엔티티를 수정하지 못하게 막는 방법은 다음과 같다.
요청당 트랜잭션 방식의 OSIV
방식은 컨트롤러 계층에서 데이터를 변경할 수 있다는 문제점이 있고, 데이터베이스 커넥션을 계속 물고 있기 때문에 많은 요청이 발생하는 서비스에서 성능 저하를 가져올 수 있다.
스프링에서는 이런 문제점들을 해결하기 위해, 비즈니스 계층에서 트랜잭션을 사용하는 OSIV
방식을 제공한다. 이름 그대로 OSIV를 사용하긴 하지만, 트랜잭션은 비즈니스 계층에서만 사용한다는 뜻이다.
위와 같이 영속성 컨텍스트가 컨트롤러나 뷰 계층까지 유지된다면, 서비스 계층의
@Transactional
어노테이션이 붙은 비즈니스 로직에서 데이터베이스 커넥션을 가져온 뒤 반환을 컨트롤러나 뷰 계층에서까지 유지되는 것이다.
즉, API 서버라면 API 응답이 반환될 때까지 커넥션을 유지하고, 뷰 템플릿을 사용하는 서버라면 뷰가 렌더링될 때까지 커넥션을 유지한다. 왜? 지연로딩을 사용하기 위함이다.
스프링 OSIV(OSIV ON)
도 요청당 트랜잭션 방식의 OSIV
방식과 마찬가지로, 영속성 컨텍스트와 데이터베이스의 커넥션의 생명주기는 같다. 하지만 트랜잭션의 범위가 다르다는 것과 엔티티 수정 여부가 차이점이다.
@Transactional
로 트랜잭션을 시작할 때 위에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작이 방식 또한 요청당 트랜잭션 방식의 OSIV
과 마찬가지로 데이터베이스 커넥션을 계속 유지하고 있기에 실시간성이 중요한 애플리케이션에서는 커넥션이 모자라서 장애로 이어질 수 있다.
JPA로 개발하면 꼭 만나게 되는 이 warn 로그.. OSIV가 켜져있으면 이런 warn 로그가 나온다!
2023-03-24 01:50:55.989 WARN 16744 --- [main] 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
스프링에서는 디폴트 값으로 OSIV
값을 true
로 갖는다. OSIV
를 false
로 해주면 아래와 같이 영속성 컨텍스트가 트랜잭션과 생명주기를 같이한다.
즉, OSIV
를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스도 낭비하지 않는다!
하지만 OSIV
를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 컨트롤러 계층(혹은 프레젠테이션 계층)에서 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다.
OSIV
를 끈 상태에서 복잡성을 관리하는 좋은 방법이 있다. 그것은 CQS(Command-Query-Separation)
패턴으로 커맨드와 쿼리를 분리해서 관리하는 디자인패턴이다.
커맨드(Command)
: 결과를 반환하지 않고, 대신 시스템의 상태를 변화시킨다.쿼리(Query)
: 결과값을 반환하고, 상태를 변화시키지 않는다.그러면 CQS
패턴을 지키면 OSIV를 끈 상태에서 복잡성을 관리하는 데에 왜 좋은걸까? 우선 CQS 패턴
은 Command
와 Query
를 분리함으로써, 각 메소드의 의미를 매우 명확하게 해준다.
OSIV를 끄게 되면 트랜잭션 안에서 조회되어야 하고 수정되어야한다. 하지만 너무 많은 기능들이 서비스 계층에 몰려있으면 유지보수측면에서도 관리하기 어려워 질 수 있다.
그럴 때 CQS 패턴
을 지키면서 개발한다면 추후에 유지보수하기 쉬울 것이다. 김영한님이 실무에서 분리하는 방법은 등록 및 수정 같은 핵심 비즈니스 로직인 커맨드와 화면을 위한 조회 쿼리 의 관심사를 분리해서 아예 다른 클래스로 만든다고 한다.
OrderService
OrderService
: 핵심 비즈니스 로직OrderQueryService
: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)이렇게 하면 자주 변경되는 로직(조회:쿼리
)과 자주 변경되지 않는 핵심 로직(등록&수정:커맨드
)를 분리할 수 있다.
실시간성이 중요한 어플리케이션에서는 OSIV
를 무조건 끄자! 잘못하면 데이터베이스 커넥션이 부족해서 장애로 이어질수도 있다.
하지만 Admin 페이지 같은 사용자가 많이 없을 서비스의 경우에는 OSIV
를 켜서 지연로딩을 적극 활용하는 것도 좋을 것 같다.
자바 ORM 표준 프로그래밍 JPA
https://dundung.tistory.com/183
https://en.wikipedia.org/wiki/Command%E2%80%93query_separation