티스토리 뷰

반응형


| 웹 애플리케이션과 영속성 관리



컨테이너 환경에서 JPA가 동작하는 내부 동작 방식



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



스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 제공하는 전략을 따라야 함


||| 스프링 컨테이너의 기본 적략



> 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용

- 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다 !

- 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료

- 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근

  ㄴ 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용

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

  ㄴ 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름 (멀티스레드 상황에 안전)


> 트랜잭션 동작

1. @Transactional 어노테이션이 있으면 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작

- 트랜잭션은 보통 서비스 계층에서 시작하므로

  서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료

2. 스프링 트랜잭션 AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작

3. 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료

3-1. 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시

3-2. 변경 내용을 데이터베이스에 반영한 후 데이터베이스 트랜잭션을 커밋

3-3. 예외 발생 시 트랜잭션을 롤백하고 종료 (이 때 플러시는 호출하지 않음)




|| 준영속 상태와 지연 로딩



* 조회한 엔티티가 Service와 Repository 계층에서는 영속성 컨텍스트에 관리되면서 영속 상태를 유지하지만,

  Controller나 View 같은 프리젠테이션 계층에서는 준영속 상태가 됨 (변경 감지와 지연로딩이 동작하지 않음)

- 보통 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생

- 단순히 데이터를 보여주기만 하는 프리젠테이션 계층에서 데이터를 수정할 일은 거의 없음


* 준영속 상태의 문제는 지연 로딩 기능이 동작하지 않는 점

- 해결 방법으로 크게 두 가지가 있음

1. View가 필요한 엔티티를 미리 로딩

=> 어디서 미리 로딩하느냐에 따라 세 가지 방법으로 나뉨

1-1. 글로벌 페치 전략 수정

1-2. JPQL 페치 조인

1-3. 강제로 초기화

2. OSIV를 사용해서 엔티티를 항상 영속 상태로 유지


||| 1.1 글로벌 페치 전략 수정


- 가장 간단한 방법

- 글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경

- @ManyToOn(fetch = FatchType.EAGER)


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

- 사용하지 않는 엔티티를 로딩

- N+1 문제 발생 : 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회 (조회 성능에 치명적),

   (영속성 컨텍스트에 찾고자하는(member) 엔티티가 없으면 대상(order) 엔티티 수만큼 SQL 실행)

                 JPQL 페치 조인으로 해결 가능


||| 1.2 JPQL 페치 조인


- JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있음

- N+1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 현시적인 방법

select o

from Order o

join fetch o.member


> JPQL 페치 조인의 단점

- 무분별하게 사용 시 화면에 맞춘 Repository 메소드가 증가

- 결국 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범


||| 1.3 강제 초기화


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

- 강제로 초기화해서 반환하였으므로 준영속 상태에서도 사용 가능

1
2
3
4
5
6
7
8
9
class OrderService {
 
    @Transactional
    public Order findOrder(id) {
        Order order = orderRepository.findOrder(id);
        order.getMember().getName();    // 프록시 객체를 강제로 초기화
        return order;
    }
}
cs


> 강제 초기화의 단점

- 프록시를 초기화하는 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 함

- 프리젠테이션 계층이 서비스 계층을 침범하는 상황

- FACADE 계층이 서비스 계층에서 프리젠테이션 계층을 위한 프록시 초기화 역할을 담당


||| FACADE 계층


- 서비스 계층과 프리젠테이션 계층 사이의 논리적인 의존성을 분리

- 프록시를 초기화하기 위해 영속성 컨텍스트가 필요하므로 FACADE에서 트랜잭션을 시작


> FACADE 계층의 역할과 특징

- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리

- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화

- 서비스 계층을 호출해서 비즈니스 로직을 실행

- Repository를 직접 호출해서 뷰가 요구하는 엔티티를 찾음


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class OrderFacade {
 
    @Autowired OrderService orderService;
 
    public Order findOrder(id) {
        Order order = orderService.findOrder(id);
        // 프리젠테이션 계층이 필요한 프록시 객체를 강제로 초기화
        order.getMember().getName();
        return order;
    }
}
 
 
class OrderService {
 
    public Order findOrder(id) {
        return orderRepository.findOrder(id);
    }
}
cs


||| 2.1 OSIV


- 영속성 컨텍스트를 뷰까지 살아있게 열어두어 뷰에서도 지연 로딩을 사용할 수 있도록 하는 방법

(준영속 상태와 지연 로딩의 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생)

- Open Session In View 는 영속성 컨텍스트를 뷰까지 열어둔다는 의미




|| OSIV



> 스프링 프레임워크의 spring-orm.jar는 다양한 OSIV 클래스를 제공

- OSIV를 서블릿 필터에서 적용할지 스프링 인터셉터에서 적용할지에 따라 원하는 클래스를 선택


* JPA OEIV 서블릿 필터 : org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter

* JPA OEIV 스프링 인터셉터 : org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor


> 스프링 프레임워크가 제공하는 OSIV는 "비즈니스 계층에서 트랜잭션을 사용하는 OSIV"

- 트랜잭션은 비즈니스 계층에서만 사용


> 스피링 OSIV의 동작 원리

1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성

(단, 트랜잭션은 시작하지 않음)

2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때

1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작

3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 풀러시

(트랜잭션은 끝내지만 영속성 큰텍스트는 종료하지 않음)

4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지

5. 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료

플러시를 호출하지 않고 바로 종료


> 스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV 특징

- 영속성 컨텍스트를 프리젠테이션 계층까지 유지

- 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티 수정 불가

- 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩 가능


c. spring OSIV 적용 뒤 상태

- setName()으로 회원 엔티티를 변경했지만 플러시가 동작하지 않음

* 서비스 계층이 끝날 때 이미 트랜잭션이 커밋되면서 플러시해버렸기 때문에

* 스프링 제공 OSIV는 요청이 끝나면 플러시를 호출하지 않고 em.close()로 영속성 컨텍스트만 종료

* em.flush()를 호출해도 트랜잭션 범위 밖이므로 데이터 수정 불가

1
2
3
4
5
6
7
8
class MemberController {
 
    public String viewMember (Long id) {
        Member member = memberService.getMember(id);
        member.setName("XXX");  // 보안상의 이유로 고객 이름을 XXX로 변경
        model.addAttribute("member", member);
    }
}
cs


> 스프링 OSIV 주의사항

- 프리젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출할 경우 

  수정한 내용이 반영되는 문제 발생

- 스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션에서 공유할 수 있으므로 이러한 문제가 발생

- OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은

  트랜잭션 생명주기와 영속성 컨텍스트의 생명주기가 같으므로 이런 문제가 발생하지 않음




출처 : 자바 ORM 표준 JPA 프로그래밍


반응형
댓글
최근에 올라온 글
최근에 달린 댓글
링크
Total
Today
Yesterday