본문 바로가기
STUDY/JPA

프록시와 연관관계 관리 #1 프록시와 즉시로딩, 지연로딩

by Anne of Green Galbes 2020. 2. 4.

이 글은 김영한의 자바 ORM 표준 JPA 프로그래밍을 보고 작성한 글임을 알려드립니다.

관련 강의 : 인프런스 자바 ORM 표준 JPA 프로그래밍 - 기본편


1. 프록시

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. 비즈니스 로직에 따라 사용되지 않는 엔티티가 있을 수도 있다. 그럼 필요 없는 엔티티를 그저 연관되어 있다는 이유만으로 함께 조회하는것은 과연 효율적일까? 아니다. 필요 없는 엔티티까지 같이 조회하는건 효율적이지 않다.

JPA에서는 그래서 엔티티가 실제 사용될때까지 데이터베이스에서 조회를 지연하는 방법을 제공하는데, 이를 지연로딩이라고 한다. 이러한 지연로딩이 가능한 이유는 프록시 객체가 있기 때문이다. 프록시 객체는 실제 엔티티인 척하는 가짜 객체를 말한다. 

[ 지연로딩과 프록시 객체 ]
  • 지연로딩
    • 엔티티가 실제 사용될때까지 데이터베이스에서 조회를 지연하는 방법
  • 프록세 객체
    • 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체
    • 지연로딩을 하기 위해 필요하다.

 

1) 프록시 기초

EntityManager.getReference() 메소드를 사용하면 해당 엔티티를 사용하기 전까지 데이터베이스 조회를 최대한 미룰 수 있다. 이 프록시 객체는 실제 객체애 대한 참조(target)을 보관한다. 그리고 프록시 객체의 메소드를 호출하면 실제 객체의 메소드를 호출하게 된다.

[ EntityManager.find() vs EntityManager.getRegerence() ] 
  • EntityManager.find() 메소드
    • 영속성 컨텍스트 조회 후, 데이터가 엔티티가 없으면 데이터베이스 조회
    • 엔티티 실제 사용 여부와 상관없이 항상 데이터베이스를 조회
  • EntityManager.getReference() 메소드리스트
    • 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미룬다.
    • 데이터베이스 접근을 위임한 프록시 객체를 반환

프록시 객체의 초기화

실제 사용될 때 데이터베이스를 조회하여 실제 엔티티 객체를 생성하는 작업을 프록시 객체의 초기화라 한다.  다음은 프록시의 초기화 과정이다.

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

프록시의 특징

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

 

2) 프록시와 식별자

다음과 같은 경우 프록시 객체의 초기화가 진행되지 않는다.

Team team = em.gerReference(Team.class, "team1");
team.getId();  //초기화되지 않음

프록시 객체가 생성되면서 식별자 값을 이미 들고있기 때문이다. 단, 엔티티 접근 방식이 프로퍼티(@Access(AccessType.PROPERTY)일 경우에만 초기화 하지 않고, 엔티티 접근 방식이 필드(@Access(AccessType.FIELD)일 경우에는 초기화 시킬 수 있다.

 

3) 프록시 확인

메소드 기능
PersistenceUnitUtil.Loaded(Object entity) 프록시 인스턴스의 초기화 여부를 확인
entity.getClass().getName()

프록시 클래스 확인 

결과에 ..javasist.. 혹은 HibernateProxy가 출력되면 프록시이다.

initialize()entity)

프록시를 강제 초기화

Hibernate에서 지원하는 기능이다.

JPA에서는 그냥 프록시 객체에서 member.getName()처럼 프록시의 메소드를 직접 호출하면 된다.

 

2. 즉시로딩과 지연로딩

JPA에서는 연관된 엔티티의 조회 시점을 선택할 수 있도록 다음 두 가지 발법을 제공하고 있다.

  • 즉시로딩
    • 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
    • @ManyToOne( fetch = FetchType.EAGER )
  • 지연로딩
    • 연관된 엔티티를 실제 사용할 때 조회횐다.
    • @ManyToOne( fetch = FetchType.LAZY )

 

1) 즉시로딩

즉시로딩을 사용해서 엔티티를 조회해보자.

//즉시로딩 설정
@Entity
public class Member {
    // ··· 생략
    @ManyToOne( fetch = FetchType.EAGER )
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // ··· 생략
}
//즉시로딩 실행 코드
Member memebr = em.find(Member.class, "memeber1");
Team team = member.getTeam();  //객체 그래프 탐색

그럼 이제 즉시 로딩 실행 쿼리를 보자. JPA는 즉시로딩을 최적화하기 위해 조인 쿼리를 많이 사용한다.

SELECT
    M.MEMBER_ID AS MEMBER_ID,
    M.TEAM_ID AS TEAM_ID,
    M.USERNAME AS USERNAME,
    T.TEAM_ID AS TEAM_ID,
    T.TEAM_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 조인 전략

