DB/자바 ORM 표준 JPA 프로그래밍

[13장] 웹 애플리케이션과 영속성 관리

꾸준함. 2025. 3. 18. 23:02

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

  • 순수 J2SE 환경에서 JPA를 사용하면 개발자가 직접 엔티티 매니저를 생성하고 트랜잭션도 관리해야 하지만
  • 스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 제공하는 전략을 따라야 함

 

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

  • 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 디폴트로 사용
    • 해당 전략은 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻
    • 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료
    • 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근

 

https://willseungh0.tistory.com/73

 

  • 스프링 프레임워크를 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작함
    • 외부에서는 단순히 서비스 계층의 메서드를 호출하는 것처럼 보이지만
    • 해당 어노테이션이 있으면 호출한 메서드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작함

 

 

https://devbksheen.tistory.com/entry/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%B2%94%EC%9C%84%EC%9D%98-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8

 

1.1 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다

  • 트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용
  • 앞선 예제에서 엔티티 매니저를 사용하는 A, B 코드 모두 같은 트랜잭션 범위에 있음
    • 따라서 엔티티 매니저는 달라도 같은 영속성 컨텍스트 사용

 

https://devbksheen.tistory.com/entry/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%B2%94%EC%9C%84%EC%9D%98-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8

 

1.2 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다

  • 여러 쓰레드에서 동시에 요청이 들어올 경우 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름
    • 스프링 컨테이너는 쓰레드마다 각각 다른 트랜잭션을 할당
    • 이에 따라 같은 엔티티 매니저를 호출해도 접근하는 영속성 컨텍스트가 다르므로 멀티 쓰레드 상황에 thread-safe 함

 

https://devbksheen.tistory.com/entry/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%B2%94%EC%9C%84%EC%9D%98-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8

 

준영속 상태와 지연 로딩

  • 앞서 언급했다시피 스프링이나 J2EE 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용
    • 트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료됨
    • 이에 따라 조회한 엔티티가 서비스와 레포지토리 계층에서는 영속성 컨텍스트에 관리되면서 영속 상태를 유지하지만 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 됨

 

 

부연 설명

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

 

가. 준영속 상태와 변경 감지

  • 변경 감지 기능은 영속성 컨텍스트가 살아 있는 트랜잭션 범위까지만 동작하고 영속성 컨텍스트가 종료된 프레젠테이션 계층에서는 동작하지 않음
  • 보통 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생하는데 단순히 데이터를 보여주기만 하는 프레젠테이션 계층에서 데이터를 수정할 일은 거의 없음
    • 오히려 프레젠테이션 계층에서도 변경 감지 기능이 동작할 경우 애플리케이션 계층 간 책임이 모호해지고 유지보수성이 떨어짐
    • 따라서 비즈니스 로직은 서비스 계층에서 끝내고 프레젠테이션 계층은 데이터를 보여주는데 집중하는 것을 권장

 

나. 준영속 상태와 지연 로딩

  • 준영속 상태의 가장 골치 아픈 문제는 지연 로딩 기능이 동작하지 않는다는 점
    • 만약 하이버네이트를 구현체로 사용하는 와중 준영속 상태에서 지연 로딩을 시도할 경우 LazyInitializationException 예외 발생

 

  • 준영속 상태의 지연 로딩 문제를 해결하는 방법은 크게 두 가지가 있음
    • 뷰가 필요한 엔티티를 미리 로딩해 두는 방법
    • OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법

 

  • 뷰가 필요한 엔티티를 미리 로딩해 두는 방법은 어디서 미리 로딩하느냐에 따라 세 가지 방법이 있음
    • 글로벌 페치 전략 수정
    • JPQL Fetch Join
    • 강제로 초기화

 

1. 글로벌 페치 전략 수정

  • 가장 간단한 방법은 글로벌 페치 전략을 지연 로딩 (FetchType.LAZY)에서 즉시 로딩 (FetchType.EAGER)로 변경하는 것
    • 엔티티에 있는 fetch 타입을 변경하면 애플리케이션 전체에서 해당 엔티티를 로딩할 때마다 해당 전략을 사용하므로 글로벌 페치 전략이라고 부름

 

 

  • 하지만 이렇게 글로벌 페치 전략을 즉시 로딩으로 설정할 경우 두 가지 단점이 따라옴
    • 사용하지 않는 엔티티를 로딩
    • N + 1 문제 발생

 

