자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.
✏️ 들어가기 전에
- 프록시와 즉시로딩, 지연로딩 : 객체는 객체 그래프로 연관된 객체들을 탐색한다. 그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다. JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다. 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다. 하지만, 자주 함께 사용하는 객체들은 조인을 사용해서 함께 조회하는 것이 효과적이다. JPA는 즉시 로딩과 지연 로딩이라는 방법으로 둘을 모두 지원한다.
- 영속성 전이와 고아 객체 : JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공한다.
- 엔터티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.
- 예를 들어 회원 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.
Member
를 조회할 때 Team
도 함께 조회해야할까?
Member
- 회원 엔티티
@Entity
public class Member {
private String username;
@ManyToOne
private Team team;
public Team getTeam() {
return team;
}
public String getUsername() {
return username;
}
...
}
Team
- 팀 엔티티
@Entity
public class Team {
private String name;
public String getName() {
return name;
}
...
}
printUserAndTeam
- 회원과 팀 정보를 출력하는 비즈니스 로직
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름 : " + member.getUsername());
System.out.println("소속팀 : " + team.getName());
}
printUser
- 회원 정보만 출력하는 비즈니스 로직
public String printUser(String memberId) {
Member member = em.find(Member.class, memberId);
System.out.println("회원 이름 : " + member.getUsername());
}
printUserAndTeam()
메소드는 memberId
로 회원 엔티티를 찾아서 회원은 물론이고 회원과 연관된 팀의 이름도 출력한다.printUser()
메소드는 회원 엔티티만 출력하는데 사용하고 회원과 연관된 팀 엔티티는 전혀 사용하지 않는다.printUser()
메소드는 회원 엔티티만 사용하므로 em.find()
로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티(Member.team
)까지 데이터베이스에서 함께 조회해 두는 것은 효율적이지 않다.➡️ 한쪽에서는 member만 출력하고, 한쪽에서는 member와 Team을 출력한다. (이럴 경우 member만 출력하는 쪽을 호출하려고 할 때 낭비가 발생한다. 구지 하나만 꺼내야 하는 건데 클래스 호출까지 해야하니? 좀 복잡한 과정이 추가되는 것 같다.)
JPA
는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다.
team.getTeam()
처럼 팀 엔티티의 값을 실제 사용하는 시점에 데이터베이스에서 팀 엔티티에 필요한 데이터를 조회하는 것이다.
이 방법을 사용하면 printUser()
메소드는 회원 데이터만 데이터베이스에서 조회해도 된다.
➡️ 그런데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.
💡 참고
JPA
표준 명세는 지연 로딩의 구현 방법을JPA
구현체에 위임했다. 따라서 지금부터는 하이버네이트 구현체에 대한 내용이다. 하이버네이트는 지연 로딩을 지원하기 위해 프록시를 사용하는 방법과 바이트코드를 수정하는 두 가지 방법을 제공하는데 바이트코드를 수정하는 방법은 설정이 복잡하므로 여기서는 별도의 설정이 필요 없는 프록시에 대해서만 알아보겠다. 바이트 코드를 수정하는 방법은 하이버네이트 공식 사이트를 참고하자!
JPA
에서 식별자로 엔티티 하나를 조회할 때는 EntityManager.find()
를 사용한다. 이 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.
Member member = em.find(Member.class, "member1");
이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다.
엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManager.getReference()
메소드를 사용하면 된다.
Member member = em.getReference(Member.class, "member1");
이 메소드를 호출할 때 JPA
는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다. (가짜 : 프록시)
em.find와 em.getReference의 차이점
JpaMain
try {
Member member = new Member();
member.setUsername("user");
em.persist(member);
em.flush();
em.clear();
// 추가
}
em.find
Member findMember = em.find(Member.class, member.getId());
em.getReference
Member findMember = em.getReference(Member.class, member.getId());
📌 정리
em.find()
: 데이터베이스를 통해서 실제 엔티티 객체 조회em.getReference()
: 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
프록시 객체의 초기화 : 프록시 객체는 member.getName()
처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
프록시 초기화 예제
// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
member.getName(); // 1. getName();
프록시 클래스 예상 코드
class MemberProxy extends Member {
Member target = null; // 실제 엔티티 참조
public String getName() {
if(target == null) {
// 2. 초기화 요청
// 3. DB 조회
// 4. 실제 엔티티 생성 및 참조 보관
this.target = ...;
}
// 5. target.getName();
return target.getName();
}
}
위의 그림(프록시 초기화)와 위의 예제 코드(프록시 클래스 예상 코드)로 프록시의 초기화 과정을 분석해보자!
member.getName()
을 호출해서 실제 데이터를 조회한다.Member target
멤버변수에 보관한다.getName()
을 호출해서 결과를 반환한다.
==
로는 비교 실패, 대신 instance of
사용하여 타입 체크를 해야 한다.)em.getReference()
를 호출해도 프록시가 아닌 실제 엔티티를 반환한다. (이미 있으므로, 영속성 컨텍스트에 저장되어있는 실제 엔티티를 반환한다.)org.hibernate.LazyInitializationException
예외를 발생시킨다.4번째 특징 관련 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()
를 호출해도 프록시가 아닌 실제 엔티티를 반환한다. 실행 해볼시
JpaMain
try {
Member member = new Member();
member.setUsername("user");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId()); //Proxy
System.out.println("refMember = " + refMember.getClass());
Member findMember = em.getReference(Member.class, member.getId()); // Member
// 프록시가 한번 반환되면 이후 프록시를 호출하면 똑같은 프록시가 나온다.
// 프록시가 아니다. 개발하는 것이 중요하다.
System.out.println("findMember = " + findMember.getClass());
System.out.println("refMember == findMember " + (refMember == findMember));
tx.commit();
}
실행 결과
5번째 특징 관련 - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
org.hibernate.LazyInitializationException
예외를 발생할 시 의심할만 상황들
JpaMain
try {
Member member = new Member();
member.setUsername("user");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId()); //Proxy
System.out.println("refMember = " + refMember.getClass());
// em.detach(refMember); // 영속성 컨텍스트에서 끄집어 낸다. refMember를 영속성 컨텍스트에서 관리안하겠다.
em.close(); // 영속성 컨텍스트를 종료해버린다. 똑같이 refMember를 호출해봐야 조회되지 않는다.
System.out.println("refMember.getUsername() = " + refMember.getUsername()); //
// could not initialize proxy [Member#1] - no Session proxy를 초기화 할 수 없다.
// 영속성 컨텍스트에 해당 refMember이 없어요!
tx.commit();
} catch (Exception e) {
System.out.println("예외 발생");
tx.rollback();
e.printStackTrace();
}finally{
em.close();
}
실행 결과
em.detach로 영속성 컨텍스트에서 끄집어 낸다. refMember를 영속성 컨텍스트에서 관리안하겠다.
em.close로 영속성 컨텍스트를 종료해버린다. 똑같이 refMember를 호출해봐야 조회되지 않는다.
// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
transaction.commit();
em.close(); // 영속성 컨텍스트 종료
member.getName(); // 준영속 상태 초기화 시도,
// org.hibernate.LazyInitializationException 예외 발생
em.close()
메소드로 영속성 컨텍스트를 종료해서 member
는 준영속 상태다. member.getName()
을 호출하면 프록시를 초기화해야 하는데 영속성 컨텍스트가 없으므로 실제 엔티티를 조회할 수 없다. 따라서 예외가 발생한다.
💡 참고
JPA
표준 명세는 지연 로딩(프록시)에 대한 내용을JPA
구현체에 맡겼다. 따라서 준영속 상태의 엔티티를 초기화할 때 어떤 일이 발생할지 표준 명세에는 정의되어 있지 않다. 하이버네이트를 사용하면org.hibernate.LazyInitializationException
예외가 발생한다.
엔티티를 프록시로 조회할 때 식별자(PK
) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
Team team = em.getReference(Team.class, "team1"); // 식별자 보관
team.getId(); // 초기화되지 않음
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()
를 호출해도 프록시를 초기화하지 않는다. 단 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY)
)로 설정한 경우에만 초기화하지 않는다.
엔티티 접근 방식을 필드(@Access(AccessType.FIELD)
)로 설정하면 JPA
는 getId()
메소드가 id
만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화한다!
프록시는 다음 코드처럼 연관관계를 설정할 때 유용하게 사용할 수 있다.
Member member = em.find(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); // SQL을 실행하지 않음
member.setTeam(team);
JPA
가 제공하는 PersistenceUnitUtil.isLoaded(Object entity)
메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다. false
를 반환한다. true
를 반환한다.boolean isLoad = em.getEntityManagerFactory()
.getPersistenceUnitUtil().isLoaded(entity);
// 또는 boolean isLoad = emf.getPersistenceUnitUtil().isLoaded(entity);
System.out.println("isLoad = " + isLoad); // 초기화 여부 확인
조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하려면 클래스명을 직접 출력해보면 된다.
System.out.println("memberProxy = " + member.getClass().getName());
// 결과 : memberProxy = jpabook.domain.Member_$$_javassist_0
위 예를 보면 클래스 명 뒤에 javasist
가 있는데 이것으로 프록시인 것을 확인할 수 있다.
프록시를 생성하는 라이브러이에 따라 출력 결과는 달라질 수 있다.
✔️ 프록시 강제 초기화
하이버네이트의 initialize()
메서드를 사용하면 프록시를 강제로 초기화할 수 있다.
Hibernate.initialize(refMember); // 프록시 강제 초기화
JPA
표준에는 프록시 강제 초기화 메서드가 없다.
따라서 강제로 초기화하려면 member.getName()
처럼 프록시의 메서드를 직접(강제) 호출하면 된다.
JPA
표준은 단지 초기화 여부만 확인할 수 있다.
프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.
member1
이 team1
에 소속해 있다고 가정해보면
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println(team.getName()); // 팀 엔티티 사용
📣 JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 두 가지 방법을 제공한다.
(1) 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- ex)
em.find(Member.class, "member1")
를 호출할 때 회원 엔티티와 연관된 팀 엔티티도 함께 조회한다.- 설정 방법 :
@ManyToOne(fetch=FetchType.EAGER)
(2) 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
- ex)
member.getTeam().getName()
처럼 조회한 팀 엔티티를 실제 사용하는 시점에JPA
가SQL
을 호출해서 팀 엔티티를 조회한다.- 설정 방법 :
@ManyToOne(fetch=FetchType.LAZY)
지연 로딩(
LAZY LOADING
)을 사용하려면@ManyToOne
의fetch
속성을FetchType.LAZY
로 지정한다.
✔️ 지연 로딩 설정
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
✔️ 지연 로딩 실행 코드
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("user");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member m = em.find(Member.class, member.getId());
// Member 클래스에서 team에 Lazy를 사용할 시, 멤버에서 팀을 조회할 때는 프록시로 조회를 한다.
System.out.println("m = " + m.getTeam().getClass()); // 객체 그래프 탐색
// 결과 : m = class Team$HibernateProxy$nNhK9NQK
// 연관관계를 프록시로 가져온 것이다.
Team team1 = team = member.getTeam();
System.out.println("=============");
System.out.println("Member로 team 조회");
m.getTeam().getName(); // team을 조회할 때
System.out.println("=============");
System.out.println("조회한 후, member.getTeam()로부터 받은 team 객체를 실행");
System.out.println("team1 = " + team1.getClass()); // 객체 그래프 탐색
// Team 엔티티를 실제 사용하여 조회할 때는 DB에서 값을 가져온 것을 확인할 수 있다.
// m은 프록시, team1은 클래스 객체
tx.commit();
실행 결과
지연 로딩 설정을 보면 회원과 팀을 지연 로딩으로 설정했다. 따라서 지연 로딩 실행 코드에서 em.find(Member.class, "member1")
를 호출하면 회원만 조회하고 팀은 조회하지 않는다.
대신에 조회한 회원의 team
멤버변수에 프록시 객체를 넣어둔다.
Team team = member.getTeam(); // 프록시 객체
반환된 팀 객체는 프록시 객체다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 그래서 지연 로딩이라 한다.
team.getName(); // 팀 객체 실제 사용
실제 데이터가 필요한 순간이 되어서야(실제 team 객체를 사용할 때) 데이터베이스를 조회해서 프록시 객체를 초기화한다.
✔️ 지연 로딩 LAZY을 사용해서 프록시 조회 추가 설명
Member member = em.find(Member.class, 1L);
Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회)
// getTeam()다음 getName, Team의 메서드를 호출하는 순간 초기화가 일어난다.
✔️ 즉시로딩 관련 추가 내용
em.find(Member.class, "member1")
호출시 실행되는 SQL
SELECT * FROM MEMBER
WHERE MEMBER_ID = 'member1'
team.getName()
호출로 프록시 객체가 초기화되면서 실행되는 SQL
SELECT * FROM TEAM
WHERE TEAM_ID = 'team1'
team1
엔티티가 영속성 컨텍스트에 이미 로딩되어 있으면 프록시가 아닌 실제 team1
엔티티를 사용한다.
- Member와 Team을 자주 함께 사용한다면? 즉시로딩을 써야 한다.
- 즉시 로딩(
EAGER LOADING
)을 사용하려면@ManyToOne
의fetch
속성을FetchType.EAGER
로 지정한다.
✔️ 즉시 로딩 설정
@Entity
public class Member {
// ...
@ManyToOne(fetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
✔️ 즉시 로딩 실행 코드
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("user");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member m = em.find(Member.class, member.getId());
// 즉시 한번에 DB에서 데이터를 가져와버린다.
System.out.println("m = " + m.getTeam().getClass()); // 객체 그래프 탐색
Team team1 = team = member.getTeam();
System.out.println("=============");
System.out.println("Member로 team 조회");
m.getTeam().getName();
// 초기화가 끝난 상태라 아무 것도 출력되지 않는다.
// 초기화 될 때 select문이 실행된다.
System.out.println("=============");
System.out.println("조회한 후, member.getTeam()로부터 받은 team 객체를 실행");
System.out.println("team1 = " + team1.getClass()); // 객체 그래프 탐색
tx.commit();
m.getTeam().getName()
을 출력해보면 select문이 실행되지 않는다.
위의 즉시 로딩 설정 코드를 보면 회원과 팀을 즉시 로딩으로 설정했다. 따라서 즉시 로딩 실행 코드에서 em.find(Member.class, "member1")
로 회원을 조회하는 순간 팀도 함께 조회한다.
✔️ 즉시 로딩(EAGER), Member조회시 항상 Team도 조회
JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.
이때 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행할 것 같지만, 대부분의 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.NAME AS NAME
FROM
MEMBER M LEFT OUTER JOIN TEAM T
ON M.TEAM_ID=T.TEAM_ID
WHERE
M.MEMBER_ID='member1'
실행되는 SQL
을 분석해보면 회원과 팀을 조회해서 쿼리 한 번으로 조회한 것을 알 수 있다.
이후, member.getTeam()
을 호출하면 이미 로딩된 Team1
엔티티를 반환한다.
💡 참고 - NULL 제약조건과 JPA 조인 전략
- 현재 회원 테이블에
TEAM_ID
외래 키는NULL
값을 허용하고 있다. 따라서 팀에 소속되지 않은 회원이 있을 가능성이 있다.JPA
에게도 이런 사실을 알려줘야 한다. 다음 코드처럼@JoinColumn에 nullable = false
을 설정해서 이 외래 키는NULL
값을 허용하지 않는다고 알려주면 JPA는 외부 조인 대신에 내부 조인을 사용한다.@Entity public class Member { // ... @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "TEAM_ID", nullable = false) private Team team; // ... }
💡 참고 - nullable 설정에 따른 조인 전략
@JoinColumn(nullable = true)
:NULL
허용(기본값), 외부 조인 사용@JoinColumn(nullable = false)
:NULL
허용하지 않음, 내부 조인 사용 또는@Entity public class Member { // ... @ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "TEAM_ID") private Team team; // ... }
이와 같이
@ManyToOne.optional = false
로 설정해도 내부 조인을 사용한다.
📌 정리
JPA는 선택적 관계면 외부 조인을 사용하고 필수 관계면 내부 조인을 사용한다.
📌 정리
지연 로딩(LAZY)
- 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
- 언제 사용할까? Member와 team 클래스를 보았을 때, Member 클래스에서 member만 조회할 때 즉, member클래스 안에 있는 team 객체 외부 조인을 사용하지 않을 때 지연 로딩을 사용한다.
즉시 로딩(EAGER)
- 연관된 데이터를 즉시 조회한다. 하이버네이트는 가능하면
SQL
조인을 사용해서 한 번에 조회한다.- 언제 사용할까? 비즈니스 로직에서 많이 사용하는데 Member, team 클래스처럼 연관관계 과정을 가진 클래스들을 같이 조회할 때 그러니까 Member클래스를 조회할 때 team 객체 외부조인을 같이 조회할 때 즉시 로딩을 사용한다.
⚠️ 주의
실무에서는 가급적 지연 로딩만 사용한다. (조인 많으면 즉시 로딩시 복잡해진다. 연관된 테이블 다 가져와버림)
즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
즉시 로딩은
JPQL
에서N + 1
문제를 일으킨다.
@ManyToOne
,@OneToOne
: 기본이 즉시 로딩 (LAZY
로 설정)
@OneToMany
,@ManyToMany
: 기본이 지연 로딩
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
// n + 1 : 처음 쿼리 select 문이 한 번 실행되고, 추가 쿼리 n개가 실행된다.
사내 주문 관리 시스템을 개발
✔️ 사용할 모델 분석
Member
)은 팀(Team
) 하나에만 소속할 수 있다. N : 1
Member
)은 여러 주문내역(Order
)을 가진다. 1 : N
Order
)은 상품정보(Product
)를 가진다. N : 1
&nsp;
✔️ 애플리케이션 로직을 분석
Member
와 연관된 Team
은 자주 함께 사용되었다. 그래서 Member
와 Team
은 즉시 로딩으로 설정했다.Member
와 연관된 Order
는 가끔 사용되었다. 그래서 Member
와 Order
는 지연 로딩으로 설정했다.Order
와 연관된 Product
는 자주 함께 사용되었다. 그래서 Order
와 Product
는 즉시 로딩으로 설정했다.
✔️ 회원 엔티티
@Entity
public class Member {
@Id
private String id;
private String username;
private Integer age;
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders;
// Getter, Setter ...
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
FetchType.EAGER
로 설정
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders;
FetchType.LAZY
로 설정
Member member = em.find(Member.class, "member1");
회원과 팀은 즉시 로딩(FetchType.EAGER
)로 설정했다.
따라서 회원을 조회할 때 연관된 teamA
도 함께 조회한다.
✔️ 회원 엔티티를 조회할 때 JPA가 실행한 SQL
SELECT
MEMBER.ID AS MEMBERID,
MEMBER.AGE AS AGE,
MEMBER.TEAM_ID AS TEAM_ID,
MEMBER.USERNAME AS USERNAME,
TEAM.ID AS TEAMID,
TEAM.NAME AS NAME
FROM
MEMBER MEMBER
LEFT OUTER JOIN
TEAM TEAM ON MEMBER.TEAM_ID=TEAM1.ID
WHERE
MEMBER0_.ID='member1'
FetchType.EAGER
로 설정했으므로 하이버네이트는 조인 쿼리를 만들어 회원과 팀을 한 번에 조회한다.FetchType.LAZY
로 설정했으므로 결과를 프록시로 조회한다.member.getTeam()
을 호출하면 이미 로딩된 팀 엔티티를 반환한다.
즉시 로딩한 teamA
는 실선으로 표현했고 지연 로딩한 주문내역은 점선으로 표현했다.
이렇게 지연 로딩으로 설정하면 실제 엔티티 대신에 프록시 객체를 사용한다.
(프록시 객체는 실제 자신이 사용할 때까지 데이터베이스를 조회하지 않는다.)
✔️ 주문내역 조회
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
// 결과 : orders = org.hibernate.collection.internal.PersistentBag
컬렉션 래퍼 : 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
💡 참고
- 컬렉션 래퍼도 컬렉션에 대한 프록시 역할을 하므로 프록시라고 불러도 된다.
member.getOrders()
를 호출해도 컬렉션은 초기화되지 않는다. 컬렉션은member.getOrders().get(0)
처럼 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화한다.
FetchType.EAGER
로 설정했다.
✏️
fetch
속성의 기본 설정 값
@ManyToOne
,@OneToOne
: 즉시 로딩(FetchType.EAGER
)@OneToMany
,@ManyToMany
: 지연 로딩(FetchType.LAZY
)
fetch
) 전략은 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다.
✔️ 왠만하면 모든 연관관계에 지연 로딩을 사용하자!
즉시로딩은? 애플리케이션 개발이 어느 정도 완료단계에 왔을 때 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하면 된다!
🔔 FetchType.EAGER 설정과 조인 전략을 정리
@ManyToOne
,@OneToOne
- (optional = false) : 내부 조인
- (optional = true) : 외부 조인
@OneToMany
,@ManyToMany
- (optional = false) : 내부 조인
- (optional = true) : 외부 조인
📌 정리
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
JPQL fetch
조인이나, 엔티티 그래프 기능을 사용해라! (뒤에서 공부)- 즉시 로딩은 상상하지 못한 쿼리가 나간다.
참고