Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

사부작사부작

OSIV와 더티 체킹 본문

프로젝트

OSIV와 더티 체킹

민철킴 2023. 5. 30. 21:34

OSIV 를 default 값인 true인 상태로 개발을 하다가, false로 바꾸고 이에 따른 더티 체킹이 작동하지 않는 문제를 겪었습니다. OSIV에 대한 설명과 개선해 간 과정을 적은 글입니다. 틀린 부분이 있을 수 있습니다.

 


먼저 OSIV를 모르는 상태로 기능을 구현하는 중이였다. 그러다가 동욱님의 블로그 글(jojoldu.tistory.com/272)을 읽다가 OSIV라는 단어를 접했고, 이걸 false로 변경하기로 했다. 

OSIV (Open-Session-In-View)

OSIV는 Hibernate 세션(DB작업을 수행하기 위한 논리적 트랜잭션 컨텍스트)을 뷰까지 열어두는 패턴을 의미한다. 스프링에서 이 패턴을 구현한 클래스로는 OpenSessionInViewInterceptor 클래스가 있다. 이 클래스의 인스턴스가 Spring MVC의 인터셉터 레지스트리에 등록되어 해당 패턴을 사용한다.

public void preHandle(WebRequest request) throws DataAccessException

Open a new Hibernate Session according and bind it to the thread via the TransactionSynchronizationManager.
public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException 

Unbind the Hibernate Session from the thread and close it.

공식문서를 보면 해당 클래스는 위의 메서드들을 가지고 있다. 풀어보면 클라이언트에서 요청이 도착하면 이 인터셉터는 Hibernate 세션을 시작한다. 기본적으로는 트랜잭션 시작되면 세션이 시작되고, 트랜잭션이 끝나면 세션도 종료된다. 하지만 이 패턴에서는 요청이 시작되는 순간부터 세션이 열리고 뷰 렌더링 시점까지 같은 세션이 유지되는 특징을 가진다.

baeldung 에서도 OSIV를 논란이 많고(Controversial) 양날의 검(double-edged sword)이라고 소개하고 있다. 그러면 왜 논란이 많은지 장단점을 알아보자.

https://www.baeldung.com/spring-open-session-in-view

OSIV On&Off 장단점

먼저 장점은 지연로딩이다.

프레젠테이션 레이어에서 연관 관계가 지연 로딩이 설정된 엔티티에 접근하는 경우에 LazyInitializationException 이 발생한다. 영속성 컨텍스트가 끝난 상태에서 지연 로딩으로 생성된 프록시 객체에 접근할 경우, 이 프록시 객체가 데이터베이스에서 실제 객체의 데이터를 가져올 수 없기 때문에 해당 에러가 발생한다.

OSIV를 활성화시켜서 영속성 컨텍스트를 계속 유지한다면 이러한 에러 없이 지연로딩이 가능해진다.
즉, 이 장점 때문에 사용하는 것이다. 뷰 렌더링 단계까지 세션을 열어두어 LazyInitializationException 없이 지연로딩을 허용할 수 있기 때문에 OSIV를 사용한다.

하지만 단점은

  1. 여러 트랜잭션이 하나의 영속성 컨텍스트를 공유해서 사용한다는 점이다. 이로 인해 의도치 않은 데이터가 저장될 수 있다.
  2. 한 번 획득한 DB 커넥션을 뷰 렌더링이 끝날 때까지 유지한다. 이로 인해 커넥션이 모자라 성능 이슈의 원인이 된다.

이러한 단점들로 OSIV를 off 하기로 했다.


하지만 문제가 발생했다.

@RequiredArgsConstructor
@Component
public class MemberArgumentResolver implements HandlerMethodArgumentResolver {
		
		// ...

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(LoginMember.class);
    }

    @Override
    public Object resolveArgument(
            MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

        String token = authExtractor.extract(request);
        if (tokenService.getBlackList(token) != null) {
            return new InvalidAccessTokenException();
        }

        String oAuth = Optional.ofNullable(token)
                .filter(t -> jwtTokenProvider.validateToken(t))
                .map(t -> jwtTokenProvider.getPayload(t))
                .orElseThrow(InvalidAccessTokenException::new);

		Member member = memberRepository.findByOAuth(oAuth);
		return member;
    }
}

먼저 나는 accessToken에서 oauth를 꺼내오고 그걸로 Repository를 호출해서 멤버 엔티티를 조회해 @LoginMember 어노테이션에 바인딩해주고 있다.

그리고 이 어노테이션을 컨트롤러 파라미터로 사용중이다. 아래처럼 말이다.

public ResponseEntity<?> changePosition(@LoginMember final Member member) {
	memberService.changePosition(member);
	...
}

또한 서비스단에서 상태 변경을 더티 체킹에 의존하게 코드를 짰다. 하지만 OSIV 를 off 시키니 더티 체킹이 더 이상 이루어지지 않았다.


원인은 다음과 같다.

더티 체킹은 영속성 컨텍스트 안에서 엔티티들의 변경 사항을 감지하는 기술이다. 이 영속성 컨텍스트에 해당하려면 2가지 경우에만 가능하다. 트랜잭션 범위 내에서 엔티티를 조회해 온 경우이거나 save 한 엔티티만 가능하다.

즉, 파라미터로 전달받은 엔티티는 영속성 컨텍스트에 들어가지 못한다. 그렇기에 변경 감지 대상이 아니고 더티 체킹은 이뤄지지 않는다.

OSIV가 활성화된 상태라면, 클라이언트의 요청 시작 시점부터 영속성 컨텍스트의 범위에 해당한다. 그렇기에 컨트롤러 단에서 넘겨받은 객체도 더티체킹이 가능했다. 그렇다면 OSIV가 off인 상황에서 어떻게 해결할 수 있을까?

 

  1. 더티 체킹 안 하기
    더티 체킹을 하지 않고 비지니스 로직이 끝나기 전에, 직접 JpaRepository의 save() 메서드를 호출할 수 있다. 하지만 비지니스 로직과 영속성 로직이 함께 존재하게 된다.
  2. 서비스단에서 엔티티 조회하기
    ArgumentResolver에서 엔티티를 직접 조회해서 바인딩 시키지 않고, 엔티티를 조회할 수 있는 값으로 바인딩시킨다. 컨트롤러에서 해당 값을 서비스 로직으로 보내고 거기서 엔티티를 조회하게 바꾼다.

 

나는 2번째 방법을 선택했다.

String oAuth = Optional.ofNullable(token)
            .filter(t -> jwtTokenProvider.validateToken(t))
            .map(t -> jwtTokenProvider.getPayload(t))
            .orElseThrow(InvalidAccessTokenException::new);

return new LoginMemberDto(oAuth);

LoginMemberDto라는 oAuth 값만 가지는 DTO 클래스를 만들고 이를 @LoginMember에 바인딩시켜 주는 방법으로 해결했다.

이 방법을 선택한 이유는 먼저 기존에 사용하던 더티 체킹을 유지하고 싶었다. 또한 컨트롤러단(ArgumentResolver)에서 Repository단으로 멤버를 조회하지 않기에 의존성을 줄일 수 있다는 점과 서비스단에서 엔티티를 조회하는 게 레이어 간의 역할에 더 바람직하다고 느꼈기 때문이다.