[학습] @ManyToOne(fetch = FetchType.LAZY)이 뭘까?

김동혁·2025년 12월 7일

기본 개념

@ManyToOne은 JPA에서 다대일(N:1) 관계를 매핑할 때 사용하는 어노테이션이다.
여기서 fetch = FetchType.LAZY는 지연 로딩(Lazy Loading) 전략을 의미한다.

Fetch 타입 비교

LAZY (지연 로딩)

  • 연관된 엔티티를 실제로 사용할 때 DB에서 조회한다
  • 성능 최적화에 유리하다

EAGER (즉시 로딩)

  • 연관된 엔티티를 처음부터 함께 DB에서 조회한다
  • @MayToOne의 기본값
  • 불필요한 데이터까지 로딩될 수 있다

코드 예제

// 주문(Order)과 회원(Member)의 다대일 관계

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    private String orderNumber;
    
    // LAZY: Member를 실제 사용할 때까지 조회하지 않음
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

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

동작 방식 비교

EAGER 사용시

Order order = orderRepository.findById(1L);
// SQL: SELECT * FROM orders o 
//      JOIN members m ON o.member_id = m.id 
//      WHERE o.id = 1
// → Order와 Member를 한 번에 조회 (JOIN 쿼리)

LAZY 사용시

Order order = orderRepository.findById(1L);
// SQL: SELECT * FROM orders WHERE id = 1
// → Order만 조회

String memberName = order.getMember().getName();
// SQL: SELECT * FROM members WHERE id = ?
// → Member를 실제 사용할 때 추가 쿼리 실행 (프록시 객체 초기화)

왜 LAZY 사용하나?

  1. 불필요한 데이터 로딩 방지
    • 연관 엔티티를 사용하지 않는 경우 조회하지 않아 성능 향상
  2. 메모리 효율성
    • 필요한 데이터만 메모리에 로드
  3. 초기 로딩 속도 개선
    • 첫 조회 시 필요한 최소한의 데이터만 가져옴

주의사항

N+1 문제

List<Order> orders = orderRepository.findAll();
// SQL: SELECT * FROM orders (1번)

for (Order order : orders) {
    String name = order.getMember().getName();
    // SQL: SELECT * FROM members WHERE id = ? (N번)
}
// → 총 N+1번의 쿼리 실행!

해결 방법

필요할 때 명시적으로 가져오는 방법을 사용한다

// Fetch Join 사용
@Query("SELECT o FROM Order o JOIN FETCH o.member")
List<Order> findAllWithMember();

// EntityGraph 사용
@EntityGraph(attributePaths = {"member"})
List<Order> findAll();

LazyInitializationException

@Transactional
public Order getOrder(Long id) {
    return orderRepository.findById(id);
}

// 트랜잭션 밖에서 사용
Order order = orderService.getOrder(1L);
order.getMember().getName(); // ❌ 예외 발생!
// → 영속성 컨텍스트가 닫혀서 프록시 초기화 불가

해결 방법

  • 트랜잭션 안에서 필요한 데이터를 미리 초기화
  • @Transactional 범위를 조정
  • Open Session In View 패턴 사용 (Spring Boot 기본값)

결론

JPA 공식 문서에는 모든 연관관계를 LAZY로 설정하고, 필요한 경우에만 fetch join으로 최적화하는 것을 권장하고 있다고 한다. @ManyToOne의 기본값은 EAGER이므로 명시적으로 LAZY를 설정하고 사용하자!

0개의 댓글