[JPA] @Transaction을 붙이지 않았을 때 생기는 문제, JPA Proxy, LazyLoading과 EagerLoading N+1 문제

2025. 2. 22. 00:46·Spring/JPA
728x90
반응형
SMALL

오늘부터 리팩토링 또는 트러블 슈팅위주로 포스팅하려고 한다.

오늘 해결했던 문제

LazyLoading으로 설정한 엔티티가 가져와지지 않는 문제

2025-02-21T21:31:27.016+09:00  WARN 5521 --- [nio-8080-exec-6] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.server.delivery.model.store.entity.Store.operatingHours: could not initialize proxy - no Session]

위 warn 로그의 내용을 미약한 영어 독해 능력으로 해독해보자면 Lazy로 설정한 객체를 초기화할 수 없는 문제로 보인다. 그리고 세션이 없다는 문구가 마지막에 있다.

이에 대해서 JPA LazyLoading과 Proxy개념을 알아보고 @Transaction과 어떻게 상호작용하는지 적어보겠다.

JPA의 Lazy Loading과 프록시 객체

JPA를 사용하면 엔티티의 연관 관계를 설정할 때 LAZY(지연 로딩)와 EAGER(즉시 로딩) 중 하나를 선택할 수 있다. 이를 잘 활용하면 성능을 최적화할 수 있지만, 적절하지 않게 설정하면 성능 저하나 LazyInitializationException과 같은 문제가 발생할 수 있다. 이번 포스팅에서는 Lazy 로딩 방식과 프록시 객체, 그리고 Eager 로딩과 N+1 문제에 대해 알아보겠다.

1. Lazy가 동작하는 방식

Lazy Loading(지연 로딩)은 실제 엔티티가 필요한 순간까지 데이터를 조회하지 않고, 프록시 객체를 활용하여 참조를 유지하는 방식이다. JPA에서는 기본적으로 @OneToMany, @ManyToMany 관계에 대해 Lazy 로딩이 기본 설정으로 되어 있다.

@Entity
public class Store {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
    private List<OperatingHours> operatingHours;
}

위와 같이 fetch = FetchType.LAZY를 설정하면, Store 엔티티를 조회할 때 operatingHours 필드는 바로 조회되지 않고 프록시 객체로 남아 있다. 이를 실제로 접근하는 순간(store.getOperatingHours().size())에 쿼리가 실행된다.

2. @Transactional을 적용하면 수행하는 동작

@Transactional 어노테이션이 적용된 메서드 내부에서는 같은 트랜잭션을 공유하기 때문에 Lazy 로딩이 정상적으로 동작한다. 하지만 트랜잭션이 종료된 후 Lazy 객체를 조회하려고 하면 LazyInitializationException이 발생한다.

@Service
public class StoreService {
    @Transactional
    public Store getStore(Long id) {
        return storeRepository.findById(id).orElseThrow();
    }
}

이처럼 @Transactional이 적용된 메서드에서 Lazy 필드를 참조하면 정상적으로 데이터를 가져올 수 있다.

3. JPA의 프록시 방식

JPA는 Lazy 로딩을 위해 프록시 객체를 활용한다. Hibernate는 실제 엔티티가 아닌 프록시 객체를 먼저 반환하고, 해당 객체에 접근하는 순간 쿼리를 실행한다.

Store store = entityManager.getReference(Store.class, 1L);
System.out.println(store.getClass()); // Proxy 객체 확인

위 코드에서 store.getClass()를 출력하면 class com.server.delivery.model.store.entity.Store_$$_jvst123처럼 프록시 클래스가 생성된 것을 확인할 수 있다.

4. 연관 관계별 기본 Fetch 전략

JPA에서 연관 관계에 따른 기본 Fetch 전략은 다음과 같다.

연관 관계 기본 Fetch 전략
@ManyToOne EAGER
@OneToOne EAGER
@OneToMany LAZY
@ManyToMany LAZY

@ManyToOne과 @OneToOne 관계는 기본적으로 EAGER로 설정되므로 필요에 따라 LAZY로 변경하는 것이 좋다.

5. 언제 Eager 로딩이 유리할까?

