본문 바로가기

Backend/Spring

[Spring] 전통적인 Spring의 Transaction과 JPA의 OSIV 전략

오늘은 Spring의 전통적인 Transaction 처리의 과정과 JPA의 OSIV전략에 대해 이해해보는 포스팅을 진행해보겠습니다.

 

스프링을 사용해보신 분이라면 @Transactional이라는 어노테이션을 많이 보셨을 겁니다. Transaction이란 어떠한 일을 처리하는 단위를 말합니다.

 

가령 저희가 "6번 게시글을 수정한다"라는 로직을 수행하려면

 

1. "6번 게시글을 가져온다"

2. "6번 게시글을 수정한다"

3. "수정된 6번 게시글을 DB에 commit 한다"

 

이렇게 하나의 로직의 처리 묶음을 하나의 Transaction이라 합니다. 

 

그러면 Spring에서는 Transaction 처리를 어떤 순서로 진행할까요? 

전통적인 Transaction 처리부터 알아보겠습니다.

 

저희가 Spring Application을 실행하면 우선 Tomcat이 실행되고 Web.xml파일이 로딩되며 Tomcat에서 DispatcherServlet이 작동을 준비할 것입니다.

 

 

이 상태에서 Client로부터 요청이 오는 상황을 요약해 보겠습니다.

 

우선 Spring은 내부적으로 위 그림과 같은 구조를 갖습니다

 

Client로부터 특정 게시글을 읽어오라는 요청을 받았다고 생각해보겠습니다.

 

1. 게시글을 읽어오라는 요청을 받았습니다. 이 요청이 받아진 순간 Spring에서는 JDBC Connection을 시작합니다. 그리고 이 순간 Transaction도 시작이 됩니다. 마지막으로 영속성 콘텍스트도 이때 시작됩니다.

 

2. 해당 요청은 Web.xml을 통해 파싱 되어 적절한 Controller에 매핑될 것입니다. 그러면 해당 Controller는 url에 맞는 서비스 메서드를 호출해줍니다.

 

3. 이제 Service단으로 요청이 넘어왔습니다. 여기서 Repository에 해당 서비스를 수행에 필요한 데이터를 Repository로부터 읽어오려 시도합니다.

 

4. Repository에서는 Service의 요청에 따라 Database에 쿼리를 보내게 됩니다.

 

그러면 마지막으로 DB에서 해당 데이터를 읽어온 뒤, 영속성 콘텍스트는 해당 데이터를 객체로 가지고 있으면서 Repository에 반환해줄 것입니다.

 

영속성 콘텍스트란?

 

 

자 그러면 다시 역순으로 Controller까지 Board가 돌아오게 됩니다. 그러면 Controller는 해당 데이터를 뷰에 얹어서 Client에게 반환해줍니다.

 

그러면 영속성 콘텍스트와 JDBC connection은 언제 종료되게 될까요?

 

1. 바로 Controller가 View를 반환하는 시점입니다. 이 지점에서 우선 Transaction이 종료되면서, 영속성 콘텍스트의 Data가 변경이 감지되었으면 Commit을 하며 종료됩니다.

 

2. JDBC 커넥션도 종료됩니다.

 

3. 모든 처리가 끝났으면 영속성 콘텍스트도 종료합니다.

 

여기까지가 전통적인 Spring의 Transaction처리의 순서입니다.

 

그렇다면 JPA의 OSIV 전략은 어떻게 작동할까요?

 

우선 JPA가 위 스프링의 기본 전략에 제기한 문제는 다음과 같습니다.

 

어차피 모든 비즈니스 로직은 Service Layer에서 처리되는데, Transaction, JDBC connection, 영속성 콘텍스트를 Controller 종료 시점까지 이어갈 필요가 있을까? 

 

실제로 맞는 말입니다. Controller의 경우 url 매핑과 반환받은 데이터를 뷰와 합쳐 반환해주는 역할만을 수행하기 때문에 실제 DB 접근과 같은 역할은 Controller의 역할이 아닙니다. 

 

