[삽질기] 영속성 컨텍스트의 지속 범위 (feat. OSIV)

Suntory·2022년 6월 26일
4

JPA 삽질기

목록 보기
2/2
post-thumbnail

안녕하세요. 얼마전에 적었던 글을 공유했다가 몰랐던 사실을 알게 되어 다시 글을 적어봅니다.

🤔 상황 소개

이전 글에서 영속성 컨텍스트에서 관리되지 않는 객체를 argument resolver가 전달해줘서 더티 체킹이 발생하지 않는 이슈가 있었는데요. 해당 원인이 단순히 '트랜잭션이 끝났기 때문에 영속성 컨텍스트가 끝나고 준영속 상태가 되었다' 라고 생각했습니다.

트랜잭션 때문?

그런데 Interceptor의 preHandle()이 아닌 ArgumentResolver에서 조회해온 객체는 영속성 컨텍스트의 관리 대상이 된다는 사실을 알게 됩니다. 그러면서 더더욱 트랜잭션 원인설이 아닌 것 같은 느낌이 들기 시작했습니다.

Propagation.MANDATORY

그럼 설마 argument Resolver에서 호출하면 트랜잭션이 뒤에 서비스까지 도달할 때 유지되는건가? 라는 생각이 들어 전파 속성 중 부모 트랜잭션이 존재하지 않으면 예외가 발생하는 MANDATORY로 설정하여 실험해봤습니다.

당연히도 서비스 메서드 호출이 끝나는 시점에 트랜잭션이 끝난다는 걸 알았지만 왜 interceptor는 안되고 argument resolver는 될까?에 대한 해답을 몰랐었기 때문에 뭐라도 해봐야겠다는 생각이었습니다.

결과는 당연히 예외가 발생했습니다. argument resolver에서 user 정보를 조회하는 트랜잭션과, logout 로직을 수행하는 트랜잭션은 별개였습니다.

⚠️ 그런데 말입니다

그런데 제가 모르는 사실이 있었습니다. 바로 OSIV(Open Session In View)라는 개념이었습니다. (김영한님의 JPA 강의를 성실히 듣지 않은 업보..)

OSIV란?


OSIV란 View에 데이터를 전달할 때 지연 로딩 등의 이유로 영속성 컨텍스트를 지속해야 하는 경우가 있습니다. 이럴 때에 대비해 영속성 컨텍스트의 생존 범위가 트랜잭션 범위가 아닌 Filter, Interceptor, Controller와 같은 layer까지 지속되는 것을 말한다고 합니다.

이 설정은 default로 ON입니다. 지난 글에서 봤던 어플리케이션은 별다른 설정을 하지 않았기 때문에 이 설정이 켜져 있었습니다.

그럼 왜 인터셉터에서 불러온 엔티티는 영속성 컨텍스트에서 관리하지 않았을까?

위 사진을 봤을 때도 이해가 되지 않았습니다. '분명 인터셉터까지 영속성 컨텍스트 관리한다며? 근데 왜 안됨?' 이라는 생각밖에 들지 않았습니다.

그래서 구글링을 해봤습니다..

인터셉터에도 순서가 있다

그러다 이 글을 보고 큰 깨달음을 얻었습니다. 설정해주는 인터셉터에도 작동 순서가 있듯이, 기본적으로 Spring이 실행될 때 올라오는 인터셉터에도 순서가 있었습니다.

그런데 영속성 컨텍스트를 켜는 인터셉터는 우리가 설정해주는 인터셉터보다 순서가 뒤였던 것입니다. 즉, 영속성 컨텍스트가 실행되기 전에 객체를 불러왔기 때문에 영속성 컨텍스트에서 관리할 수가 없었던 것입니다.

위 사진은 디스패처 서블릿에서 호출하는 인터셉터의 목록입니다. 저희 프로젝트에서 설정한 AuthInterceptor가 맨 먼저 실행됩니다. 영속성 컨텍스트는 WebRequestHandlerInterceptoryAdapter 에서 실행되므로, 해당 인터셉터에서 엔티티를 가져오면 영속성 컨텍스트에서 관리할 수 없습니다.

