프록시는 JPA에서 사용되는 가짜 객체이다. JPA는 데이터베이스에서 데이터를 바로 가져오는 대신, 실제 데이터를 감싸는 대리 객체를 생성한다. 이 대리 객체가 필요할 때 데이터를 대신 불러온다. 쉽게 말하면 데이터를 바로 꺼내기보다는 꺼낼 준비만 하고 기다리는 가짜 객체이다.
JPA의 핵심 기능 중 하나는 지연 로딩(Lazy Loading)이다. 지연 로딩은 정말 필요한 순간에만 데이터를 가져오는 방식인데, 이걸 구현하려면 프록시가 필요하다.
@Entity
class Product {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
private List<Review> reviews; // 상품 리뷰
}
fetch = FetchType.Lazy로 설정하면 상품을 가져올 때 리뷰는 가져오지 않는다. 대신, 리뷰 리스트는 프록시 객체로 초기화된다. 리뷰를 실제로 조회할 때 JPA가 DB에서 데이터를 가져온다.
프록시는 객체의 메모리를 아끼고, 불필요한 쿼리를 막는 데 유용하다.
프록시는 편리하지만, 제대로 이해하지 못하면 문제를 일으킬 수 있다.
Product product = productRepository.findById(1L);
List<Review> reviews = product.getReviews(); // Lazy Loading
em.close(); // 영속성 컨텍스트 종료
reviews.get(0); // 예외 발생! LazyInitializationException
List<Product> products = productRepository.findAll(); // 모든 상품 조회
for (Product product : products) {
System.out.println(product.getReviews().size()); // 리뷰 조회 (쿼리 발생)
}
상품이 10개라면 1번의 상품 조회 + 10번의 리뷰 조회 쿼리가 발생한다.
(Fetch Join이나 배치 로딩을 이용해 해결 가능)
Lazy와 Eager의 적절한 조합
필요한 데이터를 미리 로드(Eager)할지, 필요할 때 로드(Lazy)할지 신중히 결정한다.
JPQL 페치 조인 사용
지연 로딩이 예상되는 데이터를 함께 조회한다.
@Query("SELECT p FROM Product p JOIN FETCH p.reviews WHERE p.id = :id")
Product findByIdWithReviews(@Param("id") Long id);
Hibernate.initialize(product.getReviews());