1.1 사용하지 않는 엔티티를 로딩한다

  • 화면 A에서 order와 member 둘 다 필요해서 글로번 전략을 즉시 로딩으로 설정했다고 가정
    • 화면 B는 order 엔티티만 있으면 충분한데 화면 B는 즉시 로딩 전략으로 인해 order를 조회하면서 사용하지 않는 member도 함께 조회

 

1.2 N + 1 문제가 발생한다

  • JPA를 사용하면서 성능상 가장 조심해야 하는 것이 바로 N + 1 문제
    • N + 1이 발생하면 SQL이 상당히 많이 호출되므로 조회 성능에 치명적이며 최우선 최적화 대상
    • N + 1 문제는 JPQL 페치 조인으로 해결 가능

 

 

부연 설명

  • JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용
    • 따라서 즉시 로딩이든 지연 로딩이든 구분하지 않고 JPQL 쿼리 자체에 충실하게 SQL을 생성함

 

  • 코드 동작 순서는 다음과 같음
    • `SELECT o FROM Order o` JPQL을 분석해서 `SELECT * FROM Order` SQL을 생성
    • DB에서 결과를 받아 order 엔티티 인스턴스들을 생성
    • Order.member의 글로벌 페치 전략이 즉시 로딩이므로 order를 로딩하는 즉시 연관된 member도 로딩해야 함
    • 연관된 member를 영속성 컨텍스트에서 찾음
    • 만약 영속성 컨텍스트에 없으면 `SELECT * FROM MEMBER WHERE id = ?` SQL을 조회한 order 엔티티 수만큼 실행 
      • 이처럼 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N + 1 문제라고 지칭

 

2. JPQL 페치 조인

  • 글로벌 페치 전략을 즉시 로딩으로 설정할 경우 애플리케이션 전체에 영향을 끼치므로 너무 비효율적
  • JPQL 페치 조인은 N + 1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 현실적인 방법
    • 페치 조인은 조인 명령어 마지막에 fetch를 넣어주면 됨
    • 페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회하기 때문에 N + 1 문제가 발생하지 않음
    • 연관된 엔티티를 이미 로딩했으므로 글로벌 페치 전략은 무의미

 

 

2.1 JPQL 페치 조인의 단점

  • N + 1 문제에 대한 현실적인 대안이긴 하지만 무분별하게 사용할 경우 화면에 맞춘 레포지토리 메서드가 증가할 수 있음
    • 결국 프레젠테이션 계층이 알게 모르게 데이터 접근 게층을 침범하는 것
    • ex) 화면 A는 order 엔티티만 필요하고 화면 B는 order 엔티티와 연관된 member 엔티티 둘 다 필요하다고 가정할 경우 두 화면을 모두 최적화하기 위해 둘을 지연 로딩으로 설정하고 화면 A를 위해 order만 조회하는 메서드와 화면 B를 위해 order와 연관된 member를 페치 조인하는 메서드를 정의하는 것이 최적, 이처럼 메서드를 각각 생성하면 최적화는 할 수 잇지만 뷰와 레포지토리 간에 논리적인 의존 관게가 발생

 

  • 앞선 예제의 대안으로, order와 연관된 member를 페치 조인하는 메서드 하나만 정의하고 화면 A와 B에서 이를 호출하는 것이 최적일 수 있음
    • 화면 A는 order 엔티티만 필요해 약간의 로딩 시간 증가가 발생할 수 있지만, 페치 조인은 JOIN을 통해 한 번의 쿼리로 데이터를 조회하므로 성능 영향은 미미할 것

 

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

 

3. 강제로 초기화

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

 

 

부연 설명

  • 글로벌 페치 전략을 지연 로딩으로 설정하면 연관된 엔티티를 실제 엔티티가 아닌 프록시 객체로 조회
    • 프록시 객체는 실제 사용하는 시점에 초기화 됨

 

  • 위 예제처럼 프레젠테이션 계층에서 필요한 프록시 객체를 영속성 컨텍스트가 살아 있을 때 강제로 초기화해서 반환하면 이미 초기화했으므로 준영속 상태에서도 사용 가능
    • Hibernate를 사용하면 initialize() 메서드를 사용해서 프록시를 강제로 초기화 가능

 

  • 위 예제처럼 프록시를 초기화하는 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 하는데 이 것은 은근슬쩍 프레젠테이션 계층이 서비스 계층을 침범하는 상황
    • 따라서 비즈니스 로직을 담당하는 서비스 계층에서 프레젠테이션 계층을 위한 프록시 초기화 역할을 분리해야 함
    • FACADE 게층이 그 역할을 담당해줄 것

 

