[JPA]프록시와 연관관계 관리
[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를 통해 생명주기를 관리한다는 개념(?))