프록시
❓Member를 조회할 때 Team도 함께 조회해야 할까?
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()
VS em.getReference()
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
Member member = em.getReference(Member.class, 1L);
System.out.println("member = " + member.getClass()); // HibernateProxy 객체(가짜 클래스)
getReference() 메서드를 사용하면 진짜 객체가 아닌 하이버네이트 내부 로직으로 프록시 엔티티 객체 반환한다.
내부 구조는 틀은 같지만 내용이 비어있다. 내부에 target이 진짜 reference를 가리킨다.
프록시 특징
- 실제 클래스를 상속 받아 만들어지며, 실제 클래스와 겉 모양이 같다.
- 이론상으로 사용하는 입장에서는 진짜인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출(
getName()
)하면 프록시 객체는 실제 객체의 메소드 호출한다. - 프록시는 처음 사용할 때 한 번만 초기화
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 : " + (m1 instanceof Member)); //true
System.out.println("m2 : " + (m2 instanceof Member)); //true
- 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능.
- 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크시 주의해야함(
==
: 비교 실패(type 다름). 대신instance of
사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면
em.getReference()
를 호출해도 실제 엔티티 반환
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = "+ m1.getClass());//Member
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " reference.getClass()); //Member
m1 == reference //true
이미 Member를 1차 캐시에도 올라와 있는데, 프록시를 반환할 필요가 없다.
- 반대로
getReference()
로 프록시객체를 가지고 있으면 실제로find()
를 했을때도 프록시 객체를 반환한다. - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태(detach, clear, close 등)일 때, 프록시를 초기화하면 문제 발생
LazyInitializationException
예외가 발생한다.
- 한 트랜잭션 안에서 객체의 동일성을 보장 (프록시, 실제 객체 구별없이 동일성 보장 - 컬렉션 방식)
프록시 객체 초기화 과정
getReference()
를 사용해서 조회하려는 값이 프록시가 갖고 있지 않은 값이면 영속성 컨텍스트에 초기화 요청을 하여 실제 객체에 값을 넣어줌.- 영속성 컨텍스트에서는 실제 db를 조회해서 가져온 다음 실제 Entity에 값을 넣어 생성한 다음 프록시 객체는 실제 엔티티를 연결해서 실제 엔티티를 반환한다.
- 그 이후에는 이미 초기화되어있는 프록시 객체이기에 해당 엔티티를 반환한다.
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());
- 프록시의 동작과정을 다음과 같이 확인해볼 수 있다.
getClass
를 찍어보면 프록시 클래스임을 알 수 있다.
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인 :
PersistenceUnitUtil.isLoaded(Object entity) → entityManagerFactory.getPersistenceUnitUtil().isLoaded(object)
- 프록시 클래스 확인 방법
entity.getClass().getname()
출력(..javasist.. or HibernateProxy...) - 프록시 강제 초기화
:org.hibernate.Hibernate.initialize(entity);
- 참고: JPA 표준은 강제 초기화 없음 강제 호출
:method.getName();
즉시 로딩과 지연 로딩
지연 로딩
❓Member를 조회할 때 Team도 함께 조회해야 할까?
: member 정보만 사용하는 비즈니스 로직에서
지연 로딩 LAZY을 사용해서 프록시로 조회 fetch = FetchType.LAZY
/*Member*/
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.LAZY) //지연로딩 사용
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
...
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); //Team$HibernateProxy객체 반환
m.getTeam().getName() // team을 실제로 사용하는 시점에서 초기화 (db조회)
...
즉시 로딩
❓Member를 조회할 때 Team도 함께 조회해야 할까?
: Member와 Team을 같이 쓰는 빈도가 높을 경우
즉시 로딩 EAGER를 사용해서 함께 조회 fetch = FetchType.EAGER
/*Member*/
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.EAGER) //즉시로딩 사용
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
...
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); //Team 객체 반환
...
즉시 로딩 : Member를 조회하는 시점에서 Team도 같이 조회하는 것.(조인 사용)
프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용 (특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL 발생
: 하나의 엔티티에 연관된 엔티티가 많다면, find() 수행 시 수십, 수백개의 테이블을 한번에 불러와야 한다. - 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
@ManyToOne
,@OneToOne
은 기본이 즉시 로딩으로 되어 있다. → 직접 전부 LAZY로 설정@OneToMany
,@ManyToMany
는 기본이 지연 로딩
지연 로딩 활용 - 이론
- Member와 Team 은 자주 함께 사용 → 즉시 로딩
- Member와 Order는 가끔 사용 → 지연 로딩
- Order와 Product는 자주 함께 사용 → 즉시 로딩
지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용하자.
- 실무에서 즉시 로딩을 사용하지 마라.
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라.
- 즉시 로딩은 내가 의도하지 않은 쿼리가 수행된다.
영속성 전이(CASCAD)와 고아 객체
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용.
ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.
영속성 전이 : 저장
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
/*main*/
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);// persist 3번
이 코드에서는 persist를 3번이나 해야한다. 이는 효율적이지 못해 보인다.
persist 한 번으로 child 까지 같이 persist는 불가능 할까?
영속성 전이(CASCADE)를 이용한 엔티티 저장 방법
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL)//영속성 전이 속성(CASCADE)사용
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
/*main*/
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);// parent만 persist 해주면 child도 같이 persist된다.
CASCADE 종류
- ALL: 모두 적용(모든 곳에서 맞춰야 하면)
- PERSIST: 영속(저장할 때만 사용 할 것이면)
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
❓그럼 영속성 전이(CASCADE)는 언제 써야 할까?
: 전이 될 대상이 한 군데에서만 사용된다면 써도 된다.
하지만, 해당 엔티티(Child)가 특정 엔티티(Parent)에 종속되지 않고 여러군데서 사용된다면
사용하지 않는게 좋다.
- 라이프 사이클이 동일할 때
- 단일 소유자 관계일 때
고아 객체
고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제 : orphanRemoval = true
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);// parent만 persist -> child도 같이 persist
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // orphanRemoval 동작
고아 객체 - 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함.
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
- 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 마치 CascadeType.REMOVE처럼 동작한다.
- Parent객체를 지우게 되면 Parent가 소유하고있는 ChildList에 속한엔티티들이 전부 같이 삭제된다.
영속성 전이 + 고아 객체, 생명 주기 (CascadeType.ALL
+ orphanRemoval=true
)
- 스스로 생명주기를 관리하는 엔티티는
em.persist()
로 영속화,em.remove()
로 제거 - 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기 관리가 가능하다.
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
Aggregate Root
: 연관깊은 도메인들을 각각이 아닌 하나의 집합으로 다루는 것. 즉 데이터 변경의 단위로 다루는 연관 객체의 묶음.
Aggregate에는 루트(root)와 경계(boundary)가 있는데, 경계는 Aggregate에 무엇이 포함되고 포함되지 않는지를 정의한다. 루트는 단 하나만 존재하며, Aggregate에 포함된 특정 엔티티를 가르킨다. 경계안의 객체는 서로 참조가 가능하지만, 경계밖의 객체는 해당 Aggregate의 구성요소 가운데 루트만 참조할 수 있다.
실전 예제
글로벌 페치 전략 설정
- 모든 연관관계를 지연로딩(
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 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.