본문 바로가기

개발공부/DATABASE

[JPA] 프록시와 지연로딩/즉시로딩의 관계

💡 코드가 보이지 않으시다면 드래그 혹은 오른쪽 아래 🌜 아이콘을 눌러 테마 색을 변경해주세요.

 

 

안녕하세요!

키크니 개발자 입니다. 🦒

 

 

Team과 Member가 1 : N 으로 매핑된다고 가정합니다.

이 때 Member를 조회할 때 Team도 함께 조회해야 할까요?

 

이는 비즈니스 상황에 따라 다르게 해야합니다.

 

비즈니스 로직에서 Team을 필요로 하지 않을 때에는 

꼭 Member를 조회할 때 Team까지 조회할필요가 없습니다. 

왜냐하면 성능저하(낭비)가 발생할 수 있기 때문입니다.

 

JPA는 이러한 낭비를 하지 않기 위해 지연로딩과 프록시라는 개념으로 해결합니다.

프록시(Proxy)란?

간단하게 말해서 가짜 객체를 의미합니다.

 

EntityManager에는 em.find()em.getReference()가 있습니다.

  • em.find() : 데이터베이스를 통해 실제 엔티티를 조회합니다.
  • em.getReference() : 데이터베이스 조회를 미루고 가짜 엔티티 객체(프록시)를 조회합니다.
    즉, em.getReference()는 DB에 쿼리가 날아가지 않는데도 객체가 조회되고
    이 객체를 사용하는 순간 DB에 쿼리를 날려 진짜 객체를 가져옵니다.

 

프록시 특징

  • 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같습니다. (getId(), getName())
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됩니다.

  • 프록시 객체는 실제 객체의 참조(target = Entity)를 보관합니다.
  • 처음에 호출할 때에는 조회한 적이 없기 때문에 target이 없습니다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출합니다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화 합니다.
  • 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며
    초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능합니다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환합니다
    → 프록시로 먼저 조회하면 find로 조회해도 프록시로 반환합니다.

 

  • 프록시 객체는 원본 엔티티를 상속받으며 따라서 타입 체크시 주의해야합니다.
    (== 비교 실패, 대신 instance of 사용)
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

em.flush(); // DB 반영
em.clear(); // 영속성 컨텍스트를 완전히 초기화

Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());


Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());

System.out.println("a == a : " + (m1 == reference));

- System.out.println("a == a : " + (m1 == reference)); 에서는 항상 true가 나와야 합니다. (1차캐시로 인해서 조회된 것으로 m1, reference를 조회하고 있기 때문입니다.)

- 영속성컨텍스트에 있으면 프록시가 아니라 실제 entity를 반환하게 됩니다.

- transaction안에서의 동일성을 일치하게 해줍니다.

- 프록시도 true를 유지합니다.

 

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생합니다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

em.flush(); // DB 반영
em.clear(); // 영속성 컨텍스트를 완전히 초기화

// proxy
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());

em.detach(refMember);

refMember.getUsername();
System.out.println("refMember = " + refMember.getUsername());
  • em.detach() / em.close() / em.clear() 후 refMember.getUsername()을 하게 되면 영속성 컨텍스트에 있는 것이 관리 안되기 때문에 could not initialize proxy exception, no session 발생합니다. (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
    → 영속성 컨텍스트가 없다는 것을 알 수 있습니다.

 

프록시 객체의 초기화

프록시 객체의 초기화란?

DB에 직접 조회해서 값을 가져와 entity를 만들어 내는 과정을 의미합니다.

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

(1) em.getReference()로 가짜(프록시) 객체를 조회합니다.

(2) 영속성 컨텍스트에 초기화를 요청합니다.

그리고 영속성 컨텍스트는 실제 DB에서 값을 조회해서 실제 Entity를 생성한 후 MemberProxy의 target(Member)와 연결해줍니다.

이 후 member의 getName을 하게되면 기존에 조회된 것이 있기 때문에 target에서 name을 조회합니다.

 

지연로딩과 즉시로딩

지연로딩

@Entity
public class Member {

@Id
@GeneratedValue
private Long id;

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

@ManyToOne(fetch = FetchType.LAZY) // (1)
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}

(1) proxy 객체로 조회합니다. (지연로딩을 의미)

  • Member table만 DB에서 조회하고, team은 proxy로 조회합니다.
  • memeber.getTeam()으로 했을 때 실제 query로 조회됩니다.
Team team1 = new Team();
team1.setName(team1);
em.persist(team1);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

Member m = em.find(Member.class, member1.getId());
System.out.println("m = "+ m.getTeam().getName()); // (1)

  • m에 있는 team을 가져올 때 프록시를 가져온다. (직접적으로 team을 사용하지 않았을 때)

(1) team 내의 어떤 속성을 조회하게 되면 이 시점에서 DB에서 쿼리로 조회한다. (team의 field를 사용할 때 실제 쿼리문이 나간다.)

 

즉시로딩

@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;
	
    ..
}
Team team1 = new Team();
team1.setName(team1);
em.persist(team1);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

