개발하는 쿠키
article thumbnail
Published 2023. 6. 21. 15:00
JPA Proxy Coding/computer science

JPA 프록시란?

프록시는 JPA에서 지연 로딩(Lazy Loading)을 구현하기 위해 사용되는 기술입니다.

 

지연 로딩(Lazy Loading)은 DB에서 연관된 엔티티를 필요한 시점에 가져오는 기능으로 즉시 로딩(Eager Loading)과 반대되는 개념입니다.

 

JPA프록시는 실제 엔티티 대신 가짜 엔티티를 가져옵니다.

엔티티 A, B가 연관 관계에 있다고 가정할 때, A의 실제 엔티티를 조회하면서 B의 가짜 엔티티를 가져오는 것입니다.

그리고 B를 조회할 때, 진짜 엔티티를 가져옵니다.

 

따라서 B가 필요할 때까지는 조회 쿼리를 실행하지 않고, 프록시 객체를 반환하기 때문에 성능을 최적화할 수 있습니다.

JPA 프록시 작동 원리

1. Order 클래스와 Customer 클래스가 다 : 1 의 연관관계를 가졌다고 가정하겠습니다.

@Entity
public class Order {
    @Id
    private Long id;
    
    private String orderNumber;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;
    
    // 생성자, getter, setter 등 필요한 코드
}

@Entity
public class Customer {
    @Id
    private Long id;
    
    private String name;
    
    // 생성자, getter, setter 등 필요한 코드
}

2. 이때 order.getCustomer() 를 하게 되면 customer객체에 프록시 객체가 저장됩니다.

Order order = entityManager.find(Order.class, 1L);
Customer customer = order.getCustomer();

3. customer를 이용해서 내부값을 조회할 때 DB에 접근해서 실제 데이터를 로딩합니다.

String name = customer.getName(); // 조회 쿼리 생성

이때 프록시는 필요한 경우에만 데이터를 로딩하므로, 필요한 데이터만 로딩되어 성능이 향상됩니다.

JPA프록시 객체

JPA 프록시는 실제 객체를 감싸는 Wrapper로서, 동일한 인터페이스를 가지고 있습니다.

그래서 프록시 객체를 이용해서 getName()을 하면 실제 객체의 getName()이 실행됩니다.

 

하지만, getId()를 할 때는 select쿼리가 발생하지 않습니다. 식별자를 조회할 때는 프록시를 초기화하지 않습니다.

영속성 컨텍스트에 존재하는 엔티티의 ID를 반환하기 때문입니다. 만약, 영속성 컨텍스트에 엔티티가 없을 경우에는 DB를 조회해서 실제 엔티티를 가져옵니다.

JPA프록시 사용 시 주의사항

1. N+1 문제

원인: 부모 엔티티를 먼저 가져오고, N개의 자식 엔티티를 가져와야 할 경우 N+1 문제가 발생합니다.

부모 조회 쿼리 + 자식조회 쿼리 * N = N+1

 

Fetch Join 또는 엔티티 그래프를 사용해서 필요한 연관 엔티티를 한 번에 로딩하는 것이 좋습니다.

 

1-1. Fetch Join

부모 - 자식처럼 연관된 엔티티를 한 번에 조회하는 방법입니다.

QParent parent = QParent.parent;
QChild child = QChild.child;

List<Parent> parents = queryFactory
    .selectFrom(parent)
    .innerJoin(parent.children, child).fetchJoin()
    .fetch();

 

1-2. 엔티티 그래프

엔티티 간의 관계를 그래프로 정의하고, 이를 이용해서 한 번의 쿼리로 조회하는 방법입니다.

// Parent 엔티티의 자식 엔티티인 Child 엔티티를 함께 로드하는 그래프 코드 정의 방법
@Entity
@NamedEntityGraph(name = "parentWithChildren", attributeNodes = @NamedAttributeNode("children"))
public class Parent {
    // 엔티티 필드 및 메서드
}

// @NamedEntityGraph(name = "엔티티 그래프 이름", attributeNodes = 그래프에 포함시킬 엔티티 속성
@EntityGraph("parentWithChildren")
List<Parent> parents = entityManager.createQuery("SELECT p FROM Parent p", Parent.class)
        .setHint("javax.persistence.fetchgraph", entityManager.getEntityGraph("parentWithChildren"))
        .getResultList();

 

2. 프록시와 상태 변경 감지

프록시 객체는 실제 엔티티를 Wrapper로 감싼 형태이므로 프록시 객체를 통해 상태를 변경하면 실제 엔티티에도 영향을 줍니다.

 

그러나 잘못 사용하는 경우 프록시 객체가 초기화되지 않을 수 있습니다.

2-1. 엔티티의 상태 변경을 트랜젝션 범위 밖에서 수행하는 경우

// 잘못된 코드 예시
@Transactional
public void updateOrder(Long orderId, String newStatus) {
    Order order = entityManager.find(Order.class, orderId);
    order.setStatus(newStatus);
}

// ...

updateOrder(1L, "Shipped");
entityManager.flush(); // 프록시와 상태 변경 감지가 작동하지 않음

 

3. 동일성 비교

두 개의 프록시 객체를 비교하는 경우 실제 동일한 엔티티를 가리키고 있다고 해도 false가 반환됩니다.

따라서 프록시 객체를 비교할 때 엔티티의 식별자(ID)를 비교해야 합니다.

 

ID는 영속성 컨텍스트에서 보관하고 있으므로 프록시를 통한 비교는 실제 데이터에 접근하지 않고도 비교가 가능합니다.

 

3-1. 기본 equals메서드의 문제점

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (o == null || getClass() != o.getClass()) {
        return false;
    }
    Customer customer = (Customer) o;
    return Objects.equals(id, customer.id);
}
Customer customer = order.getCustomer();
Customer sameCustomer = order.getCustomer();
assertThat(customer).isEqualTo(sameCustomer);

위와 같이 하게 되면 테스트 실패가 뜹니다.

customer, sameCustomer는 프록시 객체를 가져오게 되는데 이것의 equals메서드를 실행하게 되면 실제 Customer객체의 equals메서드가 실행됩니다.

이 때, customer는 실제 객체로 인지되는데, 매개변수로 들어가는 sameCustomer는 프록시 객체로 인지되어 테스트 실패가 뜹니다.

 

따라서 다음과 같이 고칠 수 있습니다.

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (!(o instanceof Customer)) {
        return false;
    }
    Customer customer = (Customer) o;
    return Objects.equals(id, customer.getId());
}

참고자료

- https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/

- https://velog.io/@ohzzi/JPA-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4

반응형
profile

개발하는 쿠키

@COOKIE_

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!