4. FACADE 계층 추가

  • 프레젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 추가하면, 뷰를 위한 프록시 초기화를 해당 계층에서 담당할 수 있음
    • 이에 따라 서비스 계층은 프레젠테이션 계층을 위해 프록시를 초기화하지 않아도 됨
    • 결과적으로 FACADE 계층을 도입해서 서비스 계층과 프레젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있음
    • 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 FACADE에서 트랜잭션을 시작해야 함

 

https://milenote.tistory.com/145

 

4.1 FACADE 계층의 역할과 특징

  • 프레젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리해 줌
  • 프레젠테이션 계층에서 필요한 프록시 객체를 초기화
  • 서비스 계층을 호출해서 비즈니스 로직을 실행
  • 레포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 조회
  • 실용적인 관점에서 볼 때 FACADE의 최대 단점은 중간에 계층이 하나 더 끼어들었다는 점
    • 더 많은 코드를 작성해야 하고
    • FACADE에 단순히 서비스 계층을 호출만 하는 위임 코드가 상당히 많을 것

 

 

부연 설명

  • OrderService에 있던 프록시 초기화 코드를 OrderFacade로 이동함
  • FACADE 계층을 사용해서 서비스 계층과 프레젠테이션 계층 간에 논리적 의존 관계를 제거함
    • 이제 서비스 계층은 비즈니스 로직에 집중하고 프레젠테이션 계층을 위한 초기화 코드는 모두 FACADE가 담당

 

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

  • 뷰를 개발할 때 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류가 발생할 가능성이 높음
    • 보통 뷰를 개발할 때는 엔티티 클래스를 보고 개발하지 이것이 초기화되어 있는지 여부를 확인하기 위해 FACADE나 서비스 클래스까지 열어보는 것은 상당히 번거롭고 놓치기 쉽기 때문
    • 결국 영속성 컨텍스트가 없는 뷰에서 초기화하지 않은 프록시 엔티티를 조회하는 실수를 하게 되고 LazyInitializationException을 만나게 될 것

 

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

 

OSIV (Open Session In View)

  • 영속성 컨텍스트를 뷰까지 열어둔다는 뜻
    • 영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지되므로 뷰에서도 지연 로딩 사용 가능

 

1. 과거 OSIV: 요청 당 트랜잭션

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

 

https://ddd4117.github.io/2021/06/jpa-osivopen-session-in-view/

 

부연 설명

  • 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성하면서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료함
    • 이렇게 하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 엔티티도 영속 상태를 유지
    • 뷰에서도 지연 로딩을 할 수 있으므로 엔티티를 미리 초기화할 필요 없음
    • 뷰에서도 지연 로딩을 할 수 있게 됨에 따라 FACADE 계층 없이도 뷰에 독립적인 서비스 계층을 유지할 수 있음

 

1.1 요청 당 트랜잭션 방시기의 OSIV 문제점

  • 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있는 것이 단점
    • ex) 고객명을 출력해야 하는데 보안 정책 때문에 마스킹 처리해서 출력해야 할 경우 setter 메서드를 통해 고객명을 XXX로 변경해서 렌더링 할 뷰에 넘겨주었다고 가정했을 때 변경 감지 기능에 의해 고객명이 변경되는 심각한 문제 직면
    • 개발자는 단순히 뷰에 노출할 때만 고객명을 XXX로 변경하고 싶었는데 의도와 달리 고객명 데이터 자체가 XXX로 변경됨

 

  • 서비스 계층처럼 비즈니스 로직을 실행하는 곳에서 데이터를 변경하는 것은 당연하지만 프레젠테이션 계층에서 데이터를 잠시 변경했다고 실제 데이터베이스까지 변경 내용이 반영되면 애플리케이션을 유지보수하기 상당히 힘들어짐
  • 위 문제를 해결하려면 프레젠테이션 계층에서 엔티티를 수정하지 못하게 막아야 하며 막는 방법은 다음과 같음
    • 엔티티를 읽기 전용 인터페이스로 제공
    • 엔티티 매핑
    • DTO만 반환

 

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

  • 엔티티를 직접 노출하는 대신 읽기 전용 메서드만 제공하는 인터페이스를 프레젠테이션 계층에 제공하는 방법
  • 프레젠테이션 계층은 읽기 전용 메서드만 있는 인터페이스를 사용하므로 엔티티 수정 불가

 

 

