[Spring boot] 웹 애플리케이션과 영속성 관리

어정윤·2021년 8월 7일
0

Spring boot 스터디

목록 보기
11/15
post-thumbnail

[Spring boot] 웹 애플리케이션과 영속성 관리

트랜잭션 범위의 영속성 컨텍스트

스프링 컨테이너의 기본 전략

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 이 전략은 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다. 즉, 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.

스프링 프레임워크를 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다.

트랜잭션 범위의 영속성 컨텐스트

  1. 스프링 트랜잭션 AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작한다.
  2. 대상 메소드가 정상 종료되면 트랜잭션을 커밋하고 종료한다.
    - 이때 트랜잭션을 커밋하면 JPA는 먼저 영소성 컨텍스트를 플러시해서 변경 내용을 데이터 베이스에 반영한 후 데이터베이스 트랜잭션을 커밋한다.
    - 예외 발생 시 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다.
  • 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용
    : 다양한 위치에서 엔터티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
  • 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용
    : 여러 스레드에서 동시에 요청이 와서 같은 엔터티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.

준영속 상태와 지연 로딩

트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 종료된다. 따라서 조회한 엔터티가 서비스와 레포지토리 계층에서는 영속 상태를 유지하지만 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다.

트랜잭션 범위의 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프레젠테이션 계층에서 엔터티는 준영속 상태다. 따라서 변경 감지와 지연 로딩이 동작하지 않는다.

  • 준영속 상태와 변경 감지

변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층(트랜잭션 범위)까지만 동작한다. 단순히 데이터를 보여주기만 하는 프레젠테이션 계층에서 데이터를 수정할 일은 없기 때문에 문제되지 않는다.

  • 준영속 상태와 지연 로딩

준영속 상태의 가장 큰 문제는 지연 로딩 기능이 동작하지 않는다는 점이다. 이를 해결하는 방법은 크게 2가지가 있다.

  1. 뷰가 필요한 엔터티를 미리 로딩해두는 방법
    • 글로벌 페치 전략 수정
    • JPQL 페치 조인
    • 강제로 초기화
  2. OSIV를 사용해서 엔터티를 항상 영속 상태로 유지하는 방법

글로벌 페치 전략 수정

글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하면 된다.

글로벌 페치 전략에서 즉시 로딩 사용 시 단점

  1. 사용하지 않는 엔터티를 로딩한다.
  2. N+1 문제가 발생한다.
    : JPA를 사용하면서 성능상 가장 조심해야하는 문제로, 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 문제다. N+1이 발생하면 SQL이 상당히 많이 호출되므로 조회 성능에 치명적이다. 이런 N+1문제는 JPQL 페치 조인으로 해결할 수 있다.

JPQL 페치 조인

페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 발생하지 않는다.

JPQL 페치 조인의 단점

JPQL 페치 조인을 무분별하게 사용하면 화면에 맞춘 레포지토리 메소드가 증가할 수 있어 프레젠테이션 계층이 데이터 접근 계층을 침범하게 된다.

따라서 무분별한 최적화로 프레젠테이션 계층과 데이터 접근 계층 간의 의존관계가 급격하게 증가하는 것보다는 적절한 선에서 타협점을 찾는 것이 합리적이다.

강제로 초기화

강제로 초기화하기는 영속성 컨텍스트가 살아있을 때 프레젠테이션 계층이 필요한 엔터티를 강제로 초기화해서 반환하는 방법이다.

이는 은근 슬쩍 프레젠테이션 계층이 서비스 계층을 침범하는 상황이므로 비즈니스 로직을 담당하는 서비스 계층에서 프렌젠체이션 계층을 위한 프록시 초기화 역할을 분리해야 한다. FACADE 계층이 그 역할을 담당해줄 것이다.

FACADE 계층 추가

아래 그림은 프레젠테이션 계층과 서비스 계층 사이에 뷰를 위한 프록시 초기화를 담당하는 FACADE 계층을 하나 더 두는 방법이다.

FACADE 계층의 역할과 특징

  • 프레젠테이션 계층과 도메인 계층 간의 논리적 의존성 분리
  • 프레젠테이션 계층에서 필요한 프록시 객체를 초기화
  • 서비스 계층을 호출해서 비즈니스 로직을 수행
  • 레포지토리를 직접 호출해서 뷰가 요구하는 엔터티를 찾음

준영속 상태와 지연 로딩의 문제점

뷰를 개발할 때 필요한 엔터티를 미리 초기화하는 방법은 생각보다 오류가 발생할 가능성이 높다. FACADE를 사용해서 어느 정도 해소할 수는 있지만 상당히 번거롭다.

따라서 모든 문제는 엔터티가 프레젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 영속성 컨텍스트를 뷰까지 살아있게 열어두어 뷰에서도 지연 로딩을 사용할 수 있게 하는 것이 OSIV이다.

OSIV

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔터티는 영속 상태로 유지되므로 뷰에서도 지연 로딩을 사용할 수 있다.

과거 OSIV : 요청 당 트랜잭션

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

요청 당 트랜잭션 방식의 OSIV의 문제점

요청 당 트랜잭션 방식의 OSIV는 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔터티를 변경할 수 있다는 문제가 있다. 이런 문제를 해결하려면 프레젠테이션 계층에서 엔터티를 수정하지 못하게 막으면 된다.

프레젠테이션 계층에서 엔터티를 수정하지 못하게 막는 방법은 다음과 같다.

  • 엔터티를 읽기 전용 인터페이스로 제공

  • 엔터티 레핑 : 엔터티의 읽기 전용 메소드만 가지고 있는 엔터티를 감싼 객체를 만들어 프레젠테이션 계층에 반환

  • DTO만 반환 : 프레젠테이션 계층에 엔터티 대신 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환(OSIV의 장점을 살릴 수 없고, 엔터티를 거의 복사한 DTO 클래스를 하나 더 만들어야 한다.)

    지금까지 설명한 방법 모두 코드량이 상당히 증가한다는 단점이 있다. 이러한 문제점들로 인해 요청 당 트랜잭션 방식의 OSIV는 최근에는 거의 사용하지 않는다.

스프링 OSIV : 비즈니스 계층 트랜잭션

스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서만 트랜잭션을 유지하는 방식이다.

동작 원리

  1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성(단, 트랜잭션은 시작하지 않음)
  2. 서비스 계층에서 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작
  3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시(이때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않음)
  4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔터티는 영속 상태를 유지
  5. 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료(이때 플러시를 호출하지 않고 바로 종료)

트랜잭션 없이 읽기

영속성 컨텍스트는 트랜잭션 범위 안에서 엔터티를 조회하고 수정하 수 있는데, 트랜잭션 범위 밖에서 엔터티를 조회만 할 수 있는 것을 트랜잭션 없이 읽기라고 한다.

비즈니스 계층 트랜잭션 OSIV의 특징

  • 영속성 컨텍스트를 프레젠테이션 계층까지 유지
  • 프레젠테이션 계층에는 트랜잭션이 없으므로 엔터티 수정 불가능
  • 프레젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해 지연 로딩 가능

스프링 OSIV 주의사항

스프링 OSIV를 사용하면 프레젠테이션 계층에서 엔터티를 수정해도 데이터베이스에 반영되지 않지만, 엔터티 수정 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.

이런 문제를 해결하려면 단순히 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔터티를 변경하면 된다.

스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있어 이런 문제가 발생한다. OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같으므로 이런 문제가 발생하지 않는다.

profile
성장ing

0개의 댓글