현재 개발중인 애플리케이션은 Multi Datasource를 사용중이다.
Header로 전달받은 값에 따라 연결할 데이터베이스를 결정하고, 경우에 따라선 한 API에서 2개 이상의 데이터베이스에 접근해야 한다. JPA를 사용중이고, 몇 십개의 엔티티들은 대부분 다 관계성을 띄고 있어 FetchType을 기본적으로 Lazy Association으로 가져간다.
OSIV (Open Session In View)
OSIV는 개발중에 처음 알게된 용어이다. 엔티티들의 수많은 관계속에서 딱 원하는 Response 데이터 형태를 만들어내기란 여간 힘든게 아니었다. 삽질의 시간속에서 허우적 댈 때쯤 Service Layer에서 CRUD중 C/U/D의 경우에는 @Transactional로 선언된 메소드들이 있었다. 그런데 @Transactional 메소들를 벗어난 이후의 동작이 예상대로 돌아가지 않는 것이다. 특히 데이터베이스 각각에 Update를 해야되는 경우였는데 (그 외에도 몇개의 문제상황이 있었다) A데이터베이스에서 쿼리를 하고, B데이터베이스에다 작업을 하기 위해 MultiDataSource를 사용하기 위해 이용되는 DatabaseContextHolder에 B데이터베이스를 재할당하였다. 당시 나는 당연히 B로 연결이 될 것이라 생각했는데 이게 웬걸 B 데이터베이스로 연결되지 않고 처음에 맺었던 A 데이터베이스에다가 모든 작업을 하고 있던게 아닌가. A에 하는 작업과 B에 하는 작업을 메소드 분리하고, 각각의 메소드에 @Transactional을 걸어줘봤는데 실패, propagation을 requires_new 로도 해보고 여러가지를 시도해봤는데도 실패. 그렇게 삽질을 하고 나서야 알게된게 OSIV였다.
일단 @Transactionl에 대해 알아보고, OSIV를 알아보도록 하자
- @Transactional
- 스프링에서는 간단하게 어노테이션 방식으로 @Transactional을 메소드, 클래스, 인터페이스 위에 추가하여 사용하는 방식이 일반적이다. 이 방식을 선언적 트랜잭션이라 부르며, 적용된 범위에서는 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit 혹은 rollback을 진행해준다.
- Spring이 제공하는 어노테이션으로 @Transactional을 메서드 또는 클래스에 명시하게 되면 특정 메서드 또는 클래스가 제공하는 모든 메서드에 대해 내부적으로 AOP를 통해 트랜잭션 처리코드가 전 후 로 수행된다.
- AOP는 일반적으로 두가지 방식이 있는데 그 중에 하나인 Dynamic Proxy로 예를 들어보면
- @Transactional가 걸려있는 메소드 a() 가 있으면 Proxy는 런타임 시점에 해당 a()메소드를 호출하는 코드를 포함, 트랜잭션 처리에 필요한 코드를 전후로 감싸 트랜잭션 처리를 다이나믹 프록시 객체에 대신 위임한다.
- AOP는 일반적으로 두가지 방식이 있는데 그 중에 하나인 Dynamic Proxy로 예를 들어보면
- 하지만, @Transactional이 걸려있는 메소드가 종료됟더라도 OSIV에 의해 트랜잭션이 끝나도 영속성 컨텍스트는 유지된다.
- OSIV와 영속성컨텍스트
- OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어두는 기능이다. 영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지된다. 뷰까지 영속성 컨텍스트가 살아있다면 뷰에서도 지연 로딩을 사용할 수가 있다.
- OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 그래서 View Template이나 API 컨트롤러에서 지연 로딩이 가능하다.
- 스프링에서는 OSIV가 기본값(true)으로 설정되어있다. 기본적으로는 트랜잭션을 시작할 때 영속성 컨텍스트가 DB 커넥션을 가져온다. 커넥션을 획득한 후에는 API 응답이 끝날때까지 유지한다. 트랜잭션이 끝나도 지연 로딩으로 프록시 객체를 초기화할 상황이 생기기 때문이다. 따라서 영속성 컨텍스트가 DB 커넥션을 계속 물고 있어야 한다.
- JPA의 영속성 컨텍스트는 결국 DB를 1:1로 쓰면서 동작한다.
- OSIV의 문제점?
- 오랫동안 DB 커넥션을 물고 있기 때문에 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이는 결국 장애로 이어진다.
- Multi Datasource를 사용하는 경우에는 처음 맺은 DB 커넥션을 계속 가져가기 때문에 DB Connection을 바꾸는 데에 있어 문제가 생길 수 있다.
- OSIV = false
- OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다. 하지만 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다.
- CRUD 중에서 CUD의 경우에는 기본적으로 서비스레이어에 @Transactional을 설정하였고, Controller에는 비즈니스 로직이 없기 때문에 문제가 없을 수 있지만 R의 경우는 서비스레이어에서 Lazy Loading을 이용해 엔티티를 조작하려고 할 때 Exception이 발생하기 때문에 이 경우에는 @Transational과 read-only값을 true로 설정함으로써 해결이 가능하다.
- OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다. 하지만 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다.
- OSIV=false & MultiDatasource
- OSIV를 true로 하게 되면 최초 연결된 세션을 재사용하여 DetermineRoutingDatasource 클래스의 determineCurrentLookupKey 메소드가 동작하지 않게 되고, 결과적으로 DatabaseContextHolder로 재설정한 데이터베이스로 연결이 되지 않는다. 따라서 MultiDatasource를 사용하는 애플리케이션의 경우엔 OSIV를 false로 가져가는 것이 맞다.
Datasoure를 변경하려고 하면 최초 연결된 세션을 재사용하기 때문에 DetermineRoutingDatasource 클래스의determineCurrentLookupKey 메소드가 동작하지 않게 되고, 결과적으로 DatabaseContextHolder로 재설정한 데이터베이스로 연결이 되지 않았다. 이 문제는 OSIV 를 false로 둠으로써 해결할 수 있었다.
OSIV는 Spring에서 기본적으로 true로 가져가고 있지만, 애플리케이션의 복잡도와 성능을 고려해보고 이 값을 false로 변경해보는 것을 고려해보아야 한다.
참고
https://dodeon.gitbook.io/study/kimyounghan-spring-boot-and-jpa-optimization/04-osiv
https://ykh6242.tistory.com/102
https://incheol-jung.gitbook.io/docs/q-and-a/spring/osiv
'스프링' 카테고리의 다른 글
Spring Data JPA / Dynamic DataSource 설정 (0) | 2022.03.28 |
---|---|
Spring vs Spring Boot 차이점 (0) | 2021.08.09 |
SpringBoot i18n - 다국어 설정 DB/ehcache/CookieLocaleResolver (0) | 2020.10.21 |
SpringBoot Maven 프로젝트 Code Pipeline으로 자동빌드&배포 (0) | 2020.05.08 |
Spring의 기본개념과 동작원리 + 자기반성 (0) | 2020.04.29 |