자바 ORM 표준 JPA 프로그래밍 - 기본편 챕터 13 정리

정종일·2023년 6월 30일
0

Spring

목록 보기
17/18

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


  • J2SE 환경 : 개발자가 직접 엔티티 매니저를 생성, 트랜잭션 관리
  • J2EE, 스프링 : 컨테이너가 제공하는 전략을 따라야 함

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


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

  • 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료
  • 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근
  • 스프링 프레임워크를 사용하면 매소드 실행 직전에 스프링의 트랜잭션AOP가 먼저 동작
  1. 메소드에 @Transactional 을 선언해서 메소드를 호출할 때 트랜잭션을 먼저 시작
  2. 조회한 엔티티는 트랜잭션 범위 안에 있으므로 영속성 컨텍스트의 관리를 받는다
  3. 선언한 메소드가 정상 종료되면 트랜잭션을 커밋하는데, 이 때 영속성 컨텍스트를 종료. 영속성 컨텍스트가 사라졌으므로 엔티티는 준영속 상태
  4. 서비스 메소드가 끝나면서 트랜잭션과 영속성 컨텍스트 종료. 컨트롤러에 반환된 엔티티는 준영속 상태
💡 트랜잭션이 같으면 같은 영속성 컨텍스트 다르면 다른 영속성 컨텍스트 사용

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


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

  • 준영속 상태와 변경감지
→ 변경감지 기능은 영속성 컨텍스트가 살아있는 서비스 계층까지만 동작.  프리젠테이션 계층에서도 동작하면 애플리케이션 계층이 가지는 책임이 모호해지며 범위가 넓어져 유지보수 또한 어려워진다.
  • 준영속 상태와 지연로딩 → 준영속 상태의 가장 골치아픈 문제는 지연로딩이 동작하지 않는다는 점. 문제 해결 방법은 크게 두가지가 있다
    1. 뷰가 필요한 엔티티를 미리 로딩해두는 방법
      1. 글로벌 페치 전략 수정
      2. JPQL 페치 조인
      3. 강제 초기화
    2. OSIV를 사용하여 엔티티를 항상 영속 상태로 유지하는 방법

1. 글로벌 페치 전략 수정


글로벌 페치 전략을 지연로딩에서 즉시로딩으로 변경하면 된다. 하지만 단점이 2가지 있다.

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

  2. N+1 문제가 발생한다

    → 이 문제는 JPQL 페치 조인으로 해결 가능

2. JPQL 페치 조인


글로벌 페치전략을 즉시 로딩으로 설정하면 어플리케이션 전체에 영향을 주므로 너무 비효율적이다

페치조인을 사용하면 SQL JOIN 을 사용하여 페치 조인 대상까지 함께 조회한다

  • 페치조인의 단점
    1. 무분별하게 사용하면 화면에 맞춘 리포지토리 메소드가 증가
    2. 뷰와 리포지토리 간에 논리적인 의존관계가 발생

3. 강제 초기화


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

4. FACADE 계층 추가


프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두어 의존성을 분리하는 방법

  • 뷰를 위한 프록시 초기화 담당
  • 서비스 계층은 프리젠테이션 계층을 위해 프록시를 초기화하지 않아도

FACADE의 역할과 특징

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

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


모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 영속성 컨텍스트를 뷰까지 살아있게 열어두자. 이 방법이 OSIV 이다

3. OSIV


Open Session In View

영속성 컨텍스트를 뷰까지 열어둔다는 뜻. 따라서 뷰에서도 지연로딩 사용가능

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


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

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


컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 문제가 존재한다.

이를 해결하기 위한 방법들은 아래와 같다

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

    → 읽기전용 메소드만 있는 인터페이스를 사용하면 엔티티 수정이 불가능하다

  • 엔티티 래핑

    → 읽기 메소드만 존재하는 래퍼 객체를 만든다

  • DTO만 반환

    → 가장 전통적인 방법. 프리젠테이션 계층에 엔티티 대신 가장 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환하는 것. 하지만 이 방법은 OSIV의 장점을 살릴 수 없고 엔티티와 거의 비슷한 DTO클래스도 하나 더 필요하다.

위 방법들은 코드량이 상당히 증가한다. 위 문제점들 덕에 최근에는 거의 사용하지 않는다. 이러한 문제점들을 보완해서 비지니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다. (스프링 프레임워크가 제공하는 OSIV)

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


스프링 프레임 워크가 제공하는 OSIV

스프링 프레임워크가 제공하는 OSIV 라이브러리


  • 하이버네이트 OSIV 서블릿 필터
  • 하이버네이트 OSIV 스프링 인터셉터
  • JPA OEIV 서블릿 필터
  • JPA OEIV 스프링 인터셉터

** OEIV : Open EntityManager In View

스프링 OSIV 분석


