Home JPA 프록시 그리고 지연 로딩
Post
Cancel

JPA 프록시 그리고 지연 로딩

Member와 Team의 관계가 있을 때, Member를 조회하면 Team까지 함께 조회해야 할까?

1
2
3
4
5
6
public void printUserAndTeam(String memberId) {
	Member member = em.find(Member.class, memberId);
	Team team = member.getTeam();

	System.out.pritnln(member + " " + team);
}
1
2
3
4
5
public void printUser(String memberId) {
	Member member = em.find(Member.class, memberId);
	
	System.out.pritnln(member);
}

이 두 경우를 보자. 멤버와 팀을 전부 출력해야 하는 경우 em.find()를 할 때 member와 같이 team 정보까지 같이 가져오면 좋다.

하지만 멤버만 출력해야 하는 상황에서도 team까지 가져온다면?

=> 낭비이다.

JPA는 이러한 부분을 프록시, 지연 로딩과 같은 개념을 통해 해결을 한다.

프록시

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

em.find()는 em.find()가 실행되는 시점에 조회 쿼리가 발생한다. 하지만 아래 코드를 보자.

1
2
3
4
5
6
7
8
Member findMember = em.getReference(Member.class, member.getId());
//이 시점에는 em.find()와 다르게 쿼리가 발생하지 않음.

System.out.println(findMember.getId());
//이 시점에서도 id값은 이미 알고 있기 때문에 굳이 DB에 접근할 필요 없으므로 조회쿼리 발생 X

System.out.println(findMember.getUsername());
//getReference()를 사용한 객체를 실제로 DB에서 조회할 때 쿼리가 발생.

getReference()를 호출하는 시점에는 실제 엔티티 객체가 아닌 가짜 엔티티 객체가 반환된다.

Entity target이 실제 레퍼런스가 들어갈 부분이고, 위의 코드로 보면 우선은 id만을 가지고 있는 가짜가 반환되는 것이다.

프록시 기초

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

프록시 객체는 실제 객체의 참조(target)을 보관한다. 프록시 객체를 호출하게 되면 프록시 객체는 실제 객체의 메서드를 호출하는 방식이다.

프록시 객체의 초기화 과정

1
2
Member member = em.getReference(Member.class, "1L");
member.getName();

  • getReference()의 member는 프록시 객체이다. (가짜 객체이다.)
  • 이 프록시 객체에서 getName()을 호출하는 시점을 보면 프록시 객체의 target에 값이 없다.
    • 이러면 JPA가 영속성 컨텍스트에 초기화 요청을 한다. (실제 값을 달라는 느낌의 요청)
    • 요청을 받은 영속성 컨텍스트가 DB에서 객체를 조회해 실제 Member Entity를 생성한다.
    • 이렇게 생성된 실제 객체를 프록시 객체의 target에 매칭해준다.
  • 실제 객체가 매칭되었기 때문에 프록시 객체의 getName()이 호출되면 프록시 객체에서 실제 객체의 getName()을 호출해 값을 반환하게 된다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
    • 위와 같은 초기화 과정이 한 번 일어난다는 뜻이다.
  • 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능하게 되는 것이다.
  • 프록시 객체는 원본 엔티티를 상속받는 형태이다. 따라서 타입 체크 시 주의해야 한다.
    • == 비교 X, 대신 instance of 사용
    • Member 클래스끼리 비교하는데 어떤 Member는 find(), 어떤 Member는 reference()로 반환했다면 ==을 사용했을 때 true를 기대했는데 false가 나오게 되는 것이다.
      • reference()를 사용한 객체는 프록시 객체이기 때문이다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 존재한다면, em.getReference()를 호출해도 실제 엔티티를 반환한다.
    • 예를 들어 find()로 이미 영속성 컨텍스트에 올려놓아 1차 캐시에서 꺼내쓸 수 있는데 프록시 객체를 반환해 사용할 이유가 없다.
    • 더해 JPA는 기본적으로 PK가 같으면 기본적으로 한 트랜잭션 안에서 동일함을 보장해주는 것이 기본 메커니즘이기 때문에 영속성 컨텍스트에 이미 존재한다면 reference()를 해도 실제 엔티티를 반환하는 것이다.
      • 심지어 이런 메커니즘 때문에 getReference()로 먼저 호출하고, 이후 같은 member를 find()로 호출해도 프록시 객체를 반환한다.
      • 이 정도로 JPA는 동일성을 중요시 생각한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
    • 하이버네이트 기준 org.hibernate.LazyInitializationException 예외 발생

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistanceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인
    • entity.getClass().getName()
  • 프록시 강제 초기화
    • Hibernate.initialize(entity);

참고로 JPA 표준은 강제 초기화가 없다.

참고

실제로 getReference()를 잘 사용하지 않지만, 즉시 로딩과 지연 로딩에 대해 잘 이해하기 위해 이러한 개념을 알아두어야 한다.

지연 로딩 (LAZY)

Member를 사용하기 위해 Member를 조회했는데 연관 관계인 Team까지 한 번에 조회해 낭비가 발생하는 문제.

이러한 문제를 해결하기 위해 JPA가 제공하는 지연 로딩, 즉시 로딩에 관해 설명한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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;

	//...
}

위와 같이 fetch 타입을 지정해주었을 때 아래와 같은 매커니즘으로 동작한다.

  • Member를 조회했을 때 DB에서 member에 대한 정보를 조회한다.
  • 지연 로딩을 사용한 Team은 프록시로 가져오게 된다.
    • Team 객체를 실제 사용하는 시점에 이 Team 프록시 객체를 초기화하고 사용하게 되는 것이다.

즉시 로딩 (EAGER)

그런데 만약 Member와 Team을 자주 함께 사용한다면?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class Member {
	
	@Id @GeneratedValue
	private Long id;

	@Column(name = "USERNAME")
	private String name;

	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "TEAM_ID")
	private Team team;

	//...
}

조회 시 Member와 Team을 Join해 전부 조회한다. 즉 Team 객체도 프록시 객체가 아닌 실제 엔티티 객체를 반환하게 된다.

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

  • 가급적 지연 로딩만 사용한다. (특히 실무)
    • 예제에서는 두 테이블만 있어 조인을 해도 크게 쿼리가 많이 나가지 않지만, 테이블이 5~10개 더 연결되어 조인을 하게 된다면 쿼리의 양만 해도 엄청날 것이다.
  • 즉시 로딩을 적용하면 예상치 못한 SQL이 발생한다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이다.
    • LAZY로 설정해주어야 한다.
This post is licensed under CC BY 4.0 by the author.