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

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

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

스프링 컨테이너는 **트랜잭션 범위의 영속성 컨텍스트** 전략을 기본으로 사용한다. ( 트랜잭션 범위와 영속성 컨텍스트의 생명주기가 같다)

트랜잭션을 커밋하면 JPA는 영속성 컨텍스트를 플러시해서 변경 내용을 데베에 반영 후 데베 트랜잭션을 커밋한다.
만약 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다.
(즉 1차 캐시의 엔티티 값은 변경됐는데 데베의 값은 변경되지 않음)

여러 스레드가 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름으로 멀티 쓰레드 상황에도 안전하다.

준영속 상태와 지연로딩

주로 서비스 계층에서 트랜잭션을 사용하여 DAO 영역까지만 트랜잭션 상태를 유지한다. 그러므로 웹 애플리케이션 계층의 관점으로 보면 , 컨트롤러와 뷰는 트랜잭션 범위 밖인 것이다 즉 엔티티가 **비,준영속성 상태** 인 것이다. 비,준영속성 상태에서 변경감지와 지연로딩이 동작하지 않는다.
  1. 준영속상태의 변경감지
    이 문제는 별로 중요하지 않는다. 오히려 프레젠테이션 계층에서 변경 감지가 일어나는 것이 애플리케이션 계층이 갖는 책임이 모호하다.

  2. 준영속상태의 지연로딩
    지연로딩이 문제이다.
    연과된 엔티티를 지연로딩으로 설정해서 프록시 객체를 조회한다면 , 아직 초기화되지 않는 프록시 객체를 사용하면 실제 데이터를 불러오려고 초기화를 시도하지만 준영속상태는 영속성 컨텍스트에 없음으로 LazyInitailiaztionException 이 발생한다

지연로딩 문제 해결하기

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

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

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

단점:
1. 사용하지 않는 엔티티를 조회
2. N+1 문제 발생

N+1 문제란 em.find() 로 엔티티를 조회하면 데베에 JOIN 쿼리를 사용해서 한번에 연관된 엔티티까지 조회한다.

Order order = em.find(Order.class ,1L);
select o.*, m.*
	from Order o
    left join Member m on m.Member_ID = m.Member_Id
    where o_id = 1

위의 코드는 문제가 되지 않지만 JPQL 을 사용할 때 문제가 발생하다.

List<Order> orders = em.createQuery("select o from Order o", Order.class).getResultList();
select * from Order;
select * Member where id = ? 
select * Member where id = ? 
select * Member where id = ? 
select * Member where id = ? 
select * Member where id = ? 
...

JPA가 JPQL 을 분석해서 SQL을 생성할 때는 글로벌패치 전략을 참고하지 않는다.

내부적으로
1. select o from Order o -> select from Order 을 생성한다.
2. 데베에서 결과를 받아 Order 엔티티 인스턴스를 생성한다.
3. Order.member의 글로벌 페치 전략이 즉시 로딩임으로 연관된 member도 로딩해야한다.
4. 연관된 member를 영속성 컨텍스트에서 찾는다.
5. 영속성 컨텍스트에 없으면 select
from member where id = ? 을 조회한 order 엔티티 수만큼 수행한다.

주문 엔티티가 10개면 맴버를 조회하는 쿼리를 10번 실행한다.

1-2 JPQL 페치 조인

select o from Order o -> select o from Order o join fetch o.member 

sql
select o.*, m.*
	from order o
    join member m on o.member_id = m.member_id 

페치 조인을 활용하면 N+1 문제가 발생하지 않는다. 연관된 엔티티를 이미 로딩했음으로 글로벌 페치 전략을 의미가 없다.

페치 조인의 단점은 레파지토리 계층이 프리젠테이션 계층에 논리적으로 영향을 받는다는 것이다.
A 페이지에서는 Order 만 필요하고, B 페이지에서는 Order와 주문에 연관된 Member 가 필요하다면
페지조인 JPQL 문을 두개 만들어야한다. 이는 논리적 문제를 발생한다.
논리적 문제를 해결하기 위해 주문과 맴버를 한번에 조회하는 JPQL 문을 하나만 작성하고 A 페이지에서도 불필요한 맴버를 로딩하는 것이다. 페치조인을 사용하면 하나의 쿼리문을 날리기에 큰 문제는 되지 않는다.

강제로 초기화

서비스 계층에서 연관된 엔티티를 강제로 초기화하는 것이다.

order.member.getName(); // 초기화

하지만 비지니스 로직을 처리해야하는 서비스 계층에서 프렌젠테이션 계층의 일까지 맡고 있음으로 문제가 된다.
이를 해결하기 위해서는 프레젠테이션 계층과 서비스 계층 사이에 계층 하나를 끼워 넣을 수 있다 (FACADE 계층)

FACADE 계층 추가

뷰를 위한 프록시 객체를 초기화하는 계층이다.FACADE 계층부터 트랜잭션을 시작하는 것이다.

이 방법들은 모두 프레젠테이션 계층이 준영속 상태임으로 제약되는 것들이 너무 많다 (계층간 논리적 의존 침해..)

OSIV ; open session in view

영속성 컨텍스트를 뷰까지 열어둔다는 의미

클라리언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉트에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션을 종료한다(요청 당 트랜잭션 Transaction Per request)

하지만 보안 등의 어떠한 이유로 컨트롤러 계층에서 일시적으로 데이터를 변경해서 사용자에게 보여주려고 시도할때 문제가 발생한다. 왜냐하면 현재 컨틀롤러 계층도 트랜잭션 안에 포함됐고, 요청이 끝날때 flash를 발생하기 때문에, 일시적으로 바꾼 데이터의 값이 데베에 적용되기 때문이다.

해결방법
1. 엔티티를 읽기 전용 인터페이스로 제공
2. 엔티티 래핑
3. DTO만 반환

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

interface MemberView {
	public String getName();
}

@Entity
public class Member implements MemberView {
	@Id
    Long id
    
    private String name;
	...

class MemberService {
	public MemberView getMember(id){
    	return memberRepo.findById(id);
        ...
        

엔티티 래핑

class MemberWrapper {
	private Member member;
    
    pulbic MemberWrapper (Member member ){
    	this.member= member ;
   }
   
   public getMember (){
   	return this.member
   }
   // setter 는 없다.

DTO만 반환

하지만 이 방법은 OSIV 방법의 장점을 극대화 할 수 없고 엔티티를 복사한 듯한 DTO 클래스를 하나 더 만들어야한다.

하지만 요청 당 트랜잭션 방식의 OSIV은 코드량이 증가한다. 이로인해 개발자들이 선호하지 않게 되었고,
이를 해결하기 위해 비지니스 계층에서만 트랜잭션을 사용하는 스프링 프레임워크의 OSIV 방식을 사용하게 되었다.

스프링 프레임워크 OSIV

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

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

주의사항

프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.

좋은 웹페이지 즐겨찾기