비지니스 계층에서 트랜잭션을 사용하는 OSIV.

  1. 클라이언트의 요청이 들어오면 영속성 컨텍스트를 생성한다. 하지만 트랜잭션을 시작하지는 않는다.
  2.  서비스 계층에서 트랜잭션을 시작하면 앞에서 생성해 둔 영속성 컨텍스트에 트랜잭션을 시작한다.
  3. 비즈니스 로직을 실행하고 서비스 계층이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트 플러시 한다. (트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다.)
  4. 클라이언트의 요청이 끝날 때 영속성 컨텍스트를 종료한다.

트랜잭션 없이 읽기


트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 TransactionRequiredException 이 발생한다. 단순히 조회만 할 경우 트랜잭션이 없어도 되는데 이것을 트랜잭션 없이 읽기라 한다.

  • 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다
  • 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다.

스프링이 제공하는 OSIV를 사용하면 프리젠테이션 계층에서는 트랜잭션이 없으므로 엔티티 수정이 불가하다. 트랜잭션 OSIV는 다음과 같은 특징이 있다.

  • 영속성 컨텍스트를 프리젠테이션 계층까지 유지
  • 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정할 수 없음
  • 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연로딩 가능
class MemberController {
		public String viewMember (Long id) {
				Member member = memberService.getMember(id);
				member.setName("XXX");
				model.addAttribute("member", member);
		}
}

위에서는 2가지 이유로 플러시가 동작하지 않는다

  1. 영속성 컨텍스트 변경 내용을 데이터베이스에 반영하려면 영속성 컨텍스트를 플러시해야하지만 트랜잭션을 사용하는 서비스 계층이 끝날 때 트랜잭션이 커밋되며 이미 플러시를 해버림.
    스프링 OSIV서블릿 필터나 인터셉터는 요청이 끝나면 영속성 컨텍스트만 종료해버리므로 플러시가 일어나지 않음
  2. 프리젠테이션 계층에서 em.flush() 를 호출해서 강제로 플러시해도 트랜잭션 범위 밖이라 TransactionRequiredException 이 발생

스프링 OSIV 주의사항


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

class MemberController {
		public String viewMember (Long id) {
				Member member = memberService.getMember(id);
				member.setName("XXX");
		
				memberService.biz();
				return "view";
		}
}

class MemberService {
		
		@Transactional
		public void biz() {
				// 비지니스 로직...
		}
}
  1. 컨트롤러에서 회원 엔티티를 조회하고 이름을 수정
  2. biz() 메소드를 실행해서 트랜잭션이 있는 비즈니스 로직 실행
  3. 트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션을 시작. 그 후 biz() 메소드 실행
  4. 메소드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시
  5. 변경감지 동작, 수정사항 반영

** 순서를 바꾼다면 문제가 발생하지 않는다. ( biz() 실행 후 setName() )

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

3. OSIV 정리


  • 스프링 OSIV의 특징

    1. 한번 조회한 엔티티는 요청이 끝날 때 까지 영속 상태를 유지
    2. 엔티티 수정은 트랜잭션이 있는 계층에서만 동작
  • 스프링 OSIV 단점

    1. 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다. 특히 롤백에 주의
    2. 엔티티 수정 후 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다
    3. 프리젠테이션 계층에서 지연로딩에 의한 SQL이 실행. 성능 튜닝시에 확인해야 할 부분이 넓다
  • OSIV vs FACADE vs DTO

    → 준영속 상태가 되기 전에 프록시를 초기화해야한다. 어떤 방법을 사용하든 OSIV를 사용하는것과 비교해서 코드량이 많아진다

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

    → 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다. 하지만 복잡한 화면을 구성할 때 비효율적이다. 필요한 데이터만 DTO로 조회하는 것이 효과적이며 수많은 테이블 조인이 있을 경우 표현하기 어렵다.

  • OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용 불가

    → JVM을 벗어난 원격 상황에서는 사용 불가. JSON이나 XML을 생성할 때 지연로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가
    결국 클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 한다. 이렇게 JSON으로 생성한 API는 한번 정의하면 수정하기 어려운 외부 API와 언제든지 수정할 수 있는 내부 API로 나눌 수 있다

  1. 외부 API

    1. 변경이 어렵다
    2. 외부에 노출한다
    3. 서버와 클라이언트를 동시 수정이 어렵다
    4. ex) 타 팀과 협업하기 위한 API
  2. 내부 API

    1. 외부에 노출하지 않는다
    2. 언제든지 변경 가능하다
    3. 서버와 클라이언트를 동시에 수정할 수 있다
    4. ex) 같은 프로젝트에 있는 화면을 구성하기 위한 AJAX 호출

    외부 API는 엔티티를 직접 노출하기보다는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다.

    4. 너무 엄격한 계층


    OSIV를 사용하기 전에는 프리젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화해야 했다. 그리고 초기화는 영속성 컨텍스트가 살아있는 서비스 계층이나 FACADE 계층이 담당했다.

    하지만 OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화 할 필요가 없다.

    따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무런 문제가 없다.

profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글