Spring transition, osiv(t/f)

이지예·2026년 5월 6일

개발

목록 보기
9/58

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 응답

문제 상황 예시

// Order 엔티티 - member를 LAZY 로딩으로 설정
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;
}

// Service - 트랜잭션 종료 시 영속성 컨텍스트 닫힘
@Service
public class OrderService {
    @Transactional
    public Order findOrder(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }
}

// Controller - 트랜잭션 종료 후 영역
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public String getOrder(@PathVariable Long id) {
        Order order = orderService.findOrder(id);
        // 여기서 member에 접근 -> 영속성 컨텍스트 닫혀 있음 -> 예외 발생
        return order.getMember().getName(); // LazyInitializationException!
    }
}

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 기준)
# application.yml
spring:
  jpa:
    open-in-view: true   # 기본값
    # open-in-view: false

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 = trueOSIV = 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();

        // 트랜잭션 안에서 Lazy 로딩 처리
        String memberName = order.getMember().getName();   // Lazy 로딩 -> 정상 동작
        int itemCount = order.getItems().size();           // Lazy 로딩 -> 정상 동작

        // DTO로 변환 후 반환 (영속성 컨텍스트와 무관)
        return new OrderDto(order.getId(), memberName, itemCount);
    }
}
// Controller는 DTO만 받음 -> 영속성 컨텍스트 불필요
@RestController
public class OrderController {

    @GetMapping("/orders/{id}")
    public OrderDto getOrder(@PathVariable Long id) {
        return orderService.findOrder(id); // 이미 DTO로 변환된 결과
    }
}

9. 권장 패턴 -> Command/Query Service 분리

  • OSIV = false 환경에서 Service 계층이 비대해지는 문제를 해결하기 위한 패턴
  • 비즈니스 로직을 처리하는 Command Service와 조회 전용 Query Service를 분리
// Command Service : 비즈니스 로직 처리 (쓰기 위주)
@Service
@Transactional
public class OrderService {

    public Long createOrder(OrderCreateDto dto) {
        // 주문 생성, 재고 감소 등 비즈니스 로직
    }

    public void cancelOrder(Long orderId) {
        // 주문 취소 로직
    }
}

// Query Service : 조회 전용 (읽기 위주, DTO 반환)
@Service
@Transactional(readOnly = true)
public class OrderQueryService {

    public OrderDetailDto findOrderDetail(Long orderId) {
        // 복잡한 Fetch Join, DTO 변환
    }

    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 = falseCommand/Query Service 분리 + DTO 변환
profile
github: @dlwldP

0개의 댓글