JPA를 쓰면서 어디선가 많이 들어봤는데.. OSIV가 뭐지?
이번글은 OSIV에 대해 알아보겠습니다.
OSIV
OSIV(Open Session in View)는 영속성 컨텍스트를 뷰까지 유지하는 기능입니다.
즉, 영속성 컨텍스트가 닫히기 전에 컨트롤러나 뷰에서도 엔티티를 영속 상태로 유지할 수 있어, 지연 로딩이 가능해집니다.
OSIV는 과거에 지원했던 방식과 현재 Spring에서 지원하는 방식에 차이점이 존재합니다.
먼저, 과거에 사용되었던 OSIV 방식에 대해서 살펴보겠습니다.
과거에 사용된 OSIV
과거에 사용되었던 OSIV 방식은 요청 단위 트랜잭션을 유지하는 방식이였습니다.
즉, HTTP 요청이 시작될 때 트랜잭션을 열고, 응답이 완료되는 경우 트랜잭션을 닫는 구조였습니다.
흐름을 이미지로 나타내면 다음과 같습니다.
- 클라이언트에서 요청이 들어오는 경우 트랜잭션과 영속성 컨텍스트를 시작합니다.
- 요청이 처리되고 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료합니다.
영속성 컨텍스트가 요청이 시작되는 시점부터 끝나는 시점까지 살아있기 때문에 엔티티가 영속 상태를 유지합니다.
이로 인해서 뷰 계층에서 지연 로딩이 가능해 지므로, 뷰에 독립적인 서비스 계층을 유지할 수 있게 됩니다.
이러한 방식은 어떤 문제점이 있을까요?
먼저, 컨트롤러나 뷰 같은 프리젠테이션 계층에서 엔티티를 변경할 수 있다는 점입니다.
이 부분이 문제가 되는 이유는 Controller나 뷰 단에서 행여나 엔티티를 잘못 수정하거나 삭제할 경우 그대로 DB에 반영되기 때문입니다.
어떻게 DB에 반영이 될까요?
뷰를 렌더링한 후에 트랜잭션을 커밋하게 되고 이는 영속성 플러시가 발생합니다.
영속성 플러시가 발생한 후, 변경 감지 기능이 작동하고 스냅샷을 비교해 변경된 엔티티를 DB에 반영시킵니다.
Spring에서 제공하는 OSIV 이러한 문제점을 어떻게 다루는지 알아보겠습니다.
스프링 OSIV
스프링이 제공하는 OSIV는 이전 프리젠테이션 계층에서 트랜잭션이 시작되는 것이 아닌, 비즈니스 계층에서만 사용하도록 합니다.
흐름을 이미지로 나타내면 다음과 같습니다.
클라이언트에서 요청이 들어와도 트랜잭션을 시작하지 않고, 서비스 계층에서 트랜잭션을 시작하는 경우 이전에 생성해둔 영속성 컨텍스트에 트랜잭션을 시작합니다.
순서는 다음과 같습니다.
- 클라이언트 요청이 들어오면, 서블릿 필터, 스프링 인터셉터에서 영속성 컨텍스트를 생성합니다.
- 서비스 계층에서 @Transactional로 트랜잭션을 시작하는 경우 생성해둔 영속성 컨텍스트로 트랜잭션을 시작합니다.
- 서비스 계층의 비즈니스 로직이 종료되면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시합니다. (트랜잭션은 끝내지만, 영속성 컨택스트는 종료하지 않습니다.)
- 프레젠테이션 계층까지 영속성 컨텍스트가 유지되어 엔티티는 영속 상태를 유지합니다.
- 서블릿 필터나 스프링 인터셉터로 요청한 결과가 돌아오는 경우 영속성 컨텍스트를 종료합니다.(플러시는 호출하지 않고 종료됩니다.)
스프링 OSIV는 트랜잭션 범위를 비즈니스 계층으로 축소시킴으로 프레젠테이션 계층에서 엔티티를 수정할 수 있던 문제점을 해결했습니다. 단, 엔티티의 영속화는 되어있기 때문에 지연로딩은 가능합니다.
트랜잭션이랑 엔티티를 수정하는 것이랑 무슨 상관이 있나요?
영속성 컨텍스트를 이용한 엔티티의 변경은 기본적으로 트랜잭션 범위 안에서만 발생됩니다.
트랜잭션 범위 밖에서 플러시를하는 경우엔 예외가 발생하게 됩니다.
스프링에서 제공하는 OSIV의 특징을 정리하면 다음과 같습니다.
- 트랜잭션은 비즈니스 계층에서만 사용하기 때문에 프레젠테이션 계층에서 데이터를 수정할 수 없습니다.
- 영속성 컨텍스트의 범위는 프레젠테이션 계층까지 유지하기 때문에 프레젠테이션 계층에서 지연 로딩이 가능합니다.
지연 로딩이랑 영속성 컨텍스트의 범위랑 무슨 상관이 있나요?
기본적으로 엔티티가 영속 상태일 때만 지연 로딩을 사용할 수 있습니다.
영속성 컨텍스트가 살아 있다는 것은, 해당 컨텍스트가 엔티티를 계속 관리하고 있다는 의미이며, 이로 인해 엔티티는 영속 상태를 유지하고 지연 로딩을 사용할 수 있습니다.
스프링에서 제공하는 OSIV에는 문제점이 없을까요?
프레젠테이션 계층에서 엔티티를 수정하고나서 바로 트랜잭션이 시작되는 서비스 계층을 호출하면 수정되는 문제가 발생합니다.
문제가 발생하는 흐름은 다음과 같습니다.
- OSIV가 활성화된 상태에서 프레젠테이션 계층에서 엔티티를 조회합니다.
- 프레젠테이션 계층에서 엔티티의 값을 변경 합니다. (현 시점에는 트랜잭션이 존재하지 않습니다.)
- 서비스 계층을 호출하면서 트랜잭션이 시작됩니다.
- 서비스 계층의 비즈니스 로직이 종료되고 트랜잭션이 커밋됩니다.
- 트랜잭션이 커밋되는 순간 변경 감지가 일어나 변경 사항이 DB에 반영됩니다.
Spring에서 제공하는 OSIV의 또 다른 문제점은 데이터베이스 커넥션을 오랫동안 점유한다는 점입니다.
OSIV가 활성화된 상태에서는 영속성 컨텍스트가 요청이 끝날 때까지 유지되며, 이로 인해 DB 커넥션이 즉시 반납되지 않고 지속적으로 유지될 수 있습니다.
또한, 지연 로딩이 가능한 이유도 DB 커넥션이 유지되기 때문입니다.
즉, 영속성 컨텍스트가 살아 있다는 것은 트랜잭션이 종료된 후에도 DB 커넥션이 반납되지 않고 유지되는 상태를 의미하며,
이로 인해 프레젠테이션 계층에서도 Lazy Loading을 수행할 수 있습니다.
실시간 트래픽이 중요한 애플리케이션에서는 DB 커넥션이 장기간 점유되면 커넥션이 부족해질 위험이 있으며, 이는 결국 장애로 이어질 수 있습니다.
예를 들어, 프레젠테이션 계층에서 외부 API를 호출하는 경우, 외부 API의 응답 시간이 길어질수록 그 시간만큼 DB 커넥션이 반환되지 않고 계속 점유됩니다. 만약 호출한 외부 API에 장애가 발생하는 경우, 대기 시간 동안 불필요하게 커넥션 리소스를 낭비하게 되고, 다른 요청들이 DB에 접근하지 못하는 문제가 발생할 수 있습니다.
그렇다면, 어떻게 이 문제를 해결할 수 있을까요?
OSIV를 false로 설정하면 됩니다. 설정은 application.properties 혹은 application.yml에 아래와 같이 명시해주면 됩니다.
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
open-in-view: false # false로 명시해 줍니다.
OSIV를 false로 설정하는 경우 아래와 같이 영속성 컨텍스트의 범위가 줄어들게 됩니다.
OSIV와 LazyLoading에 대한 부분을 테스트 해보겠습니다.
OSIV LazyLoading 테스트
DB에 10,000명의 유저 정보가 저장되어 있으며, 하나의 API 호출 시 이 10,000명에 대한 각 주문 정보의 총 개수를 조회하는 상황을 테스트합니다.
Controller
@RestController
@RequestMapping("/osiv-test")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public List<User> getUsersWithLazyLoading() {
return userService.getUsersWithLazyLoading();
}
}
Service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public List<User> getUsersWithLazyLoading() {
List<User> users = userRepository.findAll();
for (User user : users) {
user.getOrders().size();
}
return users;
}
}
Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
DB 커넥션 풀 설정 상태는 다음과 같습니다.
hikari:
maximum-pool-size: 10
minimum-idle: 2
idle-timeout: 10000
max-lifetime: 30000
connection-timeout: 5000
8개의 스레드를 사용해서 500개의 동시 연결을 하고 60초 동안 테스트를 실행한 결과는 다음과 같습니다.
(API 호출 테스트는 wrk를 사용해서 테스트 합니다.)
OSIV가 활성화된 상태
OSIV가 활성화 되지 않은 상태
requests in 1m | TPS(Transaction Per Second) | |
OSIV 활성화 | 897,772개 | 14950.2개 |
OSIV 비활성화 | 941,646개 | 15685.07개 |
OSIV를 비활성화 시킨 경우가 TPS가 더 높게 나왔습니다.
TPS가 더 높기 때문에 OSIV를 활성화 시킨 경우보다 비활성화 시킨 경우가 동일한 시간 동안 더 많은 요청을 처리할 수 있습니다.
정리
- OSIV는 영속성 컨텍스트를 뷰까지 유지하는 기능입니다.
- 실시간 트래픽이 중요한 애플리케이션인 경우 OSIV를 끄는 것이 성능적으로 더 이점이 있습니다.
'JPA' 카테고리의 다른 글
프록시 객체 (0) | 2025.02.20 |
---|---|
영속성 전이 & 고아 객체 (0) | 2025.02.19 |
N+1 문제 해결 전략 @BatchSize편 (0) | 2025.02.10 |
JPA 비관적 락 (0) | 2025.02.07 |
JPA 낙관적 락 (0) | 2025.02.06 |