여기서 외부 조인을 사용한 이유는 무엇일까?

회원 테이블의 TEAM_ID 외래키는 NULL 값을 허용한다. NULL 값을 허용한다는 말은 팀에 소속되지 않는 멤버가 있을 수 있다는 말이다. 이 경우 팀에 소속되지 않는 멤버와 팀을 내부 조인으로 조회할 경우 팀은 물론 회원도 조회할 수 없는 경우가 발생한다. JPA는 이러한 경우도 생각을 해서 외부 조인으로 회원과 팀을 조회한 것이다.

하지만 외부 조인보다 내부 조인이 성능과 최적화 면에서 더 유리하다. 그래서 외부키에 NULL 값을 허용하지 않는 경우라면 JPA가 내부 조인을 사용하도록 하는 것이 성능 측면에서 더 좋다. 그럼 JPA가 내부 조인을 사용하게 하기 위해서는 어떻게 해야할까?

//즉시로딩 설정
@Entity
public class Member {
    // ··· 생략
    @ManyToOne( fetch = FetchType.EAGER )
    @JoinColumn(name = "TEAM_ID", nullable = false)
    private Team team;
    
    // ··· 생략
}

위와 같이 @JoinColumn에서 nullable 속성을 이용하면 된다. nullable 속성을 false로 설정을 하면 외래 키는 NULL 값을 허용하지 않고, JPA는 외부 조인 대신에 내부 조인을 사용한다.

//즉시로딩 설정
@Entity
public class Member {
    // ··· 생략
    @ManyToOne( fetch = FetchType.EAGER, optional = false )
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // ··· 생략
}

아니면 @ManyToOne.optional = false로 설정해도 내부 조인을 사용한다.

 

2) 지연로딩

지연로딩을 사용해서 엔티티를 조회해보자.

//지연로딩 설정
@Entity
public class Member {
    // ··· 생략
    @ManyToOne( fetch = FetchType.LAZY )
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // ··· 생략
}
//지연로딩 실행 코드
Member memebr = em.find(Member.class, "memeber1");
Team team = member.getTeam();  //객체 그래프 탐색
team.getName();  //팀 객체 실제 사용

em.find(Member.class, "member1")를 호출하면 회원만 조회되고 팀은 조회되지 않는다. 대신에 조회한 회원의 팀 멤버변수에 프록시 객체를 넣어둦다. 반환된 팀 객체는 프록시 객체로 실제로 팀 객체를 사용하기 전까지는 데이터 로딩을 미룬다.

조회 대상이 영속성 컨텍스트에 있으면 프록시 객체를 사용할 이유가 없다. 따라서 프록시가 아닌 실제 객체를 사용한다.

 

3) 지연로딩 활용

프록시와 컬렉션 래퍼

컬랙션 래퍼틑 하이버네이트가 엔티티 내에 컬렉션이 있으면 컬렉션을 추적, 관리할 목적으로 하이버네이트의 내장 컬렉션으로 변경하는것을 말한다. 컬렉션은 get() 메소드처럼 실제 컬렉션의 데이터를 조회할 때 데이터베이스를 조회해서 초기화한다.

 

JPA 기본 패치 전략

JPA의 기본 패피 전략은 연관된 엔티티가 하나면 즉시로딩을, 컬렉션이면 지연로딩을 사용하는 것이다. 컬렉션을 로딩하는건 많은 비용이 들기 때문이다. 저자가 추천하는 방법은 우선 모든 연관관게에 지연로딩을 사용하고 개발이 어느 정도 완료된 단계에서 상황을 보면서 꼭 필요한 곳에만 즉시로딩으로 바구는 것이다.

 

컬렉션에서 FetchType.EAGER 사용시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권하지 않는다.
    • 조인은 각 테이블의 칼럼수를 곱한 수만큼 데이터를 반환하기 때문에 다수의 컬렉션을 사용하면 즉시로딩을 피하는 것이 좋다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
    • 다대일 관게인 회원, 팀 테이블에서 회원 테이블의 외래 키에 not null 제약조건을 걸어두면 내부조인을 사용해도 된다.
    • 팀 테이블에서 회원을 일대다 관계로 조인할 때 회원이 한 명도 없는 팀을 내부조인하면 팀까지 조회되지 않는 문제가 발생하므로 JPA에서는 일대다 관계에서 즉시로딩을 할 때 항상 외부조인을 사용한다.
[ FatchType.EAGER 설정과 조인 전략 ]
  • @ManyToOne, @OneToOne
    • optional = false : 내부조인
    • optional = true : 외부조인
  • @OneToMany, @ManyToMany
    • optional = false : 외부조인
    • optional = true : 내부조인

댓글