| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- 일본어공부
- 코딩
- 인프런
- java
- 가벼운학습지후기
- 마이라이트
- 코테
- 생활정보
- 일본어학습지
- jpa
- js
- 일본어독학
- 백준
- springboot
- 삼성
- React Native
- 자바
- 에러해결
- 자료구조
- 코딩테스트
- 알고리즘
- 자바스크립트
- SWEA
- 프로그래머스
- javascript
- 가벼운학습지
- New Architecture
- 웹보안
- 성인학습지
- 삼성소프트웨어아카데미
- Today
- Total
개발에 AtoZ까지
[에러] LazyInitializationException: could not initialize proxy - no Session (Spring Boot 3) 본문
[에러] LazyInitializationException: could not initialize proxy - no Session (Spring Boot 3)
AtoZ 개발자 2026. 8. 8. 09:00로컬에서는 잘 되던 조회 API가 배포하고 나서 이 로그를 뱉습니다.
org.hibernate.LazyInitializationException: could not initialize proxy
[com.example.order.Member#1] - no Session
컨트롤러를 고치면 사라지고, 서비스 코드를 정리하면 다시 나타납니다. 그러다 FetchType.EAGER로 바꿔서 덮고 넘어가는 경우가 많습니다. 그리고 몇 달 뒤 목록 API가 느려집니다.
이 글에서는 이 예외가 언제 반드시 터지는지, Spring Boot 3 환경에서 왜 다시 만나게 되는지, 그리고 상황별로 어떤 해결책을 골라야 하는지를 정리합니다. 원인과 설정 동작은 Spring Boot·Hibernate 공식 문서 기준으로 확인한 내용이고, 코드는 그대로 붙여 쓸 수 있는 최소 예제로 적었습니다.

영속성 컨텍스트가 살아 있는 구간과 프록시를 건드리는 시점이 어긋나면 예외가 납니다
이 예외는 "쿼리 실패"가 아닙니다
메시지를 나눠 읽으면 원인이 보입니다.
could not initialize proxy는 지연 로딩으로 만들어 둔 프록시 객체를 실제 데이터로 채우려 했다는 뜻입니다. no Session은 그 순간 채워 줄 Hibernate 세션(영속성 컨텍스트)이 이미 닫혀 있었다는 뜻입니다.
즉 SQL이 틀린 게 아니고, 접근 시점이 틀린 것입니다.
@ManyToOne(fetch = FetchType.LAZY)나 @OneToMany(기본값이 지연 로딩)로 매핑된 연관 엔티티는 조회 시점에 실제 값을 가져오지 않습니다. 대신 껍데기를 넣어 두고, 처음 쓸 때 쿼리를 한 번 더 보냅니다.
그 "처음 쓸 때"가 트랜잭션 밖이면 채울 방법이 없습니다. 그래서 예외가 납니다.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
// 지연 로딩: 여기에는 프록시가 들어간다
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
}
Spring Boot는 기본적으로 이 예외를 가려 줍니다
그런데 실무에서는 트랜잭션 밖에서 프록시를 건드려도 잘 동작하는 경험을 먼저 합니다. Spring Boot가 기본 설정으로 막아 주기 때문입니다.
Spring Boot 공식 문서는 이렇게 설명합니다.
If you are running a web application, Spring Boot by default registers
OpenEntityManagerInViewInterceptorto apply the "Open EntityManager in View" pattern, to allow for lazy loading in web views. If you do not want this behavior, you should setspring.jpa.open-in-viewtofalsein yourapplication.properties.
웹 애플리케이션이면 요청이 끝날 때까지 EntityManager를 열어 두는 인터셉터가 자동으로 등록된다는 뜻입니다. 그래서 컨트롤러나 뷰에서 프록시를 건드려도 세션이 남아 있어 예외가 나지 않습니다.
문제는 이 설정을 끄는 순간입니다. 성능 리뷰에서 spring.jpa.open-in-view=false로 바꾸면, 그동안 가려져 있던 접근들이 한꺼번에 예외로 드러납니다. "코드는 안 건드렸는데 갑자기 터진다"는 상황이 대부분 이 케이스입니다.
설정을 유지하더라도 안전하지 않은 구간이 있습니다.
@Async스레드나 별도 스레드풀에서 엔티티를 다루는 경우- 스케줄러·배치처럼 웹 요청이 아닌 실행 경로
- 응답 직렬화 이후, 인터셉터가 이미 닫힌 뒤의 후처리
- 테스트 코드에서 트랜잭션 없이 조회한 엔티티를 나중에 사용하는 경우
해결책은 네 가지고, 고르는 기준이 다릅니다
인터넷 검색으로 가장 먼저 나오는 답은 "EAGER로 바꿔라"입니다. 그건 가장 마지막에 고려할 선택지입니다.

