[Spring] OSIV 정리

이창윤·2022년 8월 1일
0
post-thumbnail

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

JPA를 사용하려면 스프링 컨테이너가 제공하는 전략을 따라야 한다.
스프링 컨테이너는 JPA의 영속성 컨텍스트를 지원할 때 기본 전략으로 트랜잭션 범위의 영속성 컨텍스트를 사용한다.

영속성 컨텍스트:

  • 엔티티를 영구 저장하는 환경
  • 어플리케이션과 데이터베이스 사이에서 객체를 보관하는 논리적인 개념
  • EntityManager를 통해 영속성 컨텍스트에 접근한다.

트랜잭션의 범위와 영속성 컨텍스트의 생존 범위를 맞추는 전략으로, 트랜잭션이 시작하는 순간 영속성 컨텍스트도 생성되고, 끝나는 순간 영속성 컨텍스트가 종료된다.

비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작한다.

@Transactional
@Service
public class ArticleService {

	public void getArticle(Long articleId) {
        return articleRepository.findById(articleId);
    }
}

트랜잭션의 메소드가 정상 종료되면 트랜잭션을 커밋하는데, JPA가 영속성 컨텍스트를 flush해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋한다.

트랜잭션이 같으면 같은 영속성 컨텍스트를 사용하고 다르면 다른 영속성 컨텍스트를 사용한다.

트랜잭션이 종료되면 영속성 컨텍스트가 끝나고 관리되던 엔티티가 준영속 상태가 된다.

  • 서비스 계층에서 조회한 엔티티는 서비스, 레포지토리 계층에서는 영속성 컨텍스트에 관리되지만 컨트롤러, 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다.

준영속 상태와 지연 로딩

@Controller
public class ArticleController {
	...
	public String view(Long articleId) {
    	Article article = articleService.getArticle(articleId);
        article.getContent(); //지연 로딩
    }
}

getArticle 메소드를 호출해서 반환받은 article은 준영속 상태이기 때문에 지연 로딩을 하면 예외가 발생한다.

해결 방법

  • 뷰에서 필요한 엔티티를 서비스 계층에서 미리 로딩해두는 방법 (JPQL 페치 조인 사용, FACADE 계층 추가, 강제로 초기화)
  • OSIV를 사용해서 엔티티를 프레젠테이션 계층까지 영속 상태로 유지하는 방법

OSIV (Open Session In View)란?

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

Hibernate에서 사용하는 용어.. JPA에서는 OEIV(Open EntityManager In View)라고 하지만 OSIV로 보통 부른다.

지연 로딩을 사용할 수 있다는 것이 큰 장점이다.

과거 OSIV: 요청 당 트랜잭션

요청이 들어오자 마자 트랜잭션을 시작하고 요청이 끝날 때 종료하는 방법

  1. 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들고 트랜잭션을 시작한다.
  2. 컨트롤러, 서비스, 레포지토리 계층에서 모두 영속성 컨텍스트가 유지된다.
  3. 조회된 엔티티가 뷰까지 영속 상태를 유지하므로 지연 로딩이 가능하다.
  4. 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료한다.

문제점

  • 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있다.
  • 엔티티가 영속 상태이므로 dirty checking이 동작하고 변경된 엔티티의 값을 데이터베이스에 반영해버린다.

해결법

  • 엔티티를 읽기 전용 인터페이스로 제공 (getter만 제공)
  • 엔티티 Wrapping: getter 메소드만 가지고 있는 엔티티를 감싼 객체를 만든다.
  • DTO만 반환: 엔티티 대신 DTO를 생성해서 반환한다.

코드량이 많이 증가하기 때문에 요청 당 트랜잭션 방식의 OSIV는 거의 사용하지 않는다.

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

비즈니스 계층에서 트랙잭션을 사용하는 OSIV - OSIV를 사용하지만 트랜잭션은 비즈니스 계층에서만 사용하는 방법

1. 클라이언트 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성하되 트랜잭션은 시작하지 않는다.
2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 flush한다. (트랜잭션은 종료되지만 영속성 컨텍스트는 유지)
4. 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. flush를 호출하지 않고 바로 종료한다.

특징

  • 트랜잭션이 없으므로 프레젠테이션 계층에서 엔티티를 수정할 수 있는 기존 OSIV의 단점을 보완했다.
  • 트랜잭션 없이 읽기를 사용해서 프레젠테이션 계층에서 지연 로딩을 사용할 수 있다.

주의사항

  • 프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.
@Controller
class ArticleController {
	...
    public String view(Long articleId) {
    	Article article = articleService.getArticle(articleId);
        article.setTitle("XXX");
        
        articleService.logic_that_starts_transaction();
        // 트랜잭션을 시작하는 로직
    }
}
  1. articleService의 getArticle로 반환받은 Article을 영속성 컨텍스트에 저장한다.
  2. article의 제목을 XXX로 바꾼다.
  3. 트랜잭션이 있는 메소드를 호출한다.
  4. 트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션을 시작하고 메소드를 실행한다.
  5. 메소드가 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 flush하는데, dirty checking이 동작하면서 회원 엔티티의 수정 사항을 데이터베이스에 반영한다.

스프링 OSIV의 단점

  • 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다.
  • 오랜시간 동안 테이터베이스 커넥션 리소스를 사용한다. -> 실시간 트래픽이 중요한 경우 커넥션이 모자랄 수 있어 장애로 이어질 가능성이 있다.



추천 전략

OSIV를 끄고 CQRS(Command Query Responsibility Segregation) 패턴을 사용하여 커맨트와 쿼리를 분리하는 것이 좋다.

스프링부트는 OSIV가 기본값으로 켜져있다.
jpa옵션에 open-in-view를 false로 설정해주면 된다.

  • Command: ArticleService에는 핵심 비즈니스 로직 사용
  • Query: @Transactional(readOnly=true)로 읽기 전용 트랜잭션을 사용해서 화면이나 API에 맞춘 서비스로 구현한다.

참고: CQRS 패턴

참고 및 출처

자바 ORM 표준 JPA 프로그래밍
[JPA] OSIV란?
OSIV와 성능 최적화

0개의 댓글