따라서 위 종료 시점을 Service가 종료되는 시점으로 당기면, 실제 DB에 걸리는 과부하를 줄이며 더 효율적으로 작동할 수 있겠다가 핵심입니다.

 

따라서 기존의 Controller반환시점이 아닌 Service Layer 종료 시점에 모든 일을 수행하게 됩니다.

 

 

여기서 끝이 아닙니다. DB 연결 세션과 트랜잭션의 시작도 굳이 요청이 도착한 시점일 필요가 없습니다.

 

따라서 DB 연결 세션과 트랜잭션의 시작도 Service Layer가 시작되는 시점으로 미루어버립니다.

 

자 이렇게 되면 저희가 원하는 모든 역할을 수행하며, DB 연결 세션과, Transaction의 지속시간을 상당히 줄일 수 있어 훨씬 효율적으로 로직을 수행하게 됩니다.

 

 

여기까지 이해하셨다면 거의 다 이해하셨습니다. 하지만 위 방식이 해결해야 할 하나의 문제가 있습니다.

 

package com.BlogWithSpringBoot.Board;

import com.BlogWithSpringBoot.BaseTimeEntity;
import com.BlogWithSpringBoot.Comment.Comment;
import com.BlogWithSpringBoot.User.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;

import javax.persistence.*;
import java.util.List;

@Getter @Builder
@NoArgsConstructor @AllArgsConstructor
@Entity
public class Board extends BaseTimeEntity {

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

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name="userId")
    private User user;

    @Column(nullable = false, length = 100)
    private String title;

    // Large Data
    @Lob
    private String content;

	...
}

 

자 위와 같은 Board Entity를 선언했다고 생각해보겠습니다. 

 

여기서 주의하실 점은 User입니다. ManyToOne Mapping으로 구현되어 있습니다.

 

문제는 저희가 FetchType을 Eager로 설정했기 때문에 실제 Board를 가져올 때 User를 함께 가져오게 됩니다.

 

만약에 FetchType Eager로 가져오게 되면, 반드시 Board가 포함해야 하는 모든 정보가 객체에 담겨있어야 하기 때문에 위와 같이 User를 같이 가져올 수밖에 없습니다.

 

하지만 만약에 저희가 FetchType을 Lazy로 설정했다면 어떻게 될까요?

 

이경우 Spring은 DB에서 user를 직접적으로 가져오지 않습니다. 왜냐하면 Lazy로 Fetch 한다는 것은 실제 이 데이터가 사용될지 안될지 모르는 상황이고 이러한 상황에서 모든 데이터를 다 가져오는 것은 비효율적이겠죠.

 

문제는 여기서 발생합니다.

 

만약에 Controller까지 데이터가 반환되었는데, 여기서 User정보를 원하면 어떻게 해야 할까요?

 

 

FetchType이 Lazy이기 때문에 user정보는 가져오지 않았습니다. 하지만 이 시점에서 JDBC커넥션과 영속성 콘텍스트가 이미 종료된 시점이기 때문에 user정보를 가져올 수 없는 것입니다.

 

따라서 JPA는 Lazy Fetch의 경우 유저와 같은 인터페이스를 공유하는 Proxy객체를 가져오게 됩니다.

 

 

프락시에 대해서는 이 포스팅에서 간략히 다루었습니다.

 

그리고 4. 영속성 콘텍스트 종료만 Controller의 종료 시점으로 미루게 됩니다.

 

 

이렇게 되면, Controller에서 User의 정보가 필요할 때 

 

잠시 JDBC커넥션을 다시 열어서 User객체를 가져오고, Controller에 반환해줍니다.

 

그리고 Controller가 종료될 때 영속성 콘텍스트도 종료됩니다.

 

이를 그림으로 나타내면

 

출처 : https://getinthere.tistory.com/27

위와 같이 영속성 콘텍스트만 User Interface(Controller)까지만 유지하고 나머지는 Service의 시작과 종료 시점에 끝나게 되는 것입니다.

 

이렇게 스프링의 전통적인 Transaction 전략과 JPA의 OSIV에 대해 다루어보았습니다.

 

*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.