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

김영한 님의 ‘자바 ORM 표준 JPA 프로그래밍 - 기본편’ 강의 中

프록시

=> 프록시란 가짜 객체 (실제 객체를 감싸고 있는 껍데기라고 이해하자)

  • em.find(Member.class, memberId) // 실제 객체 조회
  • em.getReference(Member.class, memberId) // 가짜(프록시) 엔티티 객체 조회


프록시 동작 원리

프록시 특징

  • 프록시 객체는 첫 사용 시 한 번만 초기화
  • 초기화 시 프록시 객체가 실제 엔티티가 되는 것은 아니다. 프록시 객체 통해서 실제 엔티티에 접근이 가능한 것
  • 프록시 객체는 원본 엔티티를 상속 받으므로 타입 체크 시 주의해야 한다(“==” 사용이 아닌 instance of 사용할 것)
  • 영속성 컨텍스트에 엔티티가 이미 있다면 em.getReference() 호출 시 실제 엔티티 반환 ( JPA 고유 특징 )
  • 준영속 상태일 때는 프록시를 초기화하면 문제 발생(하이버네이트 : org.hibernate.LazyInitializationException 예외 터뜨린다)


*JPA는 같은 트랜잭션, 영속성 컨텍스트에 속해있는 동일한 PK를 가진 객체들은 타입이 같아야 한다. (하나의 트랜잭션 안에서 동일함을 보장해준다.)
아래 코드를 보자.

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass()); // Proxy

System.out.println("refMember == findMember: " + (refMember == findMember)) // JPA 특성상 true를 보장
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass()); // Member

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Member

System.out.println("refMember == findMember: " + (refMember == findMember)) // JPA 특성상 true를 보장


프록시 확인 기능

  • 프록시 인스턴스 초기화 여부 확인
    emf.PersistenceUnitUtil.isLoaded(Object entity) (EntityManagerFactory)

  • 프록시 강제 초기화
    Hibernate.initialize(entity)

  • * JPA 표준은 강제 초기화가 없다(Hibernate 제공) : entity.getName() 등과 같이 사용할 것

즉시 로딩과 지연 로딩

*가급적 지연 로딩만 사용하자!


이유

  • 즉시 로딩 시 예상치 못한 SQL 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킴.
    (최초 쿼리 1에 결과값 N개 만큼 추가 쿼리가 나간다는 의미) em.find()는 PK를 명시하므로 내부적으로 최적화하지만, JPQL의 경우는 아님
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩


어플리케이션에서 보통 Member와 Team을 따로 조회한다면?

=> 지연 로딩 : 연관관계 객체를 프록시로서 조회함

@ManyToOne(fetch=FetchType.LAZY)

Member 조회 SQL은 먼저 나가고,
프록시를 초기화할 때 Team 조회 SQL이 또 나가서 성능 저하 단점이 있을 수 있음.
LAZY로 설정 시 실행 쿼리에 따라서 Member, Team을 한번에 가져오고 싶을 땐 fetch join을 사용!(그 외 방법으로는 @EntityGraph, @BatchSize를 이용한 방법 등이 있음)

어플리케이션에서 보통 Member와 Team을 같이 조회한다면?

=> 즉시 로딩

즉시 로딩의 경우, 한번에 Member와 Team을 조인한 SQL을 날리는 방법과 em.find() 호출 시 Member, Team 조회 SQL을 각각 한 번씩 날리는 방법이 있다.
대부분의 구현체들은 전자를 채택함.

@ManyToOne(fetch=FetchType.EAGER)

Member 조회 시 Team 조인해서 한 번에 SQL 조회문을 날림

영속성 전이: CASCADE

: 특정 엔티티를 영속 사앹로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용하는 기능

주의사항

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음

종류

  • ALL : 모두 적용
  • PERSIST : 영속 (저장할 때만 LifeCycle을 맞추겠다)
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

*운영적인 측면에서 단일 엔티티에 종속적일 때나 라이프 사이클이 거의 유사할 때는 사용해도 좋지만, 아닌 경우에는 사용하지 않는게 좋다.

고아 객체

: 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능

@OneToMany(orphanRemoval = true)

주의 사항

  • 참조하는 곳이 하나일 때만
  • 특정 엔티티가 개인 소유일 때 사용
  • CascadeType.REMOVE처럼 동작한다.

CascadeType.ALL + orphanRemoval=true

  • 두 옵션 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용함
    (Repository는 Aggregate Root만 컨텍하고 나머지는 만들지 않는 것이 낫다, Aggregate Root를 통해 생명주기를 관리한다는 개념(?))