본문 바로가기
Spring JPA

애플리케이션과 영속성 관리 - 16

by 홍굴이 2021. 8. 8.

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

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

 

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

  • 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같다.
  • 즉, 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다.
  • 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.

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

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

 

스프링 트랜잭션 AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다.

이때 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋한다.

예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다.

  • 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
    즉, 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
  • 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.
    즉, 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
    다시말해서, 스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당한다.

준영속 상태와 지연 로딩

트랜잭션은 보통 서비스 계층에서 시작하므로 끝나는 시점에 트랜잭션이 종료되면서 컨트롤러나 뷰같은 프리젠테이션 계층에서는 준영속 상태가 된다.

따라서 연관관계를 지연 로딩(LAZY)로 설정하고 View 혹은 Controller에서 호출하면 예외가 발생한다.

 

준영속 상태와 변경 감지

  • 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생한다.
  • 단순히 데이터만 보여줘야하는 프리젠테이션 계층에서 데이터를 수정하는 일은 없어야 하므로 문제가 되지 않는다.

준영속 상태와 지연 로딩

  • 연관된 엔티티를 지연 로딩으로 설정하면 프록시 객체로 조회한다.
  • 프록시 객체를 조회하면 실제 데이터를 불러오려고 초기화를 시도한다.
  • 준영속 상태에서는 영속성 컨텍스트가 없으므로 지연 로딩을 할 수 없으므로 예외가 발생한다.

해결 방법

  • 뷰가 필요한 엔티티를 미리 로딩해두는 방법
  • OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법

뷰가 필요한 엔티티를 미리 로딩하는 방법 - 글로벌 페치 전략 수정

연관 관계를 지연 로딩에서 즉시 로딩으로 변경하면 된다.

 

단점

  • 사용하지 않는 엔티티를 로딩한다.
  • N+1 문제가 발생한다.

N+1 문제

JPQL로 엔티티를 조회하는 상황을 가정해보자.

List<Entity> entityList = 
    entityManager.createQuery("select e from Entity e", Entity.class)
    .getResultList();

실행된 SQL

select * from Entity	//JPQL로 실행된 SQL
select * from AssociationEntity where id=?	//EAGER로 실행된 SQL
select * from AssociationEntity where id=?	//EAGER로 실행된 SQL
select * from AssociationEntity where id=?	//EAGER로 실행된 SQL
select * from AssociationEntity where id=?	//EAGER로 실행된 SQL
select * from AssociationEntity where id=?	//EAGER로 실행된 SQL
select * from AssociationEntity where id=?	//EAGER로 실행된 SQL
...
...

JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다.

  1. JPQL을 분석해서 select * from Entity를 생성한다.
  2. 데이터베이스 결과를 받아 Entity 엔티티 인스턴스를 생성한다.
  3. Entity.associationEntity의 글로벌 페치 전략이 즉시 로딩이므로 entity를 로딩하는 즉시 연관된 associationEntity도 로딩해야 한다.
  4. 연관된 associationEntity를 영속성 컨텍스트에서 찾는다.
  5. 영속성 컨텍스트에 없다면 SQL을 조회한 entity의 수만큼 실핸한다.

뷰가 필요한 엔티티를 미리 로딩하는 방법 - JPQL 페치 조인

JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인 전략을 사용한다.

이것은 N+1 문제를 해결할 수 있다.

 

페치 조인은 조인 명령어 마지막에 fetch를 넣어주면 된다.

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

페치 조인은 N+1문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 현실적인 방법이다.

 

단점

무분별하게 사용하면 화면에 맞춘 레파지토리 메소드가 증가할 수 있다.

결국 프리젠테이션 계층에 알게 모르게 데이터 접근 계층을 침범하는 것이다.


뷰가 필요한 엔티티를 미리 로딩하는 방법 - 강제로 초기화

class OrderService {

    @Transactional
    public OrderEntity findOrder(Long id) {
    	OrderEntity orderEntity = orderRepository.findOrder(id);
        orderEntity.getMember().getName();	//프록시 강제로 초기화
        return orderEntity;
    }
    
}

