Java 아키텍처·Spring 용어 사전
JPALAZY · EAGER · 프록시 객체

지연 로딩

연관 엔티티를 실제로 쓸 때까지 조회를 미루는 것. 모든 연관은 LAZY가 기본이어야 한다.

연관된 엔티티를 실제로 사용하는 순간까지 조회를 미루는 전략.

어떻게 미루나 — 프록시

@Entity
class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;          // 진짜 Member 가 아니라 프록시가 들어온다
}

Order o = em.find(Order.class, 1L);       // SELECT order ...  (member 는 안 읽는다)
System.out.println(o.getMember().getClass());
// class Member$HibernateProxy$xxx        ← 껍데기

System.out.println(o.getMember().getName());   // 이 순간 SELECT member ... 가 나간다

프록시는 id만 들고 있는 껍데기다. id 외의 필드에 접근하는 순간 초기화(조회)가 일어난다.

o.getMember().getId();     // 프록시가 이미 아는 값 → 쿼리가 안 나간다

왜 항상 LAZY인가

@ManyToOne · @OneToOne 의 기본값이 EAGER 다 ← 함정 @OneToMany · @ManyToMany 의 기본값은 LAZY

// 반드시 명시한다
@ManyToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY)

EAGER의 문제는 예측 불가능성이다.

EAGER 로 두면

  • 필요 없어도 항상 조인해 읽는다
  • JPQL 로 조회하면 조인이 아니라 추가 쿼리가 나간다 → N+1 이 된다
  • 연관이 연쇄되면 한 번의 조회가 테이블 여러 개를 끌고 온다

"전부 LAZY로 두고, 필요할 때 페치 조인으로 명시적으로 가져온다" 가 정석이다.

프록시를 다룰 때의 함정

// ① instanceof · equals 가 어긋난다
if (o.getMember() instanceof Member) { }        // 프록시도 Member 의 자식이라 true
member.equals(o.getMember());                   // 클래스 비교로 짠 equals 면 false

// ② 초기화 여부 확인
Hibernate.isInitialized(o.getMember());
em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(o.getMember());

equals/hashCodeid 기준으로 작성하고 getClass() 대신 instanceof를 쓰는 것이 프록시 안전한 구현이다.

면접 함정

  • "@ManyToOne의 기본값은 LAZY"EAGER다. 명시하지 않으면 조용히 EAGER로 돈다.
  • "EAGER면 N+1이 없다" → JPQL 조회에서는 오히려 N+1이 난다.

한 번에 여러 프록시를 초기화하기

// 컬렉션을 강제로 초기화한다
Hibernate.initialize(order.getItems());

// DTO 로 변환하며 자연스럽게 초기화 (권장 — 트랜잭션 안에서)
return new OrderDto(o.getId(), o.getMember().getName(),
                    o.getItems().stream().map(ItemDto::from).toList());

equals·hashCode를 프록시 안전하게

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Member m)) return false;   // getClass() 비교는 프록시에서 깨진다
    return id != null && id.equals(m.getId());    // id 기준
}
@Override public int hashCode() { return getClass().hashCode(); }   // 상수여도 된다

getClass() == o.getClass() 로 비교하면

  • Member 와 Member$HibernateProxy 가 다르다고 판정된다
  • Set 에 넣거나 contains 로 찾을 때 조용히 실패한다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — JPA·영속성 — 영속성 컨텍스트·N+1