
JPA 사용 시 LazyInitializationException 에러 원인과 해결법
JPA를 사용하는 개발자라면 한 번쯤은 마주치는 LazyInitializationException 에러.
처음엔 낯설지만, 점점 프로젝트가 커지면서 본격적인 관계 매핑이 시작되면 마치 복병처럼 찾아오곤 하죠.
“분명히 엔티티를 불러왔는데, 왜 이 에러가 나지?”라며 디버깅의 늪에 빠져본 경험, 있으신가요?
이 에러는 단순히 설정 문제일 수도 있지만, JPA의 지연 로딩(Lazy Loading) 원리를 정확히 이해하지 못한 채 사용했기 때문일 가능성이 큽니다.
이번 글에서는 이 LazyInitializationException이 발생하는 진짜 이유부터, 실무에서 이를 어떻게 해결하고 방지할 수 있는지까지 사례 중심으로 풀어보겠습니다.
목차
JPA 사용 시 LazyInitializationException 에러 원인과 해결법
JPA를 사용하는 개발자라면 한 번쯤은 마주치는 LazyInitializationException 에러.
처음엔 낯설지만, 점점 프로젝트가 커지면서 본격적인 관계 매핑이 시작되면 마치 복병처럼 찾아오곤 하죠.
“분명히 엔티티를 불러왔는데, 왜 이 에러가 나지?”라며 디버깅의 늪에 빠져본 경험, 있으신가요?
이 에러는 단순히 설정 문제일 수도 있지만, JPA의 지연 로딩(Lazy Loading) 원리를 정확히 이해하지 못한 채 사용했기 때문일 가능성이 큽니다.
이번 글에서는 이 LazyInitializationException이 발생하는 진짜 이유부터, 실무에서 이를 어떻게 해결하고 방지할 수 있는지까지 사례 중심으로 풀어보겠습니다.
목차
📌 LazyInitializationException이란?
LazyInitializationException
은 Hibernate에서 지연 로딩(Lazy Loading) 방식으로 로딩된 연관 엔티티를 세션이 닫힌 후 접근할 때 발생하는 런타임 에러입니다.
쉽게 말해, JPA가 데이터베이스와 연결된 상태가 아닌데 연관 객체를 불러오려고 하면 이 에러가 터지는 것이죠.
예시 메시지: org.hibernate.LazyInitializationException: could not initialize proxy - no Session
📌 에러가 발생하는 이유
이 에러의 핵심 원인은 영속성 컨텍스트(Persistence Context)가 종료된 후, 아직 로딩되지 않은 연관 객체를 접근했기 때문입니다.
주로 다음과 같은 상황에서 자주 발생합니다:
- Service 계층에서 데이터를 조회 후, Controller에서 연관 객체를 접근할 때
- FetchType.LAZY로 설정된 연관 필드를 View나 JSON 직렬화 도중 참조할 때
- Open Session in View 패턴을 사용하지 않거나, 트랜잭션 범위 밖에서 엔티티를 사용할 때
📌 실무에서 마주친 실제 사례
몇 달 전 진행한 프로젝트에서 이런 일이 있었습니다.
Spring Boot + JPA 환경에서 게시글(Post)과 작성자(User)가 @ManyToOne 관계였고, User는 LAZY로 설정되어 있었습니다.
@Entity
public class Post {
@ManyToOne(fetch = FetchType.LAZY)
private User writer;
}
게시글 목록을 조회한 후, 화면에서 작성자 이름을 출력하려고 했죠. 그런데 아래와 같은 오류가 발생했습니다:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
원인은 간단했습니다. Controller에서 Post 엔티티 리스트를 그대로 View에 넘겼고, View에서 작성자 이름을 꺼내려 할 때, 이미 세션이 종료된 상태였던 것입니다.
이 오류로 인해 우리는 DTO를 사용해 필요한 필드만 미리 추출하는 방식으로 코드를 리팩토링했습니다.
📌 해결 방법과 우회 전략
이 문제를 해결하는 방법은 다양하며, 상황에 따라 다른 접근이 필요합니다:
- DTO 사용 (데이터 전용 객체):
Service 계층에서 필요한 정보만 추출하여 DTO에 담고, Controller에서는 엔티티 대신 DTO를 전달합니다. - Fetch Join 사용:
JPQL에서JOIN FETCH
를 사용하여 연관 객체를 미리 로딩합니다.@Query("SELECT p FROM Post p JOIN FETCH p.writer") List<Post> findAllWithWriter();
- Open Session in View 패턴 사용:
Spring Boot의 기본 설정에서는 OSIV가 true입니다. 이 설정을 유지하면 View에서도 지연 로딩이 가능합니다.
단, 너무 많은 객체를 로딩하거나 보안상 이슈가 있다면 비추천입니다. - Hibernate.initialize() 명시적 초기화:
트랜잭션 안에서 지연 로딩 객체를 강제로 초기화하는 방법도 있습니다.
하지만 이 방식은 남용할 경우 코드 가독성이 떨어지고 유지보수가 어려워질 수 있습니다.Hibernate.initialize(post.getWriter());
실무에서는 DTO 방식과 Fetch Join을 가장 많이 사용합니다. 불필요한 연관 객체 접근을 방지하고, 성능 최적화도 함께 도모할 수 있기 때문이죠.
📌 방지하기 위한 베스트 프랙티스
LazyInitializationException을 예방하려면 처음부터 구조를 잘 설계하는 것이 중요합니다. 다음과 같은 습관이 도움이 됩니다:
- 엔티티 반환을 지양하고 DTO 중심 설계
- Fetch Join 전략을 데이터 조회 요구사항에 맞게 정리
- 단위 테스트를 통해 Lazy 로딩 상황 검증
- 엔티티 간 순환 참조 및 과도한 연관관계 지양
특히, 단순한 CRUD 수준을 넘어 도메인 로직이 복잡해질수록 DTO와 Fetch 전략의 중요성은 더욱 커지게 됩니다.
📌 추가적인 경험과 실용적인 팁
개발을 하다 보면 처음 보는 에러 메시지에 당황하고, 구글링을 시작하면서 점점 그 늪에 빠져드는 순간이 있죠.LazyInitializationException
도 그런 에러 중 하나였습니다. 저도 한참을 헤맸고, 수많은 시행착오를 거쳐 어느 순간부터는 "이건 미리 방지해야 하는 에러"라는 걸 체득하게 됐습니다.
1. 무조건 엔티티 대신 DTO 설계부터 시작하세요
처음엔 JPA가 너무 편해서 엔티티를 그대로 View에 노출시키곤 했습니다. "어차피 연관된 값도 가져오겠지"라는 안일한 생각이었죠.
하지만 프로젝트 규모가 커지고 연관관계가 복잡해질수록 엔티티 직접 노출은 위험하다는 걸 뼈저리게 느꼈습니다.
이후부터는 항상 필요한 데이터만 추출해서 DTO로 가공하는 걸 기본으로 하고 있어요. 유지보수와 성능 모두에서 이득입니다.
2. 필수 연관 엔티티는 Fetch Join으로 적극적으로 가져오자
모든 관계를 Lazy로 두는 건 맞지만, 그 중에서도 매번 조회할 때마다 반드시 필요한 연관 데이터가 있다면?
그럴 땐 주저 말고 JOIN FETCH
를 사용하세요. Service 단에서 데이터를 한번에 가져올 수 있어, 뷰 렌더링 시 에러가 나는 일은 거의 없어집니다.
예를 들어 게시글과 작성자 정보는 대부분 같이 사용되니, 조회 시 함께 Fetch Join 처리하는 게 유리합니다.
3. OSIV(Open Session in View)는 단기적으론 좋지만, 장기적으론 고려해야 할 점이 많아요
OSIV 설정을 켜두면 지연 로딩 에러는 쉽게 해결되지만, 서비스 계층이 아닌 뷰에서 DB 세션에 접근하게 됩니다.
이는 트랜잭션 관리 흐름을 흐릴 수 있고, 보안상 민감한 객체가 뷰에서 호출되는 등 부작용이 생길 수 있어요.
특히 API 서버라면 OSIV는 꺼두고, 서비스 계층 내에서 필요한 데이터만 확실하게 처리하는 게 가장 안전합니다.
4. 테스트 케이스에서 Lazy 로딩을 철저히 검증하세요
에러는 개발할 때보다 운영 배포 후 사용자 요청에서 발견되는 게 더 치명적입니다.
그래서 저는 항상 연관 객체가 필요한 상황의 테스트 코드를 작성해서 Lazy 로딩을 검증합니다. 특히, MockMvc 테스트에서 JSON 직렬화 시점을 체크하면 생각보다 많은 실수를 미리 잡을 수 있어요.
5. 예외 로그를 피드백으로 삼아 아키텍처를 리팩토링해보세요
처음에는 에러 로그가 무섭기만 했지만, 이제는 하나의 설계 피드백처럼 보이게 됐어요.
LazyInitializationException이 떴다면, “내가 뭔가 불필요하게 설계를 했거나, 데이터 흐름을 오해하고 있는 게 아닐까?”를 먼저 돌아보게 됩니다.
이 과정을 통해 코드 품질이 높아지고, 팀원들과의 협업도 더 명확해졌습니다.
결국 중요한 건, “왜 이 객체를 지금 여기서 불러오려 하지?”라는 질문을 스스로에게 던지는 습관이에요.
데이터를 언제, 어디서, 어떻게 로딩할지를 명확하게 의도하고 코딩한다면 LazyInitializationException은 이제 더 이상 두려운 존재가 아니게 될 겁니다.
📌 결론
LazyInitializationException
은 단순한 실수로 보일 수 있지만, JPA의 핵심 개념인 지연 로딩과 영속성 컨텍스트에 대한 이해가 부족하면 반복해서 발생하는 문제입니다.
실무에서는 “왜 이 객체를 지금 접근하려고 하지?”라는 질문을 통해 설계의 의도를 다시 점검하고, Lazy 로딩 전략을 의식적으로 다뤄야만 안정적인 개발이 가능합니다.
특히, DTO를 적극적으로 활용하고, 필요 시 Fetch Join을 적절히 사용하는 것이 가장 현실적이고 효과적인 접근입니다.
단기적으로는 OSIV 설정에 기대는 것도 방법이지만, 장기적으로는 트랜잭션과 데이터 흐름을 명확히 분리한 구조가 유지보수와 성능 측면에서 더 좋습니다.
무엇보다도 중요한 건, 에러를 피하기보다는 에러를 통해 배우고, 구조를 개선해 나가는 개발자 마인드입니다.
LazyInitializationException도 결국 더 나은 아키텍처로 가기 위한 힌트일 수 있거든요. 이 에러가 익숙해질수록, 더 이상 두렵지 않고 오히려 반가워질지도 몰라요.