JPA 프록시와 즉시로딩, 지연로딩에 대해 알아보자

프록시와 즉시로딩, 지연로딩

프록시

지연로딩에 대해 이야기를 하기위해서는 프록시에 대해 먼저 알아야해!

프록시가 무엇이냐??

쉽게 말하면 가짜 엔티티라고 생각할 수 있어

EntityManger를 통해 프록시 객체를 조회하기 위해서는 em.getReference() 메소드를 이용할 수 있어!! 그러면 프록시를 가져와!!

프록시는 진짜 객체를 참조할 수 있게 끔 하는 참조 변수가 멤버 변수로 가지고 있다고 생각하면 돼!! 그래서 현재는 프록시 즉 가짜 객체로 들고왔으나 이후에 진짜 객체가 필요할 때 초기화 과정을 거친후 참조하는 변수로 실제 객체를 가리키게 한 후 이를 이용해서 접근하는 거지!!


프록시 특징

  • 실제 클래스를 상속받아서 만들어져
  • 실제 클래스와 겉모양이 같아. 상속받아서 만들었으니 당연하겠찌?
  • 사용하는 입장에서는 이론상 구분하지 않고 사용하면 되는거야
  • 위에서도 말했듯이 실제 객체의 참조(target)을 보관하고 있어
  • 그래서 target을 통해 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출해줘.

👏👏👏 참고사항!! 👏👏👏
JPA에서는 한 트랜젝션(JPA는 작업이 트랜젝션 단위자나 그렇기에 영속성 컨텍스트란 개념을 쓸 수 있었던 것이고)내에서 같은 놈에 대해서는 동일성을 보장해줘!!

이게 무슨 말이냐!? 만약 같은 id 값을 통해 처음에는 프록시를 가져오고(em.getReference()) 그 밑의 코드에서는 em.find()를 호출해서 각각의 결과가 result1, result2 라면 이 둘에 대해서는 result1 == result2 의 결과는 true를 보장해줘야 한다는거야!!

결과적으로는 result2 놈도 프록시로 가져오게 돼. 신기하지


프록시 객체의 초기화

  1. 프록시 객체에서 getName() 메소드를 호출하게 되면
  2. 프록시 객체의 처음에는 target에 해당하는 값이 존재하지 않아.
  3. JPA가 영속성 컨텍스트를 통해 실제 객체의 초기화를 요청해
  4. 그럼 Database를 조회해서 실제 Entity를 만들어줘
  5. 프록시 객체는 target이 생기게 되고 이 target을 이용한 호출이 결국에는 실제 Entity 의 getName() 메소드를 호출하는 게 되는거야.
  6. 프록시 객체에 target이 할당되면 이후에는 위와같은 초기화 작업은 필요없어.

프록시 주의할 것

  • 프록시 객체는 처음 사용할 때 한 번만 초기화 해
  • 참고로 초기화한다는 것이 프록시 객체가 실제 엔티티로 바뀌는 것이 아냐!! 프록시 객체를 통해서 (정확히는 target) 실제 엔티티에 접근이 가능하게 되는거지!!
  • 프록시 객체는 원본 엔티티를 상속받아서 만들어진다고 했자나!! 그래서 원본 엔티티와 프록시의 == 연산은 기대한 결과가 안나올 수 있어. 그러니 타입 체크시에 instance of 사용하자
  • 참고로 영속성 컨텍스트에 이미 프록시로 가져오고자하는 원본타입의 엔티티가 관리되어지고 있다면 이때는 실제 엔티티를 바로 반환해
  • 초기화 할 때 영속성 컨텍스트를 통해 요청을 하게된다고 하였는데 당연히 영속성 컨텍스트의 도움을 받을 수 없는 상황 ( clear한다거나.. close 한다거나..)이면 문제가 발생해
  • PersistenceUnitUtil.isLoaded(Object entity) 를 통해 프록시 인스턴스의 초기화 여부 확인 가능해
  • org.hibernate.Hibernate.initialize(entity)를 통해 프록시 강제 초기화를 할 수 있어.



즉시로딩과 지연로딩

즉시로딩

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER) // 이렇게 fetch=FetchType.EAGER를 통해 설정가능
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}

즉시로딩을 설정하는 방법은 위의 코드와 같아.

FetchType.EAGER를 통해 설정할 수 있어.

만약 Member와 Team이 위처럼 연관관계가 있다고 생각해보자. (Team은 코드를 안써놨으나 Team 쪽에서는 Member에 대해 @OneToMany 어노테이션을 통해 양방향관계라고 생각해보자)

이때 Member 엔티티를 조회할 때 해당 엔티티와 연관관계에 있는 Team 도 같이 바로 가져오는 게 즉시로딩이야!!


지연로딩

중요한건 지연로딩이야. 사실 즉시로딩은 실무에서 사용을 지양한다고 하넹

why? 🧐🧐🧐

n+1이나 예상치 못한 형태의 쿼리 등 문제의 소지가 많데!!

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY) //이렇게 FetchType.LAZY를 통해 설정가능해
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}

그럼 지연로딩은 무엇이냐??

아까 위 즉시로딩은 설명할 때 Member 엔티티를 조회하면서 해당 엔티티와 관련된 Team도 같이 들고온다고 했는데 지연로딩은 그렇지 않아!!

Member 엔티티만 조회해서 들구오는거야!!

그럼 해당 Member 엔티티와 연관관계에 있는 Team에 대해서는 어떻게 처리하는지가 궁금하겠지!

여기서 위에서 배웠던 개념이 나오는거야!! 바로 프록시 형태로 가져오는 거지!!

그 이후에는 위에서 설명했던 것 처럼 만약 member1이라는 엔티티를 가져오면서 해당 엔티티와 관계에 있는 team1에 대해서 프록시 처리를 해놓았다면, 이후에 member1.getTeam(),getName() 이런식으로 실제 team에 접근하는 부분을 만날 때 Database를 조회해서 team 을 가져오는거야!!


프록시와 즉시로딩 주의사항

  • 가급적이면 지연 로딩만 실무에서 사용할 것!
  • 즉시로딩을 사용하면 예상치 못한 sql이 발생할 수 있어
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으켜!! (지연로딩이 N+1이 안생긴다 그런말은 아니야)
  • @ManyToOne, @OneToOne은 기본이 즉시로딩이기 때문에 LAZY로 설정해 줘야해!! (FetchType.LAZY 로!!)
  • 그외 @OneToMany 랑 @ManyToMany(쓰지마..) 는 기본이 지연로딩이야







https://www.inflearn.com/course/ORM-JPA-Basic