나. 엔티티 래핑

  • 엔티티의 일기 전용 메서드만 가지고 잇는 엔티티를 감싼 객체를 생성하고 이것을 프레젠테이션 계층에 반환하는 방법

 

 

다. DTO만 반환

  • 프레젠테이션 계층에 엔티티 대신 단순히 데이터만 전달하는 객체인 DTO (Data Transfer Object)를 생성해서 반환하는 방법
    • 하지만 해당 방법은 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 함

 

 

여태 설명한 방법 모두 코드량이 상당히 증가한다는 단점이 있기 때문에 차라리 프레젠테이션 계층에서 엔티티를 수정하면 안 된다고 개발자들끼리 합의하는 것이 더 실용적일 수 있습니다.

지금까지 설명한 OSIV는 Request per Transaction 방식의 OSIV이며 지금까지 설명했던 문제점들로 인해 최근에는 거의 사용하지 않고 있습니다.

최근에는 상기 언급한 문제점을 어느 정도 보완해서 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용하며 스프링 프레임워크가 제공하는 OSIV가 바로 해당 방식을 사용하는 OSIV입니다.

 

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

  • 스프링 프레임워크의 spring-orm.jar는 다양한 OSIV 클래스를 제공
  • OSIV를 서블릿 필터에서 적용할지 스프링 인터셉터에서 적용할지에 따라 원하는 클래스를 선택해서 사용하면 됨
    • 하이버네이트 OSIV 서블릿 필터: org.springframework.orm.hibernate4.support.OpenSessionInViewFilter
    • 하이버네이트 OSIV 스프링 인터셉터: org.springframework.orm.hibernate4.support.OpenSessionInViewInterceptor
    • JPA OEIV 서블릿 필터: org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor
    • JPA OEIV 스프링 인터셉터: org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor

 

  • 이전에 설명했던 Request per Transaction OSIV는 프레젠테이션 계층에서 데이터를 변경할 수 있다는 문제점이 있었음
  • 스프링 프레임워크가 제공하는 OSIV는 위 문제를 어느 정도 해결함
    • 스프링 프레임워크가 제공하는 OSIV는 `비즈니스 계층에서 트랜잭션을 사용하는 OSIV`
    • 이름 그대로 OSIV를 사용하기는 하지만 트랜잭션은 비즈니스 계층에서만 사용한다는 뜻

 

https://ddd4117.github.io/2021/06/jpa-osivopen-session-in-view/

 

동작 원리

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

 

2.1 트랜잭션 없이 읽기 (Nontransactional Reads)

  • 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 함
    • 만약 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 flush 하면 javax.persistence.TransactionRequiredException 예외 발생

 

  • 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 `트랜잭션 없이 읽기`라 지칭
    • 프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능

 

  • 스프링이 제공하는 OSIV를 사용하면 프레젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없음 즉, 프레젠테이션 계층에서 엔티티를 수정할 수 있는 기존 OSIV의 단점을 보완함
    • 그리고 `트랜잭션 없이 읽기`를 사용해서 프레젠테이션 계층에서 지연 로딩 기능을 사용할 수 있음

 

 

부연 설명

  • 프레젠테이션 계층이지만 아직 영속성 컨텍스트가 살아있기 때문에 만약 영속성 컨텍스트를 flush 하면 변경 감지가 동작해서 DB에 해당 회원의 이름을 XXX로 변경할 것
  • 다행히도 여기서는 두 가지 이유로 flush가 동작하지 않음
    • 트랜잭션을 사용하는 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 이미 flush 해버렸고 스프링이 제공하는 OSIV 서블릿 필터나 OSIV 스프링 인터셉터는 요청이 끝나면 flush를 호출하지 않고 em.close()로 영속성 컨텍스트만 종료해 버리므로 flush가 일어나지 않음
    • 프레젠테이션 계층에서 em.flush()를 호출해서 강제로 flush해도 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외가 발생함

 

2.2 스프링 OSIV 주의사항

  • 스프링 OSIV를 사용하면 일반적으로 프레젠테이션 계층에서 엔티티를 수정해도 수정 내용을 DB에 반영하지 않지만 여기에는 한 가지 예외가 존재함
    • 프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제 발생

 

 

