목차
- 프록시
- 즉시 로딩과 지연 로딩
- 지연 로딩 활용
- 영속성 전이 : CASCADE
- 고아 객체
- 영속성 전이 + 고아 객체, 생명주기
- 예제
프록시
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = em.find(Member.class, 1L);
printMember(member);
// printMemberAndTeam(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
private static void printMember(Member member) {
System.out.println("member = " + member.getName());
}
private static void printMemberAndTeam(Member member) {
String userName = member.getName();
System.out.println("userName = " + userName);
Team team = member.getTeam();
System.out.println("team = " + team.getName());
}
}
- 멤버만 조회하려고할 때 팀도 조회해야할까?
프록시 기초
- em.find() : 데이터베이스를 통해 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
프록시 특징
- 프록시는 실제 클래스를 상속 받아 만들어지며, 겉 모양이 같다.
- 이론상으로 사용하는 입장에서는 진짜인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 한 트랜잭션 안에서 객체의 동일성을 보장 (프록시, 실제 객체 구별없이 동일성 보장 - 컬렉션 방식)
프록시 객체 초기화
getReference()
를 사용해서 조회하려는 값이 프록시가 갖고 있지 않은 값이면 영속성 컨텍스트에 초기화 요청을 하여 실제 객체에 값을 넣어준다.
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
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.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId()); // 프록시 객체 생성
System.out.println("findMember = " + findMember.getClass()); // ID는 갖고 있으므로 프록시에서 바로 반환
System.out.println("findMember.id = " + findMember.getId()); // ID는 영속성 컨텍스트에 초기화 요청을 통한 DB접근
System.out.println("findMember.userName() = " + findMember.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 프록시의 동작과정을 다음과 같이 확인해볼 수 있다.
getClass
를 찍어보면 프록시 클래스임을 알 수 있다.
프록시 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member1 = new Member();
member1.setName("member1");
em.persist(member1);
Member member2 = new Member();
member2.setName("member2");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 : " + (m1 instanceof Member));
System.out.println("m2 : " + (m2 instanceof Member));
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다. 즉, 타입 체크시
==
가 아닌 상속관계인instanceof
를 사용해야한다.
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member1 = new Member();
member1.setName("member1");
em.persist(member1);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 : " + (m1.getClass()));
// 영속성 컨텍스트에 m1에 대한 정보가 등록되어있다.
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference : " + (reference.getClass()));
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
private static void printMember(Member member) {
System.out.println("member = " + member.getName());
}
private static void printMemberAndTeam(Member member) {
String userName = member.getName();
System.out.println("userName = " + userName);
Team team = member.getTeam();
System.out.println("team = " + team.getName());
}
}
- 영속성 컨텍스트에 찾는 엔티티가 이미 존재하면, 프록시를 생성해도 실제 엔티티를 반환한다.
- 반대로 프록시를 먼저 생성하고 실제 객체를 생성하려고해도 프록시를 반환한다. 즉, 트랙잭션 안에서 객체의 동일성을 보장한다.
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member1 = new Member();
member1.setName("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember : " + (refMember.getClass()));
em.detach(refMember); // 준영속 상태로 만듦
refMember.getName(); // 예외 발생
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태(detach, clear, close 등)일 때, 프록시를 초기화하면 문제가 발생한다. 프록시는 한 번만 초기화된다.
LazyInitializationException
예외가 발생한다.
프록시 확인
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember : " + (refMember.getClass()));
refMember.getName(); // 프록시 없으니 초기화 요청
System.out.println("isLoaded= " + emf.getPersistenceUnitUtil().isLoaded(refMember));
- 프록시 인스턴스의 초기화 여부 확인 (
emf.getPersistenceUnitUtil().isLoaded(refMember)
) - 프록시 클래스 확인 (
refMember.getClass()
) - 프록시 강제 초기화
getName()
과 같은 것이 강제 초기화 방식이다.- 하이버네이트는
Hibernate.initialize(refMember)
가 있지만, JPA는 강제 초기화가 없어서getName()
과 같은 방식을 사용한다.
즉시로딩과 지연 로딩
회원 엔티티에 지연 로딩 적용
package hellojpa;
import javax.persistence.*;
@Entity
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
}
- 연관관계에
fetch = FetchType.LAZY
로 지연 로딩을 적용 시켜보자.
package hellojpa;
import javax.persistence.*;
public class JpaMain {
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 member1 = new Member();
member1.setName("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("member : " + m.getClass());
System.out.println("member.team = " + m.getTeam().getClass());
System.out.println("===========");
m.getTeam().getName();
System.out.println("===========");
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 지연로딩을 사용하면 프록시를 사용해서 연관된 테이블이 필요할 때만 쿼리를 날린다고 했다. 테스트를 통해 검증해보자.
Hibernate:
select
member0_.MEMBER_ID as member_i1_3_0_,
member0_.createDate as createda2_3_0_,
member0_.createdBy as createdb3_3_0_,
member0_.lastModifiedDate as lastmodi4_3_0_,
member0_.modifiedBy as modified5_3_0_,
member0_.USERNAME as username6_3_0_,
member0_.team_TEAM_ID as team_tea7_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
member : class hellojpa.Member
member.team = class hellojpa.Team$HibernateProxy$D9znehjT
===========
Hibernate:
select
team0_.TEAM_ID as team_id1_7_0_,
team0_.createDate as createda2_7_0_,
team0_.createdBy as createdb3_7_0_,
team0_.lastModifiedDate as lastmodi4_7_0_,
team0_.modifiedBy as modified5_7_0_,
team0_.name as name6_7_0_
from
Team team0_
where
team0_.TEAM_ID=?
===========
- 지연로딩이 정상적으로 적용된 결과를 얻을 수 있다. 즉,
Team
에 관련된 정보가 없으면 실제 객체를, 있으면 프록시 객체를 통해 조회 한다.
즉시 로딩 적용
Hibernate:
select
member0_.MEMBER_ID as member_i1_3_0_,
member0_.createDate as createda2_3_0_,
member0_.createdBy as createdb3_3_0_,
member0_.lastModifiedDate as lastmodi4_3_0_,
member0_.modifiedBy as modified5_3_0_,
member0_.USERNAME as username6_3_0_,
member0_.team_TEAM_ID as team_tea7_3_0_,
team1_.TEAM_ID as team_id1_7_1_,
team1_.createDate as createda2_7_1_,
team1_.createdBy as createdb3_7_1_,
team1_.lastModifiedDate as lastmodi4_7_1_,
team1_.modifiedBy as modified5_7_1_,
team1_.name as name6_7_1_
from
Member member0_
left outer join
Team team1_
on member0_.team_TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
member : class hellojpa.Member
member.team = class hellojpa.Team
===========
===========
- 즉시로딩 (EAGER) 방식은 Member 가져올 때 연관 된 모든 테이블을 가져온다.
지연로딩과 즉시로딩의 결정 여부는 비즈니스 로직에 따라 다르다. Member 와 Team을 같이 조회하는 경우가 많으면 즉시로딩(EAGER)을 Member 만 조회하는 경우가 많으면 지연로딩(LAZY)을 사용하는 것이 바람직하다. 하지만 즉시로딩 시 주의할 점이 존재해서 실무에서는 즉시로딩을 사용하지 않는다.
즉시로딩 주의
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생 (테이블이 많을 수록 더 문제가 생긴다.)
package hellojpa;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamA");
em.persist(teamB);
Member member1 = new Member();
member1.setName("member1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setName("member2");
member2.setTeam(teamB);
em.persist(member2);
em.flush();
em.clear();
// Member m = em.find(Member.class, member1.getId());
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. N+1이란 즉시 로딩에서 쿼리를 1번 날렸지만, 값을 가져오기위해 N번이 추가적으로 실행되어야하는 문제를 말한다. JPQL에서 쿼리문은 SQL로 번역되어 해당 Member 엔티티를 조회하는 쿼리를 날린다. 이 때 변환 과정에서 즉시로딩인 것을 보고 다시 팀에 관련된 쿼리를 다시 날리게 된다.
N+1 문제를 해결하기 위해선 우선적으로 모든 연관관계를 지연로딩으로 만든다. 이후 3가지 방법이 있다.
첫 번째로는 JPQL의 fetchJoin이 있다. fetchJoin은 런타임에 동적으로 원하는 객체를 가져오는 것이다.
List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
와 같이 사용한다.두 번째로는 엔티티 그래프와 어노테이션으로 해결하는 방법이 있다.
세 번째로는 배치사이즈를 사용해서 해결하는 방법이 있다. (1+1 방법)
- @ManyToOne, @OneToOne 은 기본이 즉시 로딩 (LAZY 설정 필요)
- @OneToMany, @ManyToMany는 기본이 지연 로딩
지연 로딩 활용
- 이론적이지만, 객체간 사용여부에 따라 즉시 로딩과 지연 로딩을 나누어서 설계할 수 있다. 실무에서는 지연 로딩만 사용하므로 가볍게 알아두자.
영속성 전이 : CASCADE
- 영속성 전이란 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 것을 말한다. 예를 들어 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 것을 의미한다.
CASCADE 없이 사용할 때 예제
Child 엔티티
package hellojpa;
import javax.persistence.*;
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Parent 엔티티
package hellojpa;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
JpaMain
package hellojpa;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.persist(child1);
em.persist(child2);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 생성한 객체마다 persist()를 해줘야 하는 불편함이 있다.
CASCADE 사용
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
- 부모 연관관계에 cascade를 설정하면 parent만 persist()하면 자동으로 child도 영속화 된다.
부모와 자식의 라이프 사이클이 동일하고 단일 소유자 일때 (Parent를 제외한 다른 엔티티에서 Child를 소유하지 않는 경우) 사용하자.
고아 객체
- 고아 객체란 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 의미한다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
- Parent에서 고아객체를 삭제하도록 설정해보자.
package hellojpa;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
// em.remove(findParent); 부모를 제거하니 자식도 모두 제거 (CascadeType.REMOVE)와 동일하게 동작한다.
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 부모에서 자식 객체 하나를 끊어보자.
- 다음과 같이 자동으로 부모에게서 연관관계가 끊긴 자식 객체가 삭제된 것을 볼 수 있다.
고아 객체도 cascade와 같이 참조하는 곳이 하나일 때, 단일 소유자일 때 사용하자. 또한 @OneToOne, @OneToMany 만 사용 가능하다.
영속성 전이 + 고아 객체
- 두 가지를 모두 사용하면 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
- DDD의 Aggregate Root 개념을 구현할 때 유용하다.
예제
이전 포스팅부터 예제로 해온 프로젝트에 적용하자.
글로벌 페치 전략 설정
- 모든 연관관계를 지연로딩(
fetch = FetchType.LAZY
)으로 변경하자. @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경하자.
영속성 전이 설정
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>()
Order
엔티티의 Delivery와 OrderItem을 영속성 전이 ALL 설정
본 포스팅은 인프런-김영한님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 참고하여 정리했습니다.