상황별로 어떤 해결책을 고를지 — 단건 조회, 목록 조회, 컬렉션 조회의 갈림길
1) 필요한 연관만 fetch join 으로 함께 가져오기
조회 시점에 이미 무엇이 필요한지 안다면 이게 가장 정확합니다.
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o from Order o join fetch o.member where o.id = :id")
Optional<Order> findWithMember(@Param("id") Long id);
}
2) @EntityGraph 로 선언만 바꾸기
쿼리를 새로 쓰지 않고 기존 메서드에 로딩 범위만 얹는 방식입니다.
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"member"})
Optional<Order> findById(Long id);
}
3) 처음부터 DTO로 조회하기
응답에 필요한 필드만 뽑으면 프록시가 아예 생기지 않습니다. 목록 API에 가장 잘 맞습니다.
public record OrderView(Long orderId, String memberName) { }
@Query("""
select new com.example.order.OrderView(o.id, m.name)
from Order o join o.member m
where o.status = :status
""")
List<OrderView> findViews(@Param("status") OrderStatus status);
4) 트랜잭션 안에서 초기화해 두기
기존 구조를 크게 못 바꿀 때 쓰는 방법입니다. 서비스 계층 @Transactional 안에서 필요한 값을 미리 만져 둡니다.
@Transactional(readOnly = true)
public OrderView load(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
Hibernate.initialize(order.getMember()); // 또는 order.getMember().getName();
return new OrderView(order.getId(), order.getMember().getName());
}
정리하면 이렇게 고릅니다.
| 상황 | 권장 | 이유 |
|---|---|---|
| 단건 상세 조회 | fetch join 또는 @EntityGraph |
필요한 연관이 명확하고 쿼리 1번으로 끝난다 |
| 목록 조회(카드·표) | DTO 조회 | 엔티티를 만들지 않아 프록시 문제가 사라진다 |
| 컬렉션(1:N) + 페이징 | DTO 조회 또는 배치 사이즈 | 컬렉션 fetch join과 페이징을 같이 쓰면 위험하다 |
| 레거시 구조 유지 | 트랜잭션 안 초기화 | 변경 범위가 가장 작다 |
전역 EAGER 전환 |
권장하지 않음 | 목록 조회에서 불필요한 조인·쿼리가 늘어난다 |
컬렉션 fetch join 과 페이징을 함께 쓰면 생기는 일
@OneToMany를 fetch join 하면서 Pageable을 넘기면 Hibernate가 이런 경고를 남깁니다.
HHH90003004: firstResult/maxResults specified with collection fetch;
applying in memory
조인 결과에 중복 행이 생겨 DB 레벨 페이징을 신뢰할 수 없기 때문에, 메모리에서 자르겠다는 뜻입니다. 데이터가 적을 때는 통과하고, 늘어나면 그대로 사고가 됩니다. 경고 코드와 문구는 Hibernate 버전에 따라 조금씩 다르게 찍히니, 로그에서는 collection fetch 라는 표현으로 찾으면 됩니다.
컬렉션이 필요할 때는 두 갈래로 갑니다. 목록은 DTO로 얇게 조회하고, 상세에서만 컬렉션을 채우는 방식이 가장 안전합니다. 또는 @BatchSize(하이버네이트 배치 페치)로 N+1을 줄이는 쪽을 봅니다.
EAGER 로 덮으면 왜 나중에 문제가 되나요
FetchType.EAGER는 예외를 막아 주는 설정이 아니라, 모든 조회에서 항상 함께 가져오도록 매핑 자체를 바꾸는 설정입니다.
단건 조회 하나를 살리려고 매핑을 바꾸면, 그 엔티티가 등장하는 모든 목록 조회에 조인이나 추가 쿼리가 붙습니다. 목록 100건을 조회할 때 연관 조회가 100번 나가는 N+1이 여기서 나옵니다.
그래서 순서를 이렇게 잡는 편이 좋습니다. 매핑은 지연 로딩으로 두고, 쿼리 단위로 필요한 것만 함께 가져옵니다. 에러 메시지를 없애는 것과 구조를 고치는 것은 다릅니다.

Spring Boot 공식 문서의 open-in-view 설명 (확인일 2026-08-02)
재현해서 확인하는 순서
원인을 눈으로 보고 싶다면 이 순서가 가장 빠릅니다.
application.yml에spring.jpa.open-in-view: false를 넣는다- 컨트롤러에서 엔티티를 그대로 반환하는 API를 호출한다
LazyInitializationException이 나면, 그 지점이 트랜잭션 밖에서 프록시를 건드리는 코드다- 해당 조회를 fetch join·
@EntityGraph·DTO 중 하나로 바꾼다 - 다시 호출해 SQL 로그에서 쿼리 개수가 줄었는지 확인한다
spring.jpa.show-sql=true보다 logging.level.org.hibernate.SQL=debug를 켜는 편이 실행 시점을 보기 쉽습니다.
확인 목록
- [ ] 예외 메시지의 엔티티 이름으로 어느 연관이 문제인지 특정했다
- [ ] 그 프록시를 건드리는 코드가
@Transactional안인지 밖인지 확인했다 - [ ]
spring.jpa.open-in-view값을 알고 있다 (기본값은true) - [ ] 단건인지 목록인지에 따라 fetch join / DTO를 나눠 적용했다
- [ ] 컬렉션 fetch join 에 페이징을 같이 쓰지 않았다
- [ ]
EAGER전역 전환으로 덮지 않았다
다음에 볼 글
같은 조회 API에서 자주 함께 나오는 문제가 예외 응답 형식입니다. 에러 바디와 상태 코드를 표준화하는 방법은 ProblemDetail로 에러 응답 표준화하기에 정리해 두었습니다. JPA와 MyBatis, H2를 함께 쓸 때의 차이는 H2, JPA, MyBatis 특징 및 차이에서 볼 수 있고, 상태 코드 제어 자체는 HTTP Status Code 제어 글에 있습니다.
원인·설정 동작은 공식 문서 기준으로 정리했고, 버전에 따라 기본값이 달라질 수 있으니 본인 프로젝트의 Spring Boot·Hibernate 릴리스 노트로 한 번 더 확인해 주세요. 잘못된 내용이나 보충할 사례가 있으면 댓글로 알려 주시면 반영하겠습니다.
참고한 자료
- Spring Boot Reference — Data / SQL Databases,
spring.jpa.open-in-view및OpenEntityManagerInViewInterceptor(확인일 2026-08-02) - Hibernate ORM User Guide — Fetching, 프록시와 지연 로딩 (확인일 2026-08-02)
- Spring Framework Javadoc —
OpenEntityManagerInViewInterceptor(확인일 2026-08-02)
