Java/Spring

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

SeungbeomKim 2023. 11. 14. 17:41

 

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

  • 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략 사용
  • 트랜잭션을 시작할 때 영속성 컨텍스트(Persistance Context)를 생성하고 트랜잭션이 끝나면 영속성 컨텍스트를 종료합니다.
  • 핵심 비즈니스 로직을 담당하는 Service 클래스에서 @Transactional 어노테이션을 통해 트랜잭션을 시작하게 되고, 서비스보다 상위 계층에 있는 Layer는 준영속 상태가 됩니다.

 

준영속 상태가 뭔지 설명드리기 전에 엔티티 생명주기에 대해 간략하게 설명드리겠습니다.

 

엔티티 생명주기

  • 비영속(new/transient): new 키워드로 객체를 생성만 한 상태 (영속성 컨텍스트에서 관리 X, 1차 캐시, 변경 감지 등의 기능 적용 X)
  • 영속(managed): 영속성 컨텍스트에 의해서 관리되어지는 상태(em.persist(object), em.find()..)
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태로 현재는 영속 상태가 아닌 상태(영속성 컨텍스트가 제공하는 기능 사용 X)
  • 준영속 상태의 Entity는 식별자가 존재한다는 것을 보장하지만, 비영속 상태의 Entity는 보장받지 못합니다.
  • 삭제(removed): 1차캐시와 데이터베이스에서 모두 삭제된 상

// repositoryA, repositoryB는 같은 영속성 컨텍스트에 접근
@Transactional
public void executeBusinessLogic() {
    repositoryA.findAll();
    repositoryB.findAll();
}

// repositoryC는 repositoryA과 repositoryB와 다른 영속성 컨텍스트에 접근
@Transactional
public void executeOtherBusinessLogic() {
    repositoryC.findAll();
}

 

준영속 상태와 지연로딩(Lazy Loading)

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long Id;

    @ManyToOne(fetch = FetchType.LAZY)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Member member;
}

 

  • Book 객체를 만드는 Controller에서 지연로딩 객체를 초기화하면 Exception 발생
  • 해결 방안
    • 글로벌 페치 전략: FetchType.LAZY -> FetchType.EAGER (N+1 Problem, 불필요한 엔티티 조회)
    • JPQL Fetch Join
    • 강제로 초기화 

 

강제로 초기화 예시

@RequiredArgsConstructor
public class OrderService {

    private final BookRepository bookRepository;
	
    @Transactional
    public Book findBook(final Long bookId) {
        Book book = bookRepository.findOrder(bookId);
        book.getMember().getName(); // 프록시 객체를 강제로 초기화
        return book;
}
  • 다음과 같이 비즈니스 로직에 프록시 객체를 초기화하는 역할이 같이 존재하면, 객체지향적이지 못한 코드가 됩니다. 그래서 Facade 계층을 추가해줘야 합니다.

  • Facade 계층
    • Presentation Layer와 Service Layer에서 Proxy 객체 초기화 담당
    • 기존의 트랜잭션 시작을 서비스에서 진행했지만, Facade 계층에서 시작
    • 의존관계 문제를 해결했지만, 계층이 추가되어 구조의 복잡도는 더욱 증가

 

  • OSIV(Open Session In View)
    • 영속성 컨텍스트를 View까지 열어두는 것
    • 옵션을 false(고객 서비스 기반의 트래픽이 많은 실시간 API)에서 true( ADMIN처럼 커넥션을 많이 사용하지 않는 곳)로 바꿔주면 View에서도 지연로딩 사용이 가능해집니다.(default:true)
    • Hibernate에서 부르는 용어이고, JPA에서는 OEIV라고 부릅니다.

 

  • OSIV 장점
    • OSIV를 사용하게 되면, 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없을 뿐만 아니라, 단순 엔티티 조회를 Controller에서 호출해도 괜찮습니다.
  • OSIV: true

  • 과거 OSIV: 요청 당 트랜잭션
    • OSIV의 가장 단순한 구현은 클라이언트의 요청이 들어오자마자 filter나 interceptor에서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션을 끝냅니다.
    • 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 데이터도 영속 상태를 유지하기에 Facade 계층도 필요 없어집니다.
    • 하지만, Controller, View에서 Entity 관련 정보를 수정할 수 있기에 DB의 변경이 아무 계층에서나 발생할 수 있는 문제가 존재합니다.

  • 스프링 OSIV: 비즈니스 계층 트랜잭션
    • Spring Framework가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV입니다.

  • 클라이언트의 요청이 들어오면 Servlet Filter, Interceptor에서 영속성 컨텍스트를 생성합니다. 여기에서 트랜잭션이 시작되지는 않습니다.
  • 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때, 미리 생성해 둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작합니다.
  • 서비스 계층이 끝나면 트랜잭션 commit 및 영속성 컨텍스트의 flush가 실행되고 이를 통해 트랜잭션이 끝나더라도 영속성 컨텍스트는 유지될 수 있습니다.
  • Filter, Interceptor로 요청이 돌아오면 Flush를 호출하지 않고 바로 종료합니다.

 

트랜잭션 없이 읽기

  • 프리젠테이션 계층에서 엔티티를 수정한 직후에, 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생합니다.
  • 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 롤백이 발생할 때, 문제가 생길 수 있습니다.
  • 복잡한 화면은 Dto로 반환하는 것이 좋습니다.
  • OSIV는 JVM을 벗어난 원격 상황에서 사용하지 않습니다.
  • Json으로 생성한 API를 외부 API, 내부 API 두 가지로 나눌 수 있습니다.
    • 외부 API는 변경이 자주 일어나므로 Dto를 사용
    • 내부 API는 변경이 적으므로 OSIV를 사용

<참고 자료>

https://www.yes24.com/Product/Goods/19040233