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

·2024년 4월 15일
0

JPA

목록 보기
7/17
post-thumbnail

Team과 Member가 1:N으로 매핑된다고 가정해보자.
이때 Member를 조회할 때 Team도 함께 조회해야할까?
비즈니스 로직에서 Team을 필요로 하지 않을 때에는 꼭 TEAM도 같이 조회할 필요가 없다.
➡️ JPA는 이런 낭비(성능저하)를 하지 않기 위해 지연로딩프록시라는 개념으로 해결한다.

💡프록시(Proxy)

간단히 말해서 가짜 객체를 의미한다.

📗프록시 기초

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

📗프록시 객체의 초기화

DB에 직접 조회해서 값을 가져와 entity를 만들어 내는 과정

(1) Member member = em.getReference(...); -> 프록시 객체 호출
(2) member.getName(); -> 영속성 컨텍스트에 초기화 요청
(3) 영속성 컨텍스트는 DB조회해서 실제 엔티티 객체 생성
(4) target에 실제 엔티티 객체를 연결시켜준다
➡️ 즉, target의 getName()을 통해서 멤버의 getName()이 반환됨
(5) 이 후, member의 getName()을 하게되면 기존에 조회된 것이 있으므로 target에서 name을 조회한다.

📗프록시 특징

  • 실제 클래스를 상속받아서 만들어진다
  • 실제 클래스와 겉모양이 같다
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
  • 프록시 객체는 처음 사용할 때 한번만 초기화된다
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다.
  • 초기화가 되면 프록시 객체를 통해 실제 엔티티에 접근 가능한 것
  • 프록시 객체는 원본 엔티티를 상속 받는다.
    • 따라서 타입 체크 시 주의해야 함 ( == 비교 실패. 대신 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일때, 프록시를 초기화하면 문제가 발생한다.

📗프록시 확인

  • 프록시 인스턴시의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    • entity.getClass().getName 출력
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initalize(entity);

📌참고
JPA 표준은 강제 초기화 없다
강제 호출 : member.getName()

💡지연로딩과 즉시로딩

📗지연로딩(LAZY)

필요한 시점에 연관된 객체의 데이터를 불러오는 것
➡️ (fetch = FetchType.LAZY)

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne(fetch = FetchType.LAZY)  //패치타입 LAZY : 프록시로 가져옴
    @JoinColumn(name ="TEAM_ID")
    private Team team;
	...
}
  • 프록시 객체로 조회한다
    ➡️지연로딩을 의미
  • Member만 DB에서 조회하고 team은 프록시로 조회
Team team1 = new Team();
team1.setName(team1);
em.persist(team1);

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

Member m =em.find(Member.class, member.getId());
//team의 속성 조회
System.out.println("m = " + m.getTeam().getName());
  • m에 있는 team을 가져올 때 프록시를 가져온다
    ➡️직접적으로 team을 사용하지 않았을 때
  • team내에 속성을 조회하게 되면 이 시점에서 DB에서 쿼리로 조회한다.
    ➡️team의 필드를 사용할때 실제 쿼리문이 나간다.

📗즉시로딩(EAGER)

데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한 번에 불러오는 것
➡️ (fetch = FetchType.EAGER)

@Entity
public class Member extends BaseEntity{
    @Id @GeneratedValue

    private Long id;

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

	//패치타입 EAGER : 프록시X, 바로 가져옴
    @ManyToOne(fetch = FetchType.EAGER) 
    @JoinColumn(name ="TEAM_ID")
    private Team team;
Team team1 = new Team();
team1.setName(team1);
em.persist(team1);

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

Member m =em.find(Member.class, member.getId());
//team의 속성 조회
System.out.println("m = " + m.getTeam().getName());
  • team의 속성 조회(m.getTeam().getName())시에 team에 대한 조회 쿼리가 나가는 게 아니라 member를 조회할 때 team도 한꺼번에 같이 조회한다.

📗프록시와 즉시로딩 주의

  • 가급적 지연로딩만 사용(특히나 실무에서!)
  • 즉시로딩을 적용하면 예상치 못한 SQL이 발생한다.
  • 즉시로딩은 JPQL에서 N+1 문제를 일으킨다.
    • 해결방안1 : JPQL에서 fetchJoin을 사용한다.
    • 해결방안2 : @EntityGraph를 사용한다.
    • 해결방안3 : batchSize를 설정한다.
  • @ManyToOne, @OneToOne은 기본이 즉시로딩
    -> LAZY로 설정
  • @OneToMaany, @ManyToMany는 기본이 지연로딩

📗지연로딩 활용(실무)

  • 모든 연관관계에 지연로딩을 사용해라
  • 실무에서 즉시로딩을 사용하지 마라
  • JPQL fetch 조인이나 엔티티 그래프 기능을 사용해라
  • 즉시로딩은 상상치 못한 쿼리가 나가기 때문에 사용하지 않아야 한다!
profile
백엔드 개발자를 꿈꿉니다 / 이전 블로그 : https://po-dadak.tistory.com/category

0개의 댓글

관련 채용 정보