[JPA] OSIV(open-in-view) 켤까 끌까: 영속성 컨텍스트 범위와 커넥션 고갈
![[JPA] OSIV(open-in-view) 켤까 끌까: 영속성 컨텍스트 범위와 커넥션 고갈](https://blog.kakaocdn.net/dna/Upzpy/dJMcadCaFlk/AAAAAAAAAAAAAAAAAAAAALrHXXGTRRMfRpI6oUWz_RzQtYszTbBOEnMtA7tEDTfP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1788188399&allow_ip=&allow_referer=&signature=92YjfuvSN7%2FS1AiELToThpyNnuY%3D)
[JPA] OSIV(open-in-view) 켤까 끌까: 영속성 컨텍스트 범위와 커넥션 고갈
스프링 부트로 JPA를 띄우면 로그에 이런 경고가 떠요 — "spring.jpa.open-in-view is enabled by default". OSIV(Open Session In View)인데, 켜두면 편하고 끄면 빡빡해요. 그런데 트래픽이 있는 서비스에서 이 기본값을 그대로 두면 커넥션 고갈로 이어질 수 있어요.
OSIV가 정확히 뭘 하는지, 켰을 때와 껐을 때 영속성 컨텍스트와 DB 커넥션의 수명이 어떻게 달라지는지, 커넥션 고갈은 어떤 시나리오로 오는지, 끌 때 필요한 코드 규율은 무엇인지 짚어요. 영속성 컨텍스트와 지연 로딩 글의 종합편이에요.
01. OSIV가 뭔가요
원래 영속성 컨텍스트는 트랜잭션과 함께 시작하고 끝나요. 그러면 트랜잭션 밖(컨트롤러·뷰)에서는 지연 로딩이 안 되죠. OSIV는 이 수명을 늘려서 영속성 컨텍스트를 요청 시작부터 응답 끝까지 살려두는 옵션이에요.

그래서 OSIV가 켜져 있으면(스프링 부트 기본값) 컨트롤러에서 order.getUser().getName() 같은 지연 로딩이 그냥 돼요. LazyInitializationException을 만날 일이 없죠. 여기까지만 보면 좋은 기능 같아요.
02. 공짜가 아니에요, 커넥션의 수명
문제는 영속성 컨텍스트가 살아 있는 동안 DB 커넥션도 같이 붙잡힐 수 있다는 거예요. 트랜잭션이 시작되면 커넥션을 얻는데, OSIV에서는 그 커넥션이 응답이 나갈 때까지 반납되지 않아요. 뷰 렌더링과 JSON 직렬화 중에도 지연 로딩이 가능해야 하니까요.
요청 하나가 커넥션을 들고 있는 시간 = DB 작업 시간이 아니라 요청 전체 시간이 되는 거예요. DB는 0.01초 일했는데 커넥션은 2초를 점유하는 식이죠.
03. 커넥션 고갈 시나리오

전형적인 사고가 이래요. 컨트롤러에서 외부 API를 호출하는데 그게 2초 걸려요. OSIV가 켜져 있으니 그 2초 동안 DB 커넥션을 들고 있어요. 동시 요청이 늘면 커넥션 풀(보통 10~20개)이 전부 "외부 API 대기 중"인 요청들에 묶여요. 새 요청은 커넥션을 못 받아 타임아웃 — DB는 한가한데 서비스가 죽는 역설이 벌어져요.
이 사고의 고약한 점은 평소엔 안 보인다는 거예요. 트래픽이 적을 땐 커넥션을 오래 들고 있어도 풀에 여유가 있으니까요. 트래픽이 몰리거나 외부 API가 느려지는 바로 그 순간에, 즉 가장 버텨야 할 때 터져요. 커넥션 풀 지표(active/idle/pending)를 모니터링하면 이 패턴이 미리 보여요 — active가 DB 부하와 무관하게 높게 유지되면 OSIV를 의심해요.
04. OSIV를 끄면, 규율이 필요해요
spring:
jpa:
open-in-view: false

끄면 영속성 컨텍스트와 커넥션이 트랜잭션과 함께 끝나요. 커넥션은 즉시 반납되고 풀 효율이 극대화돼요. 대신 컨트롤러부터는 엔티티가 준영속이라, 지연 로딩을 시도하면 LazyInitializationException이에요.
그래서 OSIV OFF는 코드 규율과 한 세트예요.
- 필요한 데이터는 트랜잭션 안에서 다 꺼낸다 — fetch join·@EntityGraph로 로딩을 끝내요(N+1 해법과 같은 도구).
- 서비스가 DTO를 반환한다 — 엔티티를 컨트롤러로 흘리지 않으면, 밖에서 프록시를 건드릴 일 자체가 없어요. 직렬화 무한 루프도 같이 사라지고요.
- 화면 조립용 쿼리 서비스를 분리하기도 해요 — 핵심 비즈니스 서비스와 별개로, 화면 DTO를 만드는 조회 전용 서비스(예:
OrderQueryService)를 두면 트랜잭션 경계가 깔끔해져요.
05. 그래서 켜요 꺼요
기준은 트래픽과 커넥션의 귀함이에요.
- 고객 트래픽을 받는 API 서버 → OFF가 정석. 커넥션은 한정 자원이고 위 고갈 시나리오의 비용이 편의보다 훨씬 커요.
- 어드민·내부 도구·소규모 서비스 → ON의 편의를 취해도 돼요. 동시 요청이 적어 커넥션 압박이 없고 화면이 많아 지연 로딩 편의가 크니까요.
중요한 건 "기본값이니까 그냥 둔다"가 아니라, 이 트레이드오프를 알고 선택하는 거예요. 스프링 부트가 기동 때마다 경고를 찍는 이유가 그거예요.
06. 자주 만나는 문제
OSIV를 껐더니 여기저기서 LazyInitializationException이 터져요
컨트롤러·직렬화가 엔티티의 지연 로딩에 의존하고 있던 거예요. 예외가 나는 지점마다 "트랜잭션 안에서 DTO로 꺼내기"로 바꿔요. 터지는 곳이 곧 수술 목록이에요.
트래픽이 몰리면 커넥션 풀 타임아웃이 나요
OSIV ON + 요청 중 느린 구간(외부 API·무거운 직렬화)의 조합을 의심해요. 커넥션 active 시간이 DB 작업 시간보다 훨씬 길면 확정이에요.
끄고 싶은데 수정 범위가 너무 커요
전부 한 번에 바꿀 필요는 없어요. 신규 API부터 DTO 반환 규율을 적용하고 트래픽 큰 엔드포인트부터 점진적으로 옮겨요. 다만 신규 프로젝트라면 처음부터 OFF로 시작하는 게 압도적으로 싸요.
켤까 끌까, 한눈에
OSIV는 영속성 컨텍스트(와 커넥션)의 수명을 응답 끝까지 늘려 컨트롤러 지연 로딩을 가능하게 하는 편의 기능이에요. 편한 대신 커넥션을 오래 붙잡아, 트래픽 상황에서 고갈 위험을 안고요. 선택은 이렇게 갈라요.
- 고객 트래픽을 받는 API 서버 → OFF가 정석. 더해서 "트랜잭션 안에서 DTO로 꺼내기" 규율을 한 세트로 가요.
- 어드민·내부 도구·소규모 서비스 → ON의 편의를 취해도 괜찮아요.
- 공통 → "기본값이니 그냥 둔다"가 아니라, 트레이드오프를 알고 고르기. 스프링 부트가 기동 때마다 경고를 찍는 이유예요.
영속성 컨텍스트와 지연 로딩의 종합편이었어요. 이어서 쓰기 성능 — 배치 insert와 대량 처리로 들어가고 커넥션 풀 자체의 사이징은 MySQL 클러스터의 HikariCP 편에서 따로 깊게 다뤄요.
출처: Spring Boot — Open EntityManager in View · Hibernate User Guide — Persistence Context
'백엔드 > 데이터 & DB' 카테고리의 다른 글
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."