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

즉시로딩과 지연로딩 그리고 프록시

회원 엔티티와 팀 엔티티

회원 엔티티와 팀 엔티티가 있다고 가정해보자. 둘은 다음과 같은 관계가 있다.

  • 회원은 하나의 팀에 소속될 수 있다.
  • 하나의 팀은 다수의 회원을 포함하고 있다.

이 경우 회원을 조회한다고 했을 때 팀 엔티티도 함께 조회하는 것이 좋을까? 아니면 회원 엔티티만을 조회하는 것이 좋을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Getter
public class Member {
    @Id
    private Long memberId;

    private String memberName;

    @ManyToOne
    private Team team;
}

@Entity
@Getter
public class Team {
    @Id
    private Long teamId;

    private String name;
}

팀과 회원 엔티티를 작성하였다. 그리고 아래에서 회원과 팀 정보를 출력하는 코드를 작성할 것이다.

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

    System.out.println("회원 이름 : " + member.getMemberName());
    System.out.println("팀 이름 : " + team.getName());
}

코드를 살펴보면 영속성컨텍스트에서 회원을 가져온 뒤 회원을 통해 팀을 가져왔다. 이 때는 회원과 팀의 정보가 모두 필요하므로 데이터베이스에서 둘을 함께 조회해야한다. 반면에 아래와 같은 경우는 어떨까?

1
2
3
4
public void printMember(String memberId) {
    Member member = em.find(Member.class, memberId)
    System.out.println("회원 이름 : " + member.getMemberName());
}

이 경우는 회원 엔티티만을 조회하여 회원의 정보만 출력하였다. 이 경우는 팀 엔티티는 전혀 사용하지 않았다. 그러므로 팀 엔티티까지 데이터베이스에서 조회하는 것은 효율적이지 않다. 이때는 팀 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는게 효율적이다.

위와 같이 상황에 따라서 둘을 함께 조회할 수도 있고 아닐 수도 있다. 여기서 중요한점은 엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니라는 점이다.

JPA에는 이러한 상황에서 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 아래의 두가지 방법을 제공한다.

  • 즉시로딩(EAGER) : 엔티티를 조회할 때 연관된 엔티티도 함께 조회
  • 지연로딩(LAZY) : 연관된 엔티티를 실제 사용 시점에 조회

즉시로딩(EAGER)

즉시로딩은 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것을 말한다. 위의 예제의 경우 회원을 조회할 때 팀도 함께 조회하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Getter
public class Member {
    @Id
    private Long memberId;

    private String memberName;

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

즉시 로딩은 fetchType.EAGER를 통해 설정할 수 있다. 그리고 회원을 조회할 때 팀도 조회 해야하므로 쿼리가 2번 실행될 것 같지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화 하기 위해 가능하면 조인쿼리를 사용한다.

1
2
3
4
5
6
7
8
9
SELECT
    M.MEMBER_ID AS MEMBER_ID,
    M.TEAM_ID AS TEAM_ID,
    M.MEMBER_NAME AS MEMBER_NAME,
    T.TEAM_ID AS TEAM_ID,
    T.NAME AS NAME
FROM MEMBER M
LEFT OUTER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'member1'

Null 제약조건과 JPA 조인 전략

쿼리 결과를 보면 내부 조인이 아닌 외부조인을 사용한 것을 알 수 있다. 일반적으로는 내부조인이 외부조인보다 성능과 최적화에서 더 유리하다.

그렇지만 JPA가 외부조인을 통해 값을 가져온 이유는 현재 회원 테이블에 TEAM_ID 외래 키는 Null을 허용하고 있다. 만약 팀에 소속하지 않은 회원과 팀을 내부 조인하면 팀은 물론이고 회원 데이터도 조회할 수 없으므로 JPA는 이러한 상황을 고려하여 외부조인을 사용하였다.

만약 TEAM_ID 외래 키가 Null을 허용하지 않는 경우라면 팀에 소속하지 않는 회원이 없다는 것을 보장하므로 JPA는 외부조인이 아닌 내부조인을 사용하여 조회한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Getter
public class Member {
    @Id
    private Long memberId;

    private String memberName;

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

    // 또는

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

지연로딩(LAZY)

지연로딩은 연관된 엔티티의 조회를 실제 사용 시점까지 지연 시키는 것을 말한다. 이는 FetchType.LAZY를 통해 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Getter
public class Member {
    @Id
    private Long memberId;

    private String memberName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
1
2
3
Member member = em.find(Member.class, memberId)
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용

지연로딩을 사용하면 em.find(Member.class, memberId)로 회원을 조회할 때 팀은 조회하지 않는다. 대신 team 멤버변수에 실제 팀 엔티티를 대신하여 프록시 객체를 넣어둔다. 그리고 실제 팀의 데이터를 조회하는 team.getName()이 호출되었을 때 프록시 객체를 통해 데이터베이스에서 팀을 조회한다.

프록시

JPA는 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다. 그렇기 때문에 실제 사용시점까지 조회를 지연시키기 위해 team 멤버변수에 팀 엔티티가 아닌 가짜 객체를 넣어둘 필요성이 생긴다. 이것이 위에서 언급한 프록시이다.

프록시의 특징

프록시는 team을 대신하는 가짜 객체이다. 하지만 team 멤버변수에 초기화 되기 위해서는 팀 엔티티와 같은 타입을 가져야만 한다.

그로인해 프록시 클래스는 실제 클래스를 상속 받아서 만들어진다. 따라서 사용자 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용할 수 있다.

또한 프록시 객체는 실제 객체에 대한 참조(target)를 보관하고 있다. 그리고 프록시 객체의 메소드가 호출되면 참조를 통해 실제 객체의 메소드를 호출하게 된다.

프록시 조회

영속성컨텍스트를 통해 프록시를 조회할 수 있는데 이 때는 em.getReference()를 사용하면 된다. 프록시를 조회하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다.

1
2
Member member = em.find(Member.class, "member1"); // 엔티티 조회
Member member = em.getReference(Member.class, "member1"); // 프록시 조회

프록시 객체의 초기화

프록시 객체는 team.getName()처럼 실제 사용되는 시점에 데이터베이스를 조회하여 실제 엔티티 객체를 생성한다. 그리고 이를 프록시 객체의 초기화라 한다.

여기서 주의할점은 프록시 객체를 초기화 한다고 실제 엔티티로 변경되는 것은 아니다. 단지 프록시 객체가 초기화되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있는 것이다.

프록시의 초기화 과정

  1. 프록시 객체에 team.getName()을 호출하여 실제 데이터를 조회한다.
  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
  3. 영속성 컨텍스트는 데이터베이스를 조회하여 실제 엔티티 객체를 생성한다.
  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 멤버변수에 보관한다.
  5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출하여 결과를 반환한다.

프록시 정리

  • 프록시 객체는 처음 사용할 때 한번만 초기화 된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티가 반환된다.
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.

참고

This post is licensed under CC BY 4.0 by the author.

@MappedSuperclass

-

Comments powered by Disqus.