Member m = em.find(Member.class, member1.getId());
System.out.println("m = "+ m.getTeam().getName()); // (1)

(1) 이 시점에서 team에 대한 조회 쿼리가 나가는 것이 아니라, member를 조회할 때 team도 같이 조회합니다.

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용해야합니다.(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
    → 2개일 때에는 상관없지만 만약 10개 테이블이 연관되어있으면 10개 테이블을 다 조회해오기 때문에 성능저하가 발생할 수 있습니다.
  • 즉시 로딩은 JPQL에서 N+1 문제가 발생합니다.

N + 1

Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("teamA");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member1.setTeam(teamB);
em.persist(member2);
List<Member> members = em.createQuery("select m from Member m", Member.class)
						.getResultList();

💡위의 코드는 N + 1 발생합니다. (DB에 저장된 row 수 = N , 처음 조회하는 첫 query = 1)

  • 만약 em.find() 로 조회하게 되면 PK를 기준으로 JPA에서 조회하므로 내부적으로 한 번의 쿼리를 날려 실행합니다.
  • 하지만 JPQL은 코드에 적은 쿼리가 그대로 DB에서 조회되기 때문에 쿼리 그대로 Member만 가져오고, Team은 가져오지 않을 것입니다.
  • Team 이 즉시로딩(FetchType.Eager)이라서 가져올 때 무조건 값이 다 채워져 있어야 하기 때문에 member 하나를 조회할 때마다 그 member에 해당하는 Team을 조회하게 됩니다.
    (하지만 member에 대해 어떤 team이 매핑되는지 모르기 때문에 team 모두를 조회하게 됩니다.)
  • member1 -> teamA, teamB 조회, member2 -> teamA, teamB 조회 
    즉, 2개의 member(member1, member2)를 조회했지만, team을 찾는 쿼리는 4개가 나가게 됩니다.

해결방안

1. jpql에서 fetchJoin을 사용한다. → 동적으로 내가 원하는 애들을 선택할 수 있음 → join query를 날립니다.

em.createQuery("select m from Member m join fetch m.team", Member.class);

2. @EntityGraph를 사용합니다.

3. batchSize를 설정합니다. → query문이 한번 더 나감 N + 1 → 1 + 1로 가능

 

지연로딩 활용(실무)

  • ⭐️ 모든 연관관계에 지연 로딩을 사용해야 한다! (@ManyToOne에서 FetchType.LAZY)
    @OneToMany는 Default가 FetchType.LAZY이다.
  • 실무에서 즉시 로딩(FetchType.EAGER)을 사용하면 안된다!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해야 한다!
  • 즉시 로딩은 상상하지 못한 쿼리가 나가기 때문에 사용하지 않아야 된다!

 

⭐️  참고한 곳  


자바 ORM 표준 JPA 프로그래밍 - 기본편

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

https://derekpark.tistory.com/92

https://ict-nroo.tistory.com/131

https://velog.io/@sa1341/JPA-Proxy

https://eocoding.tistory.com/31

 

 

배워야 할 것이 더 많은 주니어 개발자입니다. 🐣
내용 전달보다는 정리를 목적으로 포스팅을 하고 있습니다.
잘못 된 내용이나 부족한 부분은 댓글로 주시면 감사드리겠습니다. 
반응형