Spring/JPA

[JPA] 프록시와 지연 로딩

깃짱 2023. 7. 31. 16:30
반응형

 

💋 프록시의 등장 배경

 

아래와 같이 생긴 엔티티가 있다고 생각해보자. (자세히 다 읽을 필요는 없음)

 

@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
public class Coupon extends BaseDate {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    private LocalDate expiredDate;

    @Enumerated(EnumType.STRING)
    private CouponStatus status = CouponStatus.ACCUMULATING;

    private Boolean deleted = Boolean.FALSE;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "cafe_id")
    private Cafe cafe;

    @OneToOne(fetch = LAZY)
    @JoinColumn(name = "coupon_design_id")
    private CouponDesign couponDesign;

    @OneToOne(fetch = LAZY)
    @JoinColumn(name = "coupon_policy_id")
    private CouponPolicy couponPolicy;
    
    // ...
}

 

여러 연관관계가 보인다. 

Coupon 하나만 가져오게 되면, customer, cafe, couponDesign, couponPolicy의 객체는 모두 연결되어 조회할 수 있게 된다. 

 

하지만, 우리가 Coupon 객체를 조회할 때 항상 위의 네 가지 객체를 모두 사용할까?

어떤 경우에는 Coupon 객체를 통해서 Cafe를 조회하고, 어떤 경우에는 Customer를 조회할 수 있다.

매번 함께 출력하는 경우에는 괜찮겠지만, 서비스에서는 매번 연관된 모든 객체를 전부 조회하지는 않는다. 

 

이런 비효율을 JPA는 지연 로딩과 Proxy로 해결한다. 

 

 

💋 프록시 개념

 

✔ 프록시 객체를 가져오는 방법

프록시 객체를 만나보려면, 아래의 두 방법을 이용하면 된다.

 

 

 em.find() VS em.getReference()

em.find(): 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.

em.getReference(): 데이터베이스의 조회를 미루는 프록시 엔티티 객체를 조회한다. 

연관된 엔티티의 추가 조회는, 해당 엔티티가 필요한 시점까지 미룰 수 있고, 그전까지는 실제 객체와 형태만 맞춘 채로 조회하지 않는다. 

 

 

프록시 객체의 특징

실제 클래스를 상속받아서 만들어지기 때문에 실제 클래스와 다형성 문제를 일으키지 않는다. 

예를 들어서, 위의 클래스의 Coupon 객체에 대해 getReference()를 통해서 프록시 객체를 받았다면,

 

Coupon coupon = em.getReference(Coupon.class, couponId) 로 나타낼 수 있다. 

따라서 사용하는 입장에서는 현재 객체가 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용해도 된다. 

하지만, 타입 체크 시에는 ==로 조회하면 실제 객체와 프록시 객체의 참조값이 달라서 false를 반환하게 되므로, instance of를 통해서 타입을 체크해야 한다. 

 

✔ 프록시 객체의 초기화

1. 초기화되지 않은 상태의 MemberProxy가 있다. 현재는 프록시 객체만 생성한 상태고, getName() 요청을 받는다.

2. 프록시 객체는 영속성 컨텍스트에 초기화 요청을 한다. 현재 단계의 프록시 객체는 비어있기 때문에, 아무 내용도 들어있지 않다.

3. 영속성 컨텍스트는 데이터베이스를 조회해서 해당하는 데이터를 찾는다.

4. 실제 엔티티가 생성되어 영속성 컨텍스트에 저장된다. 

5. 이때 프록시 내부에 내용이 채워지는 것이 아니고, 프록시는 실제 엔티티의 내용을 호출해서 반환한다. 프록시 객체가 실제 엔티티로 바뀌는 것이 아니고, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근하게 된다. 

 

✔ 프록시 객체의 확인

JPA는 프록시 객체의 현재 상태에 대한 여러 확인 메서드를 제공한다. 

 

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    • entity.getClass().getName() 출력(HibernateProxy 어쩌구의 형태다.)
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity);

 

✔ 프록시 객체의 초기화는 영속성 컨텍스트의 관리를 받을 때만 가능하다!

 

프록시의 초기화는 영속성 컨텍스트의 관리를 받을 때만 가능하다. 

영속성 컨텍스트의 관리를 받지 못하는 준영속 상태의 프록시를 초기호하거나, 트랜잭션의 바깥에서 프록시를 초기화하는 경우에는 LazyInitializationException이 발생할 수 있다. 

 

프록시를 초기화할 때는 반드시! 프록시가 영속 상태여야 한다

 

 

 

💋 JPA의 지연로딩

JPA를 사용하면, 객체를 통해서 연관관계를 탐색할 수 있다는 장점이 있다. 

하지만, 객체의 관계와 달리 엔티티는 데이터베이스에 테이블의 형태로 저장되어 있다. 따라서 하나의 엔티티를 조회할 때 연관된 모든 엔티티를 조회하는 것은 굉장히 비효율적이다. 한 객체 조회 시 따라서 필요한 연관관계만 조회하고 싶을 때 JPA는 지연 로딩을 지원한다.

 

이때 우리가 사용하는 JPA 구현체인 하이버네이트에서 구현한 방식이 위에서 설명한 프록시 객체를 통한 방식이다. 

 

✔ 즉시로딩 VS 지연로딩

 

지연로딩을 사용하면 JPA는 위에서 설명한 프록시 객체를 반환한다. 

따라서, 엔티티 자체를 가져온 시점에는 프록시 객체를 초기화하지 않고, 연관된 객체를 실제로 사용하는 시점에서 프록시 객체를 초기화한다. 

 

반면에 즉시 로딩을 사용하게 되면 JPA는 프록시 객체가 아닌 실제 객체를 반환한다. 

Eager로 연관관계를 매핑하는 경우에는, 하나의 엔티티를 조회하면 연관된 모든 eager 객체를 join을 사용해서 SQL로 한꺼번에 함께 조회한다. 

 

✔ 지연로딩 설정 방법

 

지연로딩은 아래와 같이 설정할 수 있다. 

@XXXToOne은 기본이 즉시 로딩이기 때문에 별도로 LAZY 설정을 해줘야 한다. 

@XXXToMany는 기본이 지연 로딩이다. 

 

@Entity
public class Member {

  @Id
  @GeneratedValue
  private Long id;
  
  @Column(name = "USERNAME")
  private String name;
  
  @ManyToOne(fetch = FetchType.LAZY) // 지연로딩 설정
  @JoinColumn(name = "TEAM_ID")
  private Team team;
 
  // ...
}

 

즉시로딩의 경우 LAZY 대신 EAGER를 써주면 되지만, 데이터베이스에 전송되는 SQL의 형태에 대해 예측하기 어렵기 때문에, 권장되는 방법은 아니다. 

지연로딩을 사용하면서 1+N 문제를 해결하기 위해서는 fetch join, 엔티티 그래프 기능을 사용하는 것이 더 권장된다!

 

 

 

 

💋 참고자료

반응형