1. 프록시
어느 경우에 Member만 가져오고, 어느 경우에는 Member와 Team 같이 가져와야한다.
Member만 가져와야 하는데 Team도 같이 가져오는 경우에는 낭비이다.
이런 경우를 해결하기 위해서는 지연 로딩과 즉시 로딩을 사용해서 해결을 하는데, 해당 개념을 공부하기 위해서는 프록시에 대해서 먼저 알아봐야한다!
JPA에서는 find() 메서드가 아닌 getReference() 메서드를 제공하고 있다.
em.find() 메서드는 데이터베이스를 통해서 실제 엔티티 객체를 조회를 하지만,
em.getReference() 메서드는 참조를 가져오는 메서드이며, 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회. 즉, 쿼리를 날리지 않고 조회를 하는 메서드이다.
아래 코드를 보고 어떻게 동작하는지 살펴보자
import hellojpa.relationship.Member;
import hellojpa.relationship.Team;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class RelationShipMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId()); // 해당 시점에는 쿼리가 날아가지 않음
// 해당 값을 사용해야 하는 시점에 쿼리가 날아감
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
위의 코드를 실행시키면 아래와 같은 결과를 확인할 수 있다.
id를 조회할 때는 쿼리를 날리지 않고, name을 조회할 때는 쿼리를 날린 후에 print 하는 것을 확인할 수 있다.
그럼 findMember는 무엇일까?
System.out.println("findMember.class = " + findMember.getClass());
위와 같이 getClass로 출력해 보면 아래와 같은 값이 나온다.
findMember.class = class hellojpa.relationship.Member$HibernateProxy$lQnwZTKq
이것이 무엇일까...! 이것은 하이버네이트에서 만든 가짜 클래스이다. 이거시 프록시 클래스!
하이버네이트가 내부의 라이브러리를 사용해서 가짜 엔티티 객체를 주는 것이다.
껍데기는 똑같은데 안에는 텅텅 비어있다.
프록시의 특징
- 실제 클래스를 상속 받아서 만들어진다.
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
- 프록시 객체는 처음 사용할 때 한번만 초기화한다.
- 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화가 되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능하다. 프록시는 유지가 되고, 내부의 target에만 값이 채워지는 것이다.
- 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. ( == 은 비교 실패, instance of 사용해야함. 같은 Member.class여도 프록시 객체가 넘어와서 == 비교를 하게 되면 false로 나온다.)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. (detach, clear, close와 같이 영속석 컨텍스트로 관리를 하지 못하게 만든 후에 getName을 하면 org.hibernate.LazyInitializationException: could not initialize proxy ... 예외가 발생)
참고) JPA는 a == a 늘 true로 맞춰야 한다. getReference()로 호출하여 프록시 객체를 만들고, find()로 쿼리를 날린 상태라면 전자는 프록시 객체를 후자는 실제 객체를 가지고 있겠지만 == 비교를 하면 true로 나오며, 각 객체의 getClass()를 호출해보면 프록시 객체가 나오는 것을 확인할 수 있다.
Member member = new Member();
member.setName("hello");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId()); // Proxy
System.out.println("findMember.class = " + refMember.getClass());
Member findMember = em.find(Member.class, member.getId()); // Member
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("refMember == findMember " + (refMember == findMember));
프록시 객체의 초기화
getName() 호출하면 Member target에 처음에는 값이 없기 때문에 JPA가 영속성 컨텍스트에게 진짜 Member를 가져오라고 요청을 한다(초기화 요청). 그러면 영속성 컨텍스트가 DB를 통해서 값을 조회해 실제 Entity 객체를 생성해서 주고, Member target에 진짜 객체를 연결해 준다.
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
: entityManagerFactory의 PersistenceUnitUtil.isLoaded(Object entity)
- 프록시 클래스 확인 방법
: entity.getClass() 출력
- 프록시 강제 초기화(하이버네이트에서 제공)
: org.hibernate.Hibernate.initialize(entity);
JPA 표준은 강제 초기화 없으며, 강제 호출은 member.getName() 이런식으로 하면된다.
2. 즉시 로딩(EAGER)과 지연 로딩(LAZY)
1) 지연 로딩(LAZY)
Member만 필요한데 Member와 Team 같이 조회하는 것은 자원 낭비이다.
이럴 때 사용하는 방법은 지연로딩! fetch = @FetchType.LAZY를 통해서 프록시 객체로 조회할 수 있도록 하는데, Member 클래스만 DB로 조회하고, Team은 프록시 객체로 조회하는 것이다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
위와 같이 LAZY를 설정해주고 실행을 해보면
import hellojpa.relationship.Member;
import hellojpa.relationship.Team;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class RelationShipMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("hello");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("m = " + findMember.getTeam().getClass());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
아래와 같이 Member만 조회하는 쿼리를 날리고, Team은 프록시 객체를 통해서 가져오는 것을 확인할 수 있다.
만약 findMember를 통해서 team을 가지고 온다면 프록시 객체가 초기화(getName을 호출하는 순간 초기화가 됨)되면서 Team 조회해온다.
findMember.getTeam().getName();
위의 예시를 그림으로 정리하면 아래와 같다.
2) 즉시 로딩(EAGER)
만약, Member와 Team을 자주 함께 사용한다면 지연 로딩을 사용하는 것은 Member 따로, Team 따로 조회하기 때문에 오히려 손해이다. 이럴 때는 즉시 로딩 fetch = FetchType.EAGER을 사용하면 된다!
Member 엔티티에 있는 Team의 fetch를 아래와 같이 변경을 해주고
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
동일하게 실행을 시키면 아래와 같은 결과를 확인할 수 있다.
join을 통해서 바로 가져오기 때문에 이미 초기화가 끝났기 때문에 프록시 객체를 초기화할 필요가 없고, Team의 클래스를 조회하면 진짜가 출력된다.
위의 예시를 그림으로 표현하면 아래와 같다.
3) 프록시와 즉시로딩 주의점
(1) 가급적 지연 로딩만 사용하기(특히 실무에서!!!)
(2) 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
: 여러 개의 테이블이 있는 경우 다 join을 시켜서 쿼리가 날라가고 성능이 나오지 않음
(3) 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- Member를 가져오고 Member의 개수만큼 Team을 조회하기 위해서 쿼리가 날아간다.
- 1개를 가져오려고 했는데 추가 쿼리가 나가서 N+1이라고 한다.
- LAZY로 설정하고 아래와 같은 3가지 방법으로 해결할 수 있음(추후에 더 자세하게 배울 예정)
a. JPQL에서 FetchJoin, 런타임에 동적으로 원하는 것을 가져오는 것
b. EntityGraph라는 어노테이션으로 처리
c. BatchSize로 처리
(4) @ManyToOne, @OneToOne은 기본이 즉시 로딩이다. LAZY로 설정하여 지연 로딩을 사용하자
(5) @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
3. 지연 로딩 활용
이 목차는 이론적인 내용이며, 실제 실무에서는 무조건 지연 로딩을 사용하자!
Member와 Team 자주 함께 사용 -> 즉시 로딩
Member와 Order 가끔 사용 -> 지연 로딩
Order와 Product 자주 함께 사용 -> 즉시 로딩
위와 같이 설정을 했다고 하면 아래와 같은 그림으로 표현할 수 있다.
member1을 통해서 orders를 조회하려고 할 때 Order와 Product는 즉시 로딩으로 설정되어 있기 때문에 건드리는 순간에 즉시 로딩으로 상품을 가지고 오게된다.
'TIL > JPA' 카테고리의 다른 글
[TIL/JPA] 기본개념 : 값 타입 (0) | 2024.08.31 |
---|---|
[TIL/JPA] 기본개념 : 영속성전이(CASCADE), 고아 객체 (0) | 2024.08.27 |
[TIL/JPA] 기본개념 : 고급 매핑(상속관계 매핑, @MappedSuperclass) (0) | 2024.08.24 |
[TIL/JPA] 기본 개념 : 다양한 연관관계 매핑 (0) | 2024.08.22 |
[TIL/JPA] 기본 개념 : 연관관계 매핑 기초 (0) | 2024.08.17 |