쌓고 쌓다

[JPA] 프록시와 즉시로딩, 지연로딩 본문

프로그래밍/JPA

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

승민아 2023. 8. 16. 17:29

JPA는 프록시라는 기술을 사용한다.

프록시를 사용하면 연관된 객체를 처음부터 DB에서 조회하는것이 아니라, 실제 사용 시점에서 DB 조회를 한다.

( 자주 함께 사용하는 객체라면 두번 조회하는것보다 첨부터 조인 쿼리를 날리는게 효과적이다. )

 

먼저. 회원, 팀 엔티티, 테이블 상황을 보자.

Member

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    private Long id;
    private String username;

    @ManyToOne
    @JoinColumn(name="team_id")
    private Team team;

}

 

Team

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {

    @Id
    private Long id;
    private String teamname;
}

 

MEMBER, TEAM 테이블

현재 팀 이름 "team1"에 멤버 "LSM"이 속해있는 상황이다.

 

프록시 이해

public void findUser() {
    Member member = em.find(Member.class, 1L);
    System.out.println("member = " + member.getUsername());
}

회원 엔티티 하나를 조회하는 함수이다. find 메서드는 영속성 컨텍스트에서 찾아보고 없으면 DB를 조회한다.

여기서 Member 엔티티만 사용하는데 연관된 Team 엔티티까지 조회하는 것은 효율적이지 않다.

사용하는 시점까지 DB의 조회를 미루는 방법을 지연로딩이라고한다.

이때 지연 로딩을 사용하기위해 실제 엔티티 대신에 DB 조회를 지연할 가짜 객체가 필요한데 이 객체가 프록시 객체라고한다.

 

프록시 기초

EntityManager.getReference() 메서드를 사용하면 엔티티를 실제 사용하는 시점까지 DB 조회를 미룰 수 있다.

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

 

이 메서드는 실제 객체를 반환하지 않고 프록시 객체를 반환한다.

출처: 김영한

 

프록시 객체 구조

출처: 김영한

프록시 객체는 실제 클래스를 상속 받아 만들어지므로 우리는 실제 객체인지 가짜 객체인지 구분하지 않고 쓸 수 있다.

이 프록시 객체는 실제 객체에 대한 참조를 가지며 프록시 객체의 메서드를 사용하면 실제 객체의 메서드를 호출하는 구조이다.

 

그러면 프록시 객체도 어차피 DB에 조회하여 실제 객체에 대한 참조를 가지고 있어야하는거 아닐까?

아니다.

 

프록시 객체의 초기화

출처: 김영한

1번 과정을 보자.

getName() 메서드를 호출하는 실제 사용될 타이밍에 실제 엔티티 객체를 생성하여 참조를 보관하는 과정을 갖는다.

초기화 과정에서 영속성 컨텍스트의 도움을 받기에 영속성 컨텍스트가 종료(em.close())된 상태에서는 사용할 수 없다.

 

즉시 로딩

엔티티를 조회할때 연관된 엔티티도 함께 조회한다.

@ManyToOne에 fetch 속성 값을 FetchType.EAGER로 설정하자.

@Entity
public class Member {

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

}

그러면 Member 엔티티를 조회해도 조인 쿼리 하나로 두 엔티티를 조회한다.

이때 회원을 조회하는 순간 팀도 조회해야하기 때문에 회원, 팀 조회 쿼리를 각각 날려 2번 조회하지 않고

최적화를 위해 JPA는 조인 쿼리를 사용한다.

=> 이때 team 변수에 들어가는 객체는 실제 객체이다.

 

실행 SQL

Hibernate: select m1_0.id,t1_0.id,t1_0.teamname,m1_0.username from member m1_0 left join team t1_0 on t1_0.id=m1_0.team_id where m1_0.id=?

내부 조인(INNER JOIN)이 아닌 외부 조인(LEFT OUTER JOIN)을 사용하고 있는데

현재 team_id 외래키는 NULL 값을 허용하고 있기에 팀이 없는 회원이 있을 수 있다.

그래서 이런 회원까지 조회할 수 있게 외부 조인을 수행하고 있다.

하지만 내부 조인이 성능상 더 좋기에 @JoinColumn(nullable=false)로 설정하면

외래 키는 NULL을 허용하지 않는다고 JPA에 알려 내부 조인을 사용하게 할 수 있다.

 

지연로딩

@Entity
public class Member {
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="team_id")
    private Team team;
}

fetch 속성 값을 FetchType.LAZY로 바꾸면 된다.

=> 이때 team 변수에는 프록시 객체가 들어간다.

 

회원 엔티티를 찾아보면

Member member = em.find(Member.class, 1L);
System.out.println("member = " + member.getUsername());
Hibernate: select m1_0.id,m1_0.team_id,m1_0.username from member m1_0 where m1_0.id=?

이제 회원만 찾고 팀은 조회하지 않는다.

 

아래처럼 실제 Team을 사용할때 DB 조회 쿼리를 날린다.

Team team = member.getTeam();
System.out.println("team = " + team.getTeamname());

// Hibernate: select t1_0.id,t1_0.teamname from team t1_0 where t1_0.id=?

 

+ 항상 프록시 객체를 사용할까?

조회 대상이 영속성 컨텍스트에 존재한다면 DB 조회를 미루는 프록시 객체를 사용할 이유가 없다.

이때는 실제 객체를 사용한다.

 

Comments