Spring Transaction & OSIV (Open Session In View)
1. 개념 - 영속성 컨텍스트?
- 영속성 컨텍스트 = 엔티티를 저장하고 관리하는 “메모리 공간”
- JPA(“객체 ↔ DB 테이블 매핑”)는 Entity를 DB에서 조회할 때 영속성 컨텍스트(Persistence Context)라는 공간에 올려서 관리함
- EntityManager가 영속성 컨텍스트를 생성하고 관리함
- 영속성 컨텍스트가 제공하는 핵심 기능
- 1차 캐시 : 같은 트랜잭션(여러 db 작업을 하나로 묶는 것) 내에서 동일한 Entity를 다시 조회할 때 DB를 거치지 않고 캐시에서 반환
- 변경 감지(Dirty Checking) : 트랜잭션 커밋 시점에 Entity의 변경 사항을 감지하여 자동으로 UPDATE 쿼리 실행
- 지연 로딩(Lazy Loading) : 연관된 Entity를 실제로 사용하는 시점에 DB에서 조회
- 영속성 컨텍스트가 열려 있는 동안에만 Lazy Loading이 가능
- 영속성 컨텍스트가 닫힌 후 Lazy Loading을 시도하면 LazyInitializationException 발생
2. 트랜잭션과 영속성 컨텍스트의 기본 관계
- Spring의 기본 동작 : 트랜잭션 범위 = 영속성 컨텍스트 범위
- 트랜잭션이 시작될 때 영속성 컨텍스트가 생성되고, 트랜잭션이 종료될 때 영속성 컨텍스트가 함께 닫힘
기본 흐름 (OSIV 미적용)
HTTP 요청
-> Controller 진입
-> Service 진입 (@Transactional 시작) -> 영속성 컨텍스트 OPEN
-> Repository에서 DB 조회 (Entity 영속성 컨텍스트에 올라감)
-> Service 종료 (@Transactional 종료) -> 영속성 컨텍스트 CLOSE
-> Controller 복귀 <- 영속성 컨텍스트 이미 닫힘
-> View 렌더링 <- Lazy Loading 시도 시 LazyInitializationException 발생
HTTP 응답
문제 상황 예시
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
}
@Service
public class OrderService {
@Transactional
public Order findOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
}
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public String getOrder(@PathVariable Long id) {
Order order = orderService.findOrder(id);
return order.getMember().getName();
}
}
3. OSIV (Open Session In View) 개념
- Open Session In View : 영속성 컨텍스트를 HTTP 요청이 시작될 때부터 응답이 완료될 때까지 유지하는 전략(HTTP 요청이 끝날 때까지 영속성 컨텍스트(Session)를 열어두는 것)
- "Session"은 Hibernate(JPA를 실제로 동작하게 만드는 구현체)의 Session을 의미하며, JPA에서는 EntityManager에 해당
- Spring Boot에서 spring.jpa.open-in-view 설정으로 제어
- 기본값은 true (Spring Boot 기준)
spring:
jpa:
open-in-view: true
Spring Boot 애플리케이션 시작 시 OSIV가 true이면 다음 경고 로그가 출력됨
WARN - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering.
4. OSIV = true 동작 방식
흐름
HTTP 요청
-> Filter / Interceptor -> 영속성 컨텍스트 OPEN (요청 시작 시점)
-> Controller 진입
-> Service 진입 (@Transactional 시작)
-> Repository에서 DB 조회
-> Service 종료 (@Transactional 종료) <- 트랜잭션만 종료, 영속성 컨텍스트는 유지
-> Controller 복귀 <- Lazy Loading 가능 (영속성 컨텍스트 살아 있음)
-> View 렌더링 <- Lazy Loading 가능
-> HTTP 응답 완료 -> 영속성 컨텍스트 CLOSE
핵심 동작 원리
- 트랜잭션이 종료된 이후에도 영속성 컨텍스트는 열린 상태를 유지
- 트랜잭션 종료 후 영속성 컨텍스트 상태
- 1차 캐시 : 유지됨. 이미 로딩된 Entity는 재사용 가능
- Lazy Loading(“필요할 때까지 DB 조회를 미루는 것” -> 성능 때문에 사용): 가능. 단, 새로운 DB 커넥션을 사용하여 조회
- Dirty Checking : 비활성화. 트랜잭션이 없으므로 Entity를 변경해도 DB에 반영되지 않음
- flush : 발생하지 않음. 트랜잭션이 없는 상태에서 Entity를 수정해도 DB 반영 불가
osiv 이걸 어떻게 구현하냐
- Spring은 OpenEntityManagerInViewInterceptor 또는 OpenEntityManagerInViewFilter를 통해 구현
- 요청 시작 시 EntityManager를 생성하고, 응답 완료 후 닫음
- 트랜잭션은 @Transactional이 선언된 Service 메서드 범위에서만 동작
5. OSIV = false 동작 방식
흐름
HTTP 요청
-> Controller 진입
-> Service 진입 (@Transactional 시작) -> 영속성 컨텍스트 OPEN
-> Repository에서 DB 조회
-> Service 종료 (@Transactional 종료) -> 영속성 컨텍스트 CLOSE
-> Controller 복귀 <- Lazy Loading 불가 (영속성 컨텍스트 닫힘)
-> View 렌더링 <- Lazy Loading 불가
HTTP 응답
특징
- 영속성 컨텍스트가 트랜잭션 범위 안에서만 존재
- 트랜잭션 종료와 동시에 DB 커넥션 반환
- Controller나 View에서 Lazy Loading 시도 시 LazyInitializationException 발생
- 개발자가 Service 계층 안에서 필요한 데이터를 모두 로딩하거나 DTO로 변환해서 반환해야 함
6. OSIV true vs false 비교
| 항목 | OSIV = true | OSIV = false |
|---|
| 영속성 컨텍스트 범위 | HTTP 요청 전체 | 트랜잭션 범위 |
| DB 커넥션 점유 시간 | 요청 시작부터 응답 완료까지 | 트랜잭션 시작부터 종료까지 |
| Controller에서 Lazy Loading | 가능 | 불가 |
| View에서 Lazy Loading | 가능 | 불가 |
| 코드 편의성 | 높음 | 낮음 (명시적 fetch 필요) |
| 트래픽 많은 서비스에서의 위험 | DB 커넥션 고갈 위험 있음 | 상대적으로 안전 |
| 실무 권장 여부 | 소규모 또는 트래픽 적은 서비스 | 대규모 서비스 |
7. OSIV = true의 성능 문제
DB 커넥션 고갈 문제
- OSIV = true이면 DB 커넥션이 HTTP 요청 전체 동안 점유됨
- 트랜잭션이 종료된 이후 View 렌더링 시간, 외부 API 호출 시간 등에도 커넥션을 계속 들고 있음
- 동시 요청이 많아지면 커넥션 풀(Connection Pool)이 고갈될 수 있음
문제 상황 예시
[DB 커넥션 풀 : 최대 10개]
요청 1 -> 커넥션 점유 (OSIV true -> 응답 완료까지 반납 안 함)
요청 2 -> 커넥션 점유
...
요청 10 -> 커넥션 점유
요청 11 -> 커넥션 대기 -> 타임아웃 발생
특히 위험한 시나리오
- View 렌더링에 시간이 걸리는 경우 (Server Side Rendering)
- 외부 API 호출이 느린 경우
- 대용량 데이터를 처리하는 경우
8. OSIV = false 시 해결 방법
방법 1 : Fetch Join 사용
- JPQL에서 JOIN FETCH를 사용하여 연관 Entity를 한 번에 조회
- 트랜잭션 안에서 필요한 데이터를 모두 가져오므로 Lazy Loading 문제 없음
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.member JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findWithMemberAndItems(@Param("id") Long id);
}
방법 2 : @EntityGraph 사용
- 어노테이션으로 연관 Entity를 함께 로딩할 속성을 지정
- JPQL 없이 간결하게 Fetch Join 효과를 얻을 수 있음
@EntityGraph(attributePaths = {"member", "items"})
Optional<Order> findById(Long id);
방법 3 : Service 계층에서 DTO 변환 (가장 권장)
- 트랜잭션이 살아있는 Service 계층 안에서 Lazy Loading을 모두 처리하고 DTO로 변환하여 반환
- Controller에는 순수한 데이터(DTO)만 전달되므로 영속성 컨텍스트에 의존하지 않음
@Service
@Transactional(readOnly = true)
public class OrderService {
public OrderDto findOrder(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
String memberName = order.getMember().getName();
int itemCount = order.getItems().size();
return new OrderDto(order.getId(), memberName, itemCount);
}
}
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
return orderService.findOrder(id);
}
}
9. 권장 패턴 -> Command/Query Service 분리
- OSIV = false 환경에서 Service 계층이 비대해지는 문제를 해결하기 위한 패턴
- 비즈니스 로직을 처리하는 Command Service와 조회 전용 Query Service를 분리
@Service
@Transactional
public class OrderService {
public Long createOrder(OrderCreateDto dto) {
}
public void cancelOrder(Long orderId) {
}
}
@Service
@Transactional(readOnly = true)
public class OrderQueryService {
public OrderDetailDto findOrderDetail(Long orderId) {
}
public List<OrderSummaryDto> findOrderList(Long memberId) {
}
}
- Command Service : 상태 변경, 트랜잭션 필수, @Transactional 사용
- Query Service : 조회 전용, @Transactional(readOnly = true) 사용, DTO 반환
- 장점 : 역할 분리 명확, readOnly 트랜잭션으로 성능 최적화, 복잡한 조회 로직 격리
10. 정리
OSIV = true
- 영속성 컨텍스트를 HTTP 요청 전체에서 유지
- Controller, View에서 Lazy Loading 자유롭게 사용 가능
- 코드 작성이 단순하고 편리
- DB 커넥션을 요청 내내 점유하므로 트래픽이 많은 서비스에서는 커넥션 고갈 위험
- 소규모 서비스, 트래픽이 적은 환경, 빠른 프로토타이핑에 적합
OSIV = false
- 영속성 컨텍스트가 트랜잭션 범위 안에서만 존재
- DB 커넥션을 트랜잭션 종료 즉시 반납하므로 커넥션 효율 높음
- Controller, View에서 Lazy Loading 불가 -> 개발자가 Service 계층에서 처리해야 함
- 코드 복잡도 증가, 명시적 fetch 전략 필요
- 대규모 서비스, 실시간 트래픽이 많은 API 서버에 적합
선택 기준
| 서비스 규모 | 권장 설정 | 권장 패턴 |
|---|
| 소규모 / 트래픽 적음 | OSIV = true | 기본 Service 구조 |
| 대규모 / 트래픽 많음 | OSIV = false | Command/Query Service 분리 + DTO 변환 |