이해를 돕기 위해, 회원(Member)과 팀(Team)의 다대일 단방향 관계를 예시로 들어보겠습니다.
위 조건에 다대일 단방향 관계를 위한 추가 조건은 아래와 같습니다.
위 관계를 통해 작성된 엔티티 코드는 아래와 같습니다.
Member 클래스 (다대일
에서 다
에 해당합니다.)
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID)
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne // @ManyToOne 의 속성 fetch 의 기본값 은 FetchType.EAGER
@JoinColumn(name = "TEAM_ID")
private Team team;
// Getter, Setter, Constructor...
}
Team 클래스 (다대일
에서 일
에 해당합니다.)
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID)
private Long id;
@Column(name = "NAME")
private String name;
// Getter, Setter, Constructor
}
회원 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만, 그렇지 않을 때도 있습니다.
회원 엔티티를 조회할 때 연관된 팀 엔티티가 사용될 때를 살펴보겠습니다.
public void printUserAndTeam() {
Member member = entityManager.find(Member.class, 0L);
Team team = member.getTeam();
System.out.println("회원 이름 : " + member.getUsername());
System.out.println("팀 이름 : " + team.getName());
}
위 코드는 회원 엔티티를 찾음과 동시에 회원과 연관된 팀 엔티티의 이름을 찾아서 출력합니다.
JPA 는 이와 같이 자주 함께 사용하는 객체들의 경우에는 조인을 사용하여 즉시 함께 조회하는 즉시 로딩을 지원합니다.
회원 엔티티를 조회할 때 연관된 팀 엔티티가 사용되지 않을 때를 살펴보겠습니다.
public void printUser() {
Member member = entityManager.find(Member.class, 0L);
System.out.println("회원 이름 : " + member.getUsername());
}
위 코드는 회원 엔티티만 출력합니다.
즉, entityManager.find() 로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티까지 데이터베이스에서 함께 조회해 두는 것 은 효율적이지 않습니다.
실제 위 코드를 실행하였을 때, 실행되는 SQL 문은 아래와 같습니다.
SELECT m.MEMBER_ID, m.TEAM_ID, m.USERNAME, t.TEAM_ID, t.NAME
FROM
Member m
LEFT OUTER JOIN
Team t
ON m.TEAM_ID = t.TEAM_ID
WHERE m.MEMBER_ID = 0;
Proxy 패턴이란?
어떤 다른 객체로 접근하는 것을 통제하기 위해서, 그 객체의 대리자 또는 자리표시자의 역할을 하는 객체를 제공하는 패턴입니다.
JPA 의 지연로딩을 이해하기 위해서, JPA 에서 사용하는 Proxy 에 대해서 학습해보겠습니다.
엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 EntityManager.getReference() 메서드를 사용하면 됩니다.
public void printUserWithProxy() {
Member member = entityManager.getReference(Member.class, 0L);
System.out.println("before use member entity");
System.out.println("회원 이름 : " + member.getUsername());
}
위 코드를 실행했을 때, 출력되는 로그는 아래와 같습니다.
before use member entity
SELECT m.MEMBER_ID, m.TEAM_ID, m.USERNAME, t.TEAM_ID, t.NAME
FROM
Member m
LEFT OUTER JOIN
Team t
ON m.TEAM_ID = t.TEAM_ID
WHERE m.MEMBER_ID = 0;
회원 이름 : 회원1
즉, getReference()를 호출할 때 JPA 는 데이터베이스를 조회(SELECT 쿼리 실행)하지 않고 실제 엔티티 객체도 생성하지 않습니다.
대신에 데이터베이스 접근을 위임한 프록시 객체를 반환합니다.
Proxy 객체는 실제 클래스를 상속 받아서 만들어집니다.
하이버네이트가 내부적으로 상속받아서 만듭니다.
개발자는 해당 Entity의 메서드를 그대로 사용할 수 있습니다.
public void typeCheckWithProxyAndWithoutProxy() {
Member member = entityManager.find(Member.class, 0L);
Member memberWithProxy = entityManager.getReference(Member.class, 1L);
System.out.println("member 클래스 : " + member.getClass());
System.out.println("memberWithProxy 클래스 : " + memberWithProxy.getClass());
Assertions.assertNotSame(member.getClass(), memberWithProxy.getClass());
Assertions.assertTrue(member instanceof Member);
Assertions.assertTrue(memberWithProxy instanceof Member);
}
위 코드를 실행하면 출력되는 로그는 아래와 같습니다.
member 클래스 : class TIL.jpa.Domain.Member
memberWithProxy 클래스 : class TIL.jpa.Domain.Member$HibernateProxy$OZWfzq9P
Proxy 객체는 실제 객체의 참조(target)를 보관합니다.
Proxy 객체의 메서드를 호출하면 Proxy 객체는 실제 객체의 메서드를 호출합니다.
Entity를 Proxy로 조회할 때, 식별자(PK)값을 파라미터로 전달하는데, Proxy 객체는 해당 식별자 값을 보관합니다.
Member member = entityManager.getReference(Member.class, 0L);
System.out.println("회원 ID = " + member.getId());
Proxy 객체의 실제 객체인 target은 getReference()가 실행될 때는 생성되어 있지 않습니다.
member.getUserName( )처럼 실제 사용될 때 데이터베이스를 조회하여, 실제 엔티티를 생성합니다. 이를 Proxy 객체의 초기화라 합니다.
Proxy 초기화 예제 코드
public void printUserWithProxy() {
Member member = entityManager.getReference(Member.class, 0L);
System.out.println("before use member entity");
System.out.println("회원 이름 : " + member.getUsername()); // 1. getUserName();
}
Proxy 클래스 예상 코드
class MemberProxy extend Member {
Member target = null; // 실제 엔티티 참조, 초기에는 생성되어 있지 않습니다.
@Override
public String getUserName() {
if (target == null) {
// 2. 초기화 요청
// 3. DB 조회
// 4. 실제 엔티티 생성 및 참조 보관
this.target = ///;
}
// 5. target.getUserName();
return target.getUserName();
}
}
위 코드를 순서대로 설명해보겠습니다.
entityManager.getReference()로 Proxy 객체를 가져온 다음에, getUserName() 메서드를 호출 하면
MemberProxy 객체에 처음에 target 값이 존재하지 않습니다. JPA가 영속성 컨텍스트에 초기화 요청을 합니다.
영속성 컨텍스트가 DB에서 조회해서
실제 Entity를 생성해줍니다.
그리고 Proxy 객체가 가지고 있는 target(실제 Member)의 getUserName()을 호출해서 결국 member.getUserName()을 호출한 결과를 받을 수 있습니다.
Proxy 객체에 target이 할당 되고 나면, 더이상 Proxy 객체의 초기화 동작은 없어도 됩니다.
Proxy 객체의 초기화 여부 확인
entityManger.getEntityMangerFactory().getPersistenceUnitUtil().isLoaded(Object entity);
Proxy 객체 강제 초기화
org.hibernate.Hibernate.initialize(Object entity);
위 기능을 활용한 테스트 코드
public void proxyUtils() {
EntityManagerFactory emf = entityManager.getEntityManagerFactory();
Member memberWithProxy = entityManager.getReference(Member.class, 0L);
boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(memberWithProxy);
Assertions.assertFalse(isLoaded);
// proxy 강제 초기화
Hibernate.initialize(memberWithProxy);
boolean isLoadedAfterInit = emf.getPersistenceUnitUtil().isLoaded(memberWithProxy);
Assertions.assertTrue(isLoadedAfterInit);
}
다음 포스트에서는 지연 로딩 및 즉시 로딩에 대해서 알아보도록 하겠습니다.