본문 바로가기
Back-End/Database

[JPA] 프록시와 연관 관계 관리

by 달의 조각 2023. 1. 24.
이 글은 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 수강하며 정리한 글입니다.

 

프록시
즉시 로딩과 지연 로딩

영속성 전이(CASCADE)와 고아 객체

 


 

프록시

 

  데이터베이스에서 member를 조회할 때, member의 이름만 필요한 경우(A)가 있고, member가 속한 team의 정보가 필요한 경우(B)가 있다. A는 member의 정보만 가져오는 것이, B는 team의 정보까지 가져오는 것이 효율적일 것이다.

Member member = em.find(Member.class, 1L);
printMember(member); // A
printMemberAndTeam(member); // B

 

🌕 em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해 실제 엔티티 객체 조회 방법이다.
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회 방법이다. 데이터베이스에 쿼리를 날리지 않고 객체를 조회한다. 객체를 실제 사용하는 시점에 쿼리가 날아간다.
// 쿼리가 날아가지 않는다
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());

// 쿼리가 날아간다(DB의 데이터가 필요)
System.out.println("findMember.username = " + findMember.getUsername());

 

🌕 프록시 특징

em.getReference()

  프록시는 실제 클래스를 상속 받아서 만들어진다. 실제 객체의 참조(target)를 보관한다. (초기에는 null이다. 영속성 컨텍스트에 초기화를 요청한다.) 프록시 객체를 호출하면 실제 객체의 메소드를 호출하게 된다.

프록시 객체는 처음 사용할 때 한 번만 초기화한다. 프록시 객체가 실제 엔티티로 바뀌는 게 아니고, 프록시 객체를 통해 실제 엔티티에 접근이 가능하게 되는 것이다. 원본 엔티티를 상속받기 때문에 타입 체크 시 주의해야 한다. (== 비교 대신 instance of를 사용해야 한다.) 

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

m1.getClass() == m2.getClass() // false => 타입 비교 시 상속 관계는 허용되지 않는다
m1 instanceof Member // true
m2 instanceof Member // true

영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다. (JPA에서는 한 영속성 컨텍스트 내에서(한 트랜잭션) PK가 같으면 == 비교 시 true를 반환한다.) 영속성 컨텍스트의 도움을 받을 수 없는 준연속 상태일 때, 프록시를 초기화하면 문제가 발생한다. (하이버네이트의 org.hibernate.LazyInitializationException 예외)

더보기
Member m1 = em.find(Member.class, member1.getId()); // 영속 상태 => 1차 캐시에 있음!
Member reference = em.getReference(Member.class, member1.getId()); // 실제 객체
Member m1 = em.getReference(Member.class, member1.getId()); // 프록시
Member reference = em.getReference(Member.class, member1.getId()); // 프록시
m1 == reference // true
Member refMember = em.getReference(Member.class, member1.getId()); // 프록시
Member findMember = em.find(Member.class, member1.getId()); // 프록시
refMember == findMember // JPA에서는 true를 보장하기 때문에 프록시 객체가 한 번 조회되면 이후 find를 했을 때 프록시 사용

 

🌕 프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인: emf.getPersistenceUnitUtil().isLoaded(member)
  • 프록시 클래스 확인: member.getClass()
  • 프록시 강제 초기화: [하이버네이트] Hibernate.initialize(member), [JPA] member.getName()

 


 

즉시 로딩과 지연 로딩

 

🌕 [Lazy] 지연 로딩: 프록시로 조회

  Member를 조회할 때 Team에 지연 로딩 설정이 되어 있으면 프록시로 가져온다. team은 실제 team을 사용하는 시점에 초기화된다. 비즈니스 로직 상 member에 있는 값만 이용할 때가 많을 때 사용한다.

// Member.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;

 

🌕 [EAGER] 즉시 로딩

  Member를 조회할 때 항상 Team도 조회한다. 비즈니스 로직에서 member와 team을 함께 이용하는 경우가 많을 때 사용한다. 실행하면 member와 team을 조인 해서 프록시 객체가 아닌 실제 객체를 가져온다. 이 경우 지연 로딩을 사용한다면 매번 쿼리가 두 번 날아가게 될 것이다.

// Member.java
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;

 

🌕 주의

가급적 지연 로딩만 사용하는 것이 좋다.

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. (특히 테이블이 복잡하면 거대해진다.)
  • JPQL에서 N + 1 문제를 일으킨다. (JPQL → SQL 번역(즉시 로딩 적용 X) → (현재 불필요해도) 추가로 필요한 쿼리 날림) 정 이용해야 한다면 JPQL fetch 조인이나, 엔티티 그래프 기능을 사용한다.
  • @ManyToOne@OneToOne은 기본이 즉시 로딩이므로 LAZY로 설정해야 한다.

 


 

영속성 전이(CASCADE)

 

  특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.

em.persist(parent); // persist 1번
em.persist(child1); // persist 2번
em.persist(child2); // persist 3번
  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제

CASCADE를 사용하지 않으면 위의 코드와 같이 하나씩 영속 상태로 만들어야 하는 번거로움이 생긴다. 아래와 같이 설정하면 perent를 persist()를 하면 그 아래의 child까지 함께 영속 상태로 만들어 준다. 두 엔티티의 라이프 사이클이 같을 때나 단일 소유자일 때 사용해야 한다.

// Parent.java
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
em.persist(parent);

 

🌕 고아 객체

  고아 객체 제거란 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것이다. 아래의 예시에서 perent가 childList를 관리하고 있다. 만약 childList에서 하나의 요소가 컬렉션에서 빠지면 삭제된다.

참조하는 곳이 하나일 때, 특정 엔티티가 개인 소유할 때 사용해야 한다. @OneToOne, @OneToMany만 가능하다. 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 이 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거되므로 CascadeType.Remove처럼 동작한다.

// Parent.java
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();

 

🌕 영속성 전이 + 고아 객체 → 생명 주기

CascadeType.ALL + orphanRemoval = true

  스스로 생명 주기를 관리하는 엔티티는 영속성 컨택스트를 통해 em.persist()로 영속화, em.remove()로 제거할 수 있다. 두 옵션을 모두 활성화하면 부모 엔티티를 통해 자식의 생명 주기를 관리할 수 있다. 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.

댓글