위 사진은 WebRequestHandlerInterceptoryAdapter 의 내부 구조입니다. 내부에 OpenEntityManagerInViewInterceptor를 가지고 있으며, 이 인터셉터의 preHandle()에서 영속성 컨텍스트를 실행함을 유추할 수 있었습니다.

그럼 방법은?

인터셉터의 preHandle()로부터 객체를 가져와서 영속성 컨텍스트의 관리대상으로 삼으려면 인터셉터보다 이전에 영속성 컨텍스트가 실행되도록 하는 방법이 있습니다. 그 방법은 OpenEntityManagerInViewInterceptor의 filter 버전인 OpenEntityManagerInViewFilter를 설정해주는 방법입니다.

필터는 요청이 디스패처 서블릿에 들어오기 전에 실행되므로 미리 영속성 컨텍스트를 실행하고 나서 AuthInterceptor에서 엔티티를 가져오도록 하면 영속성 컨텍스트의 지원을 받을 수 있게 됩니다.

결국 선택한 방법

그래서 OSIV라는 개념을 알고 문제의 원인을 정확히 알 수 있었습니다.
그리고 저희가 선택한 방법은 유저 엔티티를 사용할 때 영속성 컨텍스트의 지원을 받지 않고, 그냥 필요할 때마다 ID를 통해 조회해오는 방법을 선택했습니다.

이렇게 생각한 이유는
첫 째, 유저 정보를 조회하는 경우가 그렇게 많을 것 같지 않다는 점과 둘 째, OSIV를 켰을 때 DB Connection이 너무 낭비되는 느낌을 받았기 때문입니다.

여태껏 트랜잭션이 일어날 때만 DB Connection을 얻고 영속성 컨텍스트를 이용하고 트랜잭션이 끝나면 반납한다고 생각했습니다. 그런데 OSIV를 켜면 요청이 들어와서 나갈 때까지 계속 커넥션을 점유하는 꼴이 되므로 DB 커넥션 풀에서 병목을 야기할 수도 있겠다라는 생각이 들었습니다.

그래서 지연 로딩을 포함한 모든 작업은 Service에서 모두 처리하고 View에서는 영속성 컨텍스트가 없어도 되도록 해보고 싶었습니다. 그래서 OSIV를 해제하고, 트랜잭션 내에서만 영속성 컨텍스트를 지속하는 방법으로 바꿨습니다.

마치며

오랜만에 블로그에 글을 적었었는데 그걸 공유하는 과정에서 몰랐던 개념을 알게 돼서 정말 좋았습니다. 글의 내용이 잘못된 것이 부끄럽기는 하지만 몰랐던 내용을 알게된 것이니 적길 잘했다는 생각이 듭니다!!

앞으로도 삽질 할때마다 적극적으로 블로그를 쓰고 공유해보겠습니다!!

깨닫게 해주신 소중한 분들의 글 출처

OSIV와 성능 최적화

OSIV와 Custom Interceptor를 같이 사용하기!

profile
천천히, 하지만 꾸준히 그리고 열심히

6개의 댓글

comment-user-thumbnail
2022년 6월 26일

센토리야 센토리야

답글 달기
comment-user-thumbnail
2022년 6월 27일

심도높은 오시브 글 잘 읽었읍니다
저도 커넥션 범위를 생각하지 않고 영속성 컨텍스트를 인터셉터에서 처리했던 적이 있는 것 같은데,
커넥션 문제가 생긴다면 인증 체크는 어디서 하는 건가요,,,,, 서비스 단에서 하나하나 해줘야 하는 걸까요?

1개의 답글
comment-user-thumbnail
2022년 6월 27일

최곱니더 샌토리선생님 ㅎㅎㅎ

1개의 답글