[JPA-Basic][8-1] 프록시와 연관관계 관리

kiteB·2021년 10월 17일

JPA

목록 보기
8/28
post-thumbnail

[ 프록시 ]

1. 등장 배경

🤔 Member를 조회할 때 항상 Team도 함께 조회해야 할까?

비즈니스 상황에 따라서 다르겠지만,

  • 회원과 팀을 함께 출력하는 printUserAndTeam은 Member, Team을 따로 가져오는 것보다 한번에 같이 가져오는 것이 성능상 이득이다.
  • 회원만 출력하는 printUser의 경우 Team 데이터까지 같이 가져올 필요가 없다. (낭비이기 때문!)

JPA는 이러한 낭비를 하지 않기 위해 프록시와 지연 로딩으로 해결한다!


em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜 (프록시) 엔티티 객체 조회

2. 프록시 특징

프록시 클래스는 실제 클래스를 상속 받아서 만들어지기 때문에 실제 클래스와 겉 모양이 같다.
우리는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출하게 된다.

3. 프록시 초기화

Member member = em.getReference(Member.class, “id1”); 
member.getName();

  • getReference()를 호출해서 프록시 객체를 가져온 다음, getName()을 호출한다.
  • getName()이 호출되면 JPA가 영속성 컨텍스트에 초기화 요청을 한다.
  • 영속성 컨텍스트는 DB를 조회한 뒤, 실제 Entity 객체를 생성해서 반환한다.
  • Member 프록시의 Member target에 실제 Entity 객체를 연결한다.
  • target.getName()이 호출되면서 원하는 로직을 실행한다.

📌 프록시 특징 정리

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 ❌! → 프록시 객체를 통해서 실제 엔티티에 접근할 수 있게 되는 것!
  • 프록시 객체는 원본 엔티티를 상속 받은 객체이므로, 타입 체크시 주의해야 한다!
    • == 비교 실패, 대신 instance of 사용
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생한다!
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다.

4. 프록시 확인

1) 프록시 인스턴스의 초기화 여부 확인

PersistenceUnitUtil.isLoaded(Object entity)
  • 초기화되지 않은 프록시 인스턴스는 false 반환

2) 프록시 클래스 확인 방법

entity.getClass().getName()

3) 프록시 강제 초기화

org.hibernate.Hibernate.initialize(entity);

📌 참고

  • JPA 표준은 강제 초기화 없음
  • 강제 호출: member.getName()

[ 즉시 로딩과 지연 로딩 ]

1. 지연 로딩

지연 로딩 LAZY를 사용해서 프록시로 조회

@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;
    
    ...
 }
  • @ManyToOne(fetch = FetchType.LAZY)
    • 지연 로딩 전략을 따른다.
    • Team team 객체는 프록시 객체로 조회한다.

내부 동작 방식

  • member1 로딩 시 연관된 Team 엔티티는 DB에서 조회해오지 않고, 프록시로 초기화해준다.

  • Member 조회 시 Team 객체는 프록시 객체로 초기화된다.

  • 프록시 객체인 team의 데이터를 참조하는 시점에 프록시 객체 team은 실제 엔티티를 가르키도록 초기화한다.

2. 즉시 로딩

Member와 Team을 자주 함께 사용한다면 함께 조회하는 것이 좋다.

즉시 로딩 EAGER를 사용해서 함께 조회

@Entity
public class Member {
    ...
    
    @ManyToOne(fetch = FetchType.EAGER
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    ...
 }
  • Member 객체를 조회할 때, Team 객체 정보까지 한번에 조회한다.
  • 즉시 로딩이므로 Team 객체는 프록시가 아닌 실제 엔티티 객체이다!

내부 동작 방식

  • member1 로딩 시 team1까지 조인해서 조회한다.

  • JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.

3. 프록시와 즉시로딩 주의점

가급적 지연 로딩만 사용해야 한다! (특히 실무에서)

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
    • 하나의 엔티티에 연관된 엔티티가 다수라면 find() 한번 수행하면 다수의 테이블이 조인된다 → 성능 저하😣
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 (EAGER)이므로 LAZY로 설정해야 한다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩 (LAZY)이다.

4. 지연 로딩 활용

✋🏻 잠깐!!
이론적인 활용이기 때문에 실무에서 이런식으로 하면 큰일난다! (실무에서는 무조건 지연 로딩만!)

  • Member와 Team은 자주 함께 사용 → 즉시 로딩
  • Member와 Order는 가끔 사용 → 지연 로딩
  • Order와 Product는 자주 함께 사용 → 즉시 로딩

member1 조회 시,

  • team은 조인하고
  • orders는 (LAZY가 걸려있으므로) 프록시 객체로 초기화한다.

  • 프록시 객체인 orders의 데이터를 참조하는 시점에 실제 엔티티로 가리키도록 초기화된다.

⭐ 실무에서

  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.
profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글