https://loosie.tistory.com/796

 

부연 설명

  • 컨트롤러에서 회원 엔티티를 조회하고 고객명을 "XXX"로 수정
  • biz() 메서드를 실행해서 트랜잭션이 있는 비즈니스 로직 실행
  • 트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션을 시작하고 biz() 메서드 실행
  • biz() 메서드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 flush
    • 이때 변경 감지가 동작하면서 회원 엔티티의 수정 사항을 DB에 반영

 

정리하면 컨트롤러에서 엔티티를 수정하고 즉시 뷰를 호출한 것이 아니라 트랜잭션이 동작하는 비즈니스 로직을 호출했으므로 위와 같은 문제가 발생합니다.

  • 문제를 해결하는 단순한 방법은 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 난 뒤 엔티티를 변경하는 것

 

 

 

3. OSIV 정리

 

3.1 스프링 OSIV의 특징

  • OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지하므로 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태를 유지함
  • 엔티티 수정은 트랜잭션이 있는 계층에서만 동작
  • 트랜잭션이 없는 프레젠테이션 계층은 지연 로딩을 포함해서 조회만 가능

 

3.2 스프링 OSIV의 단점

  • OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 함
    • 특히 트랜잭션 롤 백 시 주의해야 함

 

  • 프레젠테이션 계층에서 엔티티를 수정한 직후 비즈니스 로직을 수행하면 변경 감지 기능에 의해 엔티티가 수정될 수 있음
  • 프레젠테이션 계층에서 지연 로딩에 의한 SQL이 실행되므로 성능 튜닝 시 확인해야 할 부분이 넓음

 

3.3 OSIV vs FACADE vs DTO

  • OSIV를 사용하지 않는 대안으로는 FACADE 계층을 활용하거나 이를 변형한 다양한 방법이 있으며, 어떤 방법을 선택하든 결국 준영속 상태가 되기 전에 프록시를 초기화해야 함
  • 다른 방법은 엔티티를 직접 노출하지 않고 엔티티와 거의 유사한 DTO를 생성해서 반환하는 것
  • 어떤 방법을 사용하든 OSIV 사용하는 것과 비교했을 때 보다 많은 코드를 작성해야 한다는 단점 존재

 

3.4 OSIV를 사용하는 방법이 만능은 아니다

  • OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있지만 복잡한 화면을 구성할 때는 해당 방법이 효과적이지 않은 경우가 많음
    • ex) 복잡한 통계 화면은 엔티티로 조회하기보다는 처음부터 통계 데이터를 구상하기 위한 JPQL을 작성해서 DTO로 조회하는 것이 효과적
    • ex) 수많은 테이블을 조인해서 보여주어야 하는 복잡한 관리자 화면도 객체 그래프로 표현하기 어려운 경우가 많으며 이때도 엔티티를 직접 조회하기보다는 JPQL로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 더 나은 해결책

 

3.5 OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다

  • OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없음
    • ex) JSON이나 XML을 생성할 때는 지연 로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하므로 클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 함

 

  • 위와 같이 JSON으로 생성한 API는 한 번 정의하면 수정하기 어려운 외부 API와 언제든지 수정할 수 있는 내부 API로 나눌 수 있음
    • 외부 API: 외부에 노출하므로 한 번 정의하면 변경이 어려움 (서버, 클라이언트를 동시에 수정하기 어려움)
    • 내부 API: 외부에 노출하지 않으므로 의존하는 코드가 없으므로 언제든지 변경 가능 

 

  • 엔티티는 생각보다 자주 변경되고 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티를 변경할 때 노출하는 JSON API도 함께 변경됨
    • 이에 따라 외부 API는 엔티티를 직접 노출하기보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전함

 

너무 엄격한 계층

  • OSIV를 사용하기 전에는 프레젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화해야 했고 초기화는 아직 영속성 컨텍스트가 살아있는 서비스 게층이나 FACADE 계층이 담당했음
  • 하지만 OSIV를 사용하면 영속성 컨텍스트가 프레젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없음
  • 따라서 단순한 엔티티 조회는 아래 코드처럼 컨트롤러에서 레포지토리를 직접 호출해도 아무런 문제가 없음

 

 

https://loosie.tistory.com/796

 

참고

자바 ORM 표준 JPA 프로그래밍 - 김영한 저

 

반응형