위의 코드처럼 실제 값을 사용하여 값을 초기화하면 준영속 상태에서도 사용할 수 있다.

 

하이버네이트를 사용하면 initialize() 메소드를 사용해서 프록시를 강제로 초기화할 수 있다.

org.hibernate.Hibernate.initialize(order.getMember());	//프록시 초기화

JPA 표준에는 프록시 초기화 메소드가 없다. JPA 표준은 초기화 여부만 확인할 수 있다.

PersistenceUnitUtil persistenceUnitUtil = 
	entityManager.getEntityManagerFactory().getPersistenceUnitUtil();

boolean isLoaded = persistenceUnitUtil.isLoaded(order.getMember());

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

비즈니스 로직을 담당하는 서비스 계층에서 이렇게 프리젠테이션 계층을 위한 일까지 하는 것은 좋지 않다.


뷰가 필요한 엔티티를 미리 로딩하는 방법 - FACADE 계층 추가

FACADE 계층을 추가하면 뷰를 위한 프록시 초기화는 이곳에서 담당한다.

덕분에 서비스 계층은 프리젠테이션 계층을 위해 프록시 초기화를 하지 않아도 된다.

 

FACADE 계층의 역할과 특징

  • 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리해준다.
  • 프리젠테이션 계층에서 필요한 프록시 객체를 초기화한다.
  • 서비스 계층을 호출해서 비즈니스 로직을 실행한다.
  • 레파지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.
class OrderFacade {
	
    @Autowired
    private OrderService orderService;
    
    public OrderEntity findOrder(Long id) {
    	OrderEntity orderEntity = orderService.findOrder(id);
        orderEntity.getMember().getName();
        return orderEntity;
    }
}
class OrderService {

    public OrderEntity findOrder(Long id) {
    	return orderRepository.findOrder(id);
    }
}

 

 

단점

  • 중간에 계층이 하나 더 끼어든다.

OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법

엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생하는 문제이므로 뷰에서 지연 로딩을 사용할 수 있게 하면 된다.

OSIV (Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.

 

※ JPA에서는 OEIV (Open EntityManager In View) 라고 한다.


과거 OSIV : 요청 당 트랜잭션

클라이언트 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다.

 

문제점

컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점이다.

 

프리젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 래핑
  • DTO만 반환

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

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

OSIV를 서블릿 필터에서 적용할지 스프링 인터셉터에서 적용할지에 따라 원하는 클래스를 선택해서 사용하면 된다.

 

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

  • 하이버네이트 OSIV 서블릿 필터 : org.springframework.orm.hibernate4.support.OpenSessionInViewFilter
  • 하이버네이트 OSIV 스프링 인터셉터 : org.springframework.orm.hibernate4.support.OpenSessionInViewInterceptor
  • JPA OEIV 서블릿 필터 : org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
  • JPA OEIV 스프링 인터셉터 : org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor

스프링 OSIV 분석

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

즉, OSIV를 사용하기는 하지만 트랜잭션은 비즈니스 계층에서만 사용한다.

동작 순서

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

특징

  • 영속성 컨텍스트를 프리젠테이션 계층까지 유지한다.
  • 프리젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없다.
  • 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연로딩을 할 수 있다.

스프링 OSIV 주의사항

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

class MemberController {
    @Autowired
    private MemberService memberService;
    
    public String viewMember(Long id) {
    	Member member = memberService.getMember(id);
        member.setName("xxx");
        
        memberService.logic();	//비즈니스 로직
        return "view";
    }
}
class MemberService {
    @Transactional
    public void logic() {
    	//비즈니스 로직 실행
        //...
    }
}

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

문제를 해결하는 방법은 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경하면 된다.

'Spring JPA' 카테고리의 다른 글

컬렉션과 부가 기능 - 17  (0) 2021.08.10
스프링 데이터 JPA - 15  (0) 2021.07.31
객체 지향 쿼리 언어 - 14  (0) 2021.07.21
객체지향 쿼리 언어 - 13  (0) 2021.07.17
값 타입 - 12  (0) 2021.07.16

댓글