OSIV와 성능 최적화

땡글이·2023년 3월 23일
0

JPA

목록 보기
7/9

과거 : 요청당 트랜잭션 방식의 OSIV

OSIV의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다. 가장 단순한 구현 방법은 클라이언트의 요청이 들어오자마자 서블릿 필터나 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다. 이것을 요청당 트랜잭션 방식의 OSIV이라고 한다.

문제점

많은 사용자들이 사용하는 서비스에서 요청당 트랜잭션 방식의 OSIV 방식으로 서비스를 서빙한다면, 데이터베이스 커넥션을 너무 오래 들고 있어서 실시간성이 중요한 어플리케이션에서는 문제가 생긴다.

만약 컨트롤러 계층에서 데이터를 가공하기 위해 외부 API를 호출한다고 가정한다. 그 외부 API 호출하는데에서 3초가 걸려버리면, 데이터베이스 커넥션을 물고 계속 기다리게 된다. 데이터베이스의 커넥션과 상관이 없음에도!

또한, 요청당 트랜잭션 방식의 OSIV 는 컨트롤러 계층까지 데이터베이스 커넥션이 살아있음으로서 발생하는 문제점도 있다. 만약 컨트롤러 단에서 사용자의 이름을 노출하지 않기 위해 "XXX" 로 바꾸는 로직이 존재한다면 컨트롤러에서 바꾼 데이터가 더티체킹(Dirty checking)으로 인해 데이터베이스 내의 데이터가 변경되는 문제가 발생하게 된다.

그렇기에 컨트롤러 계층(혹은 프레젠테이션 계층) 에서 엔티티를 수정하지 못하게 막으면 된다. 컨트롤러 계층에서 엔티티를 수정하지 못하게 막는 방법은 다음과 같다.

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 래핑
  • DTO로 반환

스프링 OSIV : 비즈니스 계층 트랜잭션 (Feat.OSIV ON)

요청당 트랜잭션 방식의 OSIV 방식은 컨트롤러 계층에서 데이터를 변경할 수 있다는 문제점이 있고, 데이터베이스 커넥션을 계속 물고 있기 때문에 많은 요청이 발생하는 서비스에서 성능 저하를 가져올 수 있다.

스프링에서는 이런 문제점들을 해결하기 위해, 비즈니스 계층에서 트랜잭션을 사용하는 OSIV 방식을 제공한다. 이름 그대로 OSIV를 사용하긴 하지만, 트랜잭션은 비즈니스 계층에서만 사용한다는 뜻이다.

위와 같이 영속성 컨텍스트가 컨트롤러나 뷰 계층까지 유지된다면, 서비스 계층의 @Transactional 어노테이션이 붙은 비즈니스 로직에서 데이터베이스 커넥션을 가져온 뒤 반환을 컨트롤러나 뷰 계층에서까지 유지되는 것이다.
즉, API 서버라면 API 응답이 반환될 때까지 커넥션을 유지하고, 뷰 템플릿을 사용하는 서버라면 뷰가 렌더링될 때까지 커넥션을 유지한다. 왜? 지연로딩을 사용하기 위함이다.

스프링 OSIV(OSIV ON)요청당 트랜잭션 방식의 OSIV 방식과 마찬가지로, 영속성 컨텍스트와 데이터베이스의 커넥션의 생명주기는 같다. 하지만 트랜잭션의 범위가 다르다는 것과 엔티티 수정 여부가 차이점이다.

OSIV ON 동작원리 및 순서

  • 요청이 들어오면, 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성
  • 서비스 계층에서 @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 OFF

스프링에서는 디폴트 값으로 OSIV 값을 true로 갖는다. OSIVfalse로 해주면 아래와 같이 영속성 컨텍스트가 트랜잭션과 생명주기를 같이한다.
즉, OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스도 낭비하지 않는다!

하지만 OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 컨트롤러 계층(혹은 프레젠테이션 계층)에서 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다.

CQS : 커맨드와 쿼리 분리

OSIV를 끈 상태에서 복잡성을 관리하는 좋은 방법이 있다. 그것은 CQS(Command-Query-Separation) 패턴으로 커맨드와 쿼리를 분리해서 관리하는 디자인패턴이다.

  • 커맨드(Command) : 결과를 반환하지 않고, 대신 시스템의 상태를 변화시킨다.
  • 쿼리(Query) : 결과값을 반환하고, 상태를 변화시키지 않는다.

그러면 CQS 패턴을 지키면 OSIV를 끈 상태에서 복잡성을 관리하는 데에 왜 좋은걸까? 우선 CQS 패턴CommandQuery를 분리함으로써, 각 메소드의 의미를 매우 명확하게 해준다.

OSIV를 끄게 되면 트랜잭션 안에서 조회되어야 하고 수정되어야한다. 하지만 너무 많은 기능들이 서비스 계층에 몰려있으면 유지보수측면에서도 관리하기 어려워 질 수 있다.

그럴 때 CQS 패턴을 지키면서 개발한다면 추후에 유지보수하기 쉬울 것이다. 김영한님이 실무에서 분리하는 방법은 등록 및 수정 같은 핵심 비즈니스 로직인 커맨드화면을 위한 조회 쿼리 의 관심사를 분리해서 아예 다른 클래스로 만든다고 한다.

  • OrderService
    • OrderService : 핵심 비즈니스 로직
    • OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)

이렇게 하면 자주 변경되는 로직(조회:쿼리)과 자주 변경되지 않는 핵심 로직(등록&수정:커맨드)를 분리할 수 있다.


결론

실시간성이 중요한 어플리케이션에서는 OSIV를 무조건 끄자! 잘못하면 데이터베이스 커넥션이 부족해서 장애로 이어질수도 있다.
하지만 Admin 페이지 같은 사용자가 많이 없을 서비스의 경우에는 OSIV를 켜서 지연로딩을 적극 활용하는 것도 좋을 것 같다.

Reference

자바 ORM 표준 프로그래밍 JPA
https://dundung.tistory.com/183
https://en.wikipedia.org/wiki/Command%E2%80%93query_separation

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글