Eager 로딩이 필요한 경우는 다음과 같다.

  • 항상 함께 조회해야 하는 데이터(예: User와 UserProfile 같은 필수 연관 데이터)
  • 조회 시 한 번에 데이터를 가져오는 것이 성능상 유리한 경우

하지만 대부분의 경우 EAGER는 피하는 것이 좋다. 특히 다대일(@ManyToOne)이나 일대일(@OneToOne) 관계에서도 필요하지 않다면 LAZY로 변경하는 것이 성능 최적화에 도움이 된다.

6. Eager 로딩의 N+1 문제와 해결 방법

Eager 로딩을 사용할 때 연관된 엔티티가 많으면 N+1 문제가 발생할 수 있다. 예를 들어, Store 엔티티를 조회할 때 operatingHours가 Eager 로딩이라면 다음과 같은 문제가 생긴다.

SELECT * FROM store; -- 1개의 쿼리 (N=1)
SELECT * FROM operating_hours WHERE store_id = ?; -- 추가 쿼리 (N개)

해결 방법

  1. JPQL에서 JOIN FETCH 사용

    @Query("SELECT s FROM Store s JOIN FETCH s.operatingHours WHERE s.id = :id")
    Store findByIdWithOperatingHours(@Param("id") Long id);
  2. @EntityGraph 활용

    @EntityGraph(attributePaths = {"operatingHours"})
    @Query("SELECT s FROM Store s WHERE s.id = :id")
    Store findByIdWithGraph(@Param("id") Long id);
  3. Hibernate의 Batch Size 설정

    spring.jpa.properties.hibernate.default_batch_fetch_size=100

Batch Size를 설정하면 한 번에 여러 개의 연관 데이터를 가져올 수 있어 N+1 문제를 완화할 수 있다.

7. 정리

  • LAZY는 프록시 객체를 활용하여 실제 필요할 때만 데이터를 가져온다.
  • @Transactional을 사용하면 트랜잭션 내에서 Lazy 로딩이 가능하다.
  • @ManyToOne과 @OneToOne은 기본적으로 EAGER이므로 필요에 따라 LAZY로 변경하는 것이 좋다.
  • Eager 로딩은 항상 필요한 데이터를 함께 조회할 때 유리하지만, N+1 문제가 발생할 수 있다.
  • JOIN FETCH, @EntityGraph, Batch Size 등의 방법으로 N+1 문제를 해결할 수 있다.

적절한 Fetch 전략을 선택하여 성능을 최적화하는 것이 중요하다.

728x90
반응형
SMALL

'Spring > JPA' 카테고리의 다른 글

[JPA] JPA vs MySQL vs JDBC vs JPQL vs QueryDSL  (0) 2025.03.11
[JPA, Kafka] @Transactional과 Kafka 메세지 전송 시점  (0) 2025.03.09
[JPA] Soft Delete 개발 방법(Hard Delete과의 차이)  (0) 2025.02.17
[개념 정리]Spring Boot에서 JPA의 Soft Delete와 Cascade 연관관계  (1) 2024.12.09
save the transient instance before flushing  (1) 2024.11.20
'Spring/JPA' 카테고리의 다른 글
  • [JPA] JPA vs MySQL vs JDBC vs JPQL vs QueryDSL
  • [JPA, Kafka] @Transactional과 Kafka 메세지 전송 시점
  • [JPA] Soft Delete 개발 방법(Hard Delete과의 차이)
  • [개념 정리]Spring Boot에서 JPA의 Soft Delete와 Cascade 연관관계
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325)
      • 면접 준비 (22)
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11)
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      spring redis
      springsecurity
      JS
      자바 반복문
      자바 알고리즘
      Spring Data Redis
      java
      CSS
      프로그래머스
      jsp request
      자바스크립트
      HTML
      Til
      jsp기초
      Springframework
      스프링프레임워크
      자바
      자바기초
      백준
      JSP
      자바 면접
      redis 채팅
      Spring
      자바 면접질문
      redis
      JavaScript
      리눅스마스터2급
      리눅스
      레디스
      리눅스마스터2급정리
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [JPA] @Transaction을 붙이지 않았을 때 생기는 문제, JPA Proxy, LazyLoading과 EagerLoading N+1 문제
    상단으로

    티스토리툴바