들어가며
DB 레플리케이션을 통한 성능 개선을 진행하였다. MySQL과 스프링부트 환경에서 레플리케이션을 적용하였는데 이는 추후 블로깅 예정이다.
DB 레플리케이션을 위해서는 스프링 부트에서 여러 DataSource를 설정하고, 트랜잭션의 옵션에 따라 특정 DataSource로 분기처리를 해야한다.
이를 적용하려고 할 때 문제가 발생했다.
서비스 메소드의 readOnly = false 메소드가 읽기전용 db로만 쿼리가 날아간 것이다. 우리의 예상대로면 readOnly = false 일 경우 쓰기 db, readOnly = true 일 경우 읽기 db 로 날아가야한다. 하지만 문제가 발생했다.
문제 발견
우리의 요즘카페 서비스에서는 로그인한 사용자가 조회 하며 조회 기록 데이터를 수정하고, 좋아요 처리를 하고 등 여러 기능이 존재한다. 이 기능들에서만 문제가 발생했던 것이다.
회원이 사용하는 로직에서만 문제가 발생했다.
그럼 왜 로그인 한 유저에서만 문제가 발생할 까 ?
위의 코드는 우리의 LoginArgumentResolver 이다. 컨트롤러에서 파라메터로 Member를 받게 되면 이를 조회하고 처리를 해준다. 그 과정에서 memberRepository.findById 를 사용한다.
현재 우리가 사용하는 Repository는 다음과 같다. 이거랑 Transaction이랑 무슨 관계가 있을까 ?
@Repository
public interface MemberRepository extends JpaRepository<Member, String> {
// ...
}
구현체를 확인해보니 Transaction을 사용하고 있었다. 그리고 이 트랜잭션은 readOnly = true 였고 문제를 인지하게 되었다. 이렇기 때문에 이후 모든 쿼리에서는 조회 db로만 쿼리가 날아가는 것이었다.
문제는 OSIV
연어가 초반에 osiv 관련 이야기를 했다. 그때는 아무생각 없었는데 ..
나는 위의 문제를 보고 OSIV(open session in view) 문제 라고 인식했다.
그럼 OSIV(Open Session In View) 가 무엇이고 왜 이런 문제의 원인이 되엇을까 ?
OSIV란 JPA에서 영속성 컨텍스트를 프레젠테이션 레이어까지 열어둔다는 것이다. 이를 통해서 지연 로딩을 활용하기 위함이다.
OSIV 가 활성화 되어있다면 서비스 계층에서 컨트롤러로 직접 엔티티를 반환해도 컨트롤러에서는 지연로딩으로 모든 값을 읽어올 수 있다. 우리는 서비스 계층에서 DTO를 통해 엔티티를 반환하기 때문에 이런 문제를 직접 겪을 일은 없었다. 그래서 처음 인지하게 되었다.
그래서 정확히 뭐가 문제인데 ?
OSIV를 사용하면 영속성 컨텍스트가 API 요청 내내 유지된다.
영속성 컨텍스트는 EntityManager 생성마다 하나가 생성되며, 이 EntityManager가 DB연결이 필요한 시점에 DB 커넥션을 획득해온다. 이 때 당연히 DataSource를 사용한다. 위에서 말했던 것 처럼 OSIV 가 켜져있다면 영속성 컨텍스트 와 커넥션이 API 요청 내내 유지가 되어 문제가 생기는 것이다.
OSIV가 켜져있다면 다음과 같은 상태일 것이다.
OSIV 가 활성화 되어있는 지금 상태에서는 Service, Repository에서 발생하는 트랜잭션이 같은 영속성 컨텍스트 위에서 실행된다.
ArgumentResolver 가 Repository를 호출한 시점에 영속성 컨텍스트는 커넥션을 얻었고 이후 Service에서 실행할 시점에는 커넥션이 이미 존재하기에 따로 획득하지 않는 것이다. 그렇기에 DataSource 분기가 정상적으로 이루어지지 않고 있다.
처음 우리 목적은 서비스 메소드 마다 다른 트랜잭션 읽기 수준에 따라 다른 DataSource를 획득해오려 하였다.
하지만 로그인 사용자인 LoginArgumentResolver 에서 Repository를 통해 값을 가져올 때 OSIV 가 활성화된 상태라면 영속성 컨텍스트의 커넥션을 획득해오며, DataSource를 얻어온다. 그렇기 때문에 이후에 실행되는 비즈니스 로직에서는 LoginArgumentResolver 가 이미 영속성 컨텍스트와 커넥션을 획득했기에 다시 호출하지 커넥션을 획득하지 않는 것이다.
결론
영속성 컨텍스트가 비즈니스 로직 이전에 한번 생성되고, 커넥션도 한번만 획득하기 때문에 DataSource 선택도 LoginArgumentResolver 에서 처리가 되며 DataSource 분기가 정상적으로 이루어지지 않는 것이다.
OSIV 비활성화
OSIV를 비활성화 하면 영속성 컨텍스트 생존 범위는 트랜잭션 시작~종료 로 이루어진다. 그래서 영속성 컨텍스트가 여러번 생성되고 커넥션도 여러번 생성되기 때문에 DataSource 또한 개별 트랜잭션 마다 선택된다. 그렇게 됨으로써 DataSource 분기가 정상적으로 이루어질 수 있다.
spring:
jpa:
open-in-view: false
이를 통해 처리하였다!
이렇게 함으로써 트랜잭션 종료 시 영속성 컨텍스트를 닫고 DB 커넥션을 반환하여 커넥션 리소스를 낭비하지 않는다!
나가며
Replication 작업을 시작했을 때 JPA에서 문제는 생기지 않을 줄 알았습니다. 이 문제를 해결하면서 사용ㅇ하는 프레임워크, 라이브러리에 대해 깊은 이해가 중요하다는 것을 알게 되었습니다. OSIV 키워드 문제를 직접 경험해보고 알아간 방식 이라 더욱 머리에 남는 것 같습니다.
'코코코딩공부' 카테고리의 다른 글
테스트를 더 빠르게 진행시켜보자 (0) | 2023.10.21 |
---|---|
TestContainer 사용기 & 테스트 격리 (1) | 2023.10.16 |
DB Replication (1) | 2023.10.15 |
Flyway 적용해보기 (0) | 2023.09.23 |
N+1문제 개선기 (0) | 2023.09.11 |