DB/JPA

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

꾸준함. 2021. 9. 14. 02:19

개요

이번 게시글에서는 아래의 주제에 대해 알아보겠습니다.

  • 프록시
  • 즉시 로딩/지연 로딩
  • 영속성 전이: CASCADE
  • 고아 객체

 

1. 프록시

  • EntityManager의 find() 메서드는 DB를 통해서 실제 엔티티 객체를 조회하는 메서드
  • EntityManager의 getReference() 메서드는 DB 조회를 필요한 시점에 할 프록시 엔티티 객체를 조회하는 메서드 즉, getReference() 메서드가 호출되는 시점에는 쿼리가 실행되지 않음
  • 프록시 객체는 실제 객체의 참조를 보관
  • 프록시 객체를 호출 시 프록시 객체는 실제 객체의 메서드 호출

 

예제: 직원을 나타내는 클래스가 있고 해당 직원은 어떤 기업의 소속

 

 

설명을 위한 예제

 

 

  • printEmployeeAndCompany() 메서드를 호출할 경우 Employee 엔티티와 함께 Company 엔티티도 한 번에 조회해야 함
  • 반면, printEmployee() 메서드를 호출할 경우 굳이 Employee 엔티티 내 Company 엔티티까지 조회할 필요 없음
  • 어찌 되었든 위 예제처럼 EntityManager의 find() 메서드를 통해 엔티티를 조회하는 시점에서 쿼리가 실행돼 Employee와 Company를 모두 조회
    • printEmployee() 메서드만 호출할 경우 불필요하게 Company 엔티티도 조회하기 때문에 성능적으로 손해
    • 이를 위해 프록시와 지연 로딩이라는 개념이 나왔고 JPA에서는 getReference() 메서드를 제공함

 

 

find() vs getReference()

 

 

* 위 예제에서 getReference() 메서드 사용 시 바로 쿼리가 실행되는 것이 아니라 필요한 시점에 쿼리가 실행되는 것을 확인할 수 있음 (반면, find() 메서드 사용 시 바로 쿼리가 실행됨)

* 지연 로딩 관련해서는 추후 설명

 

 

1.1 프록시 객체 초기화 과정

앞서 프록시가 DB 조회가 필요한 시점에 쿼리를 실행하여 실제 객체의 참조를 보관한다는 것을 알 수 있었습니다.

이를 프록시 객체의 초기화라고 부르고 전체적인 과정은 아래와 같습니다.

 

Employee foundEmployee = entityManager.getReference(Employee.class, employee.getId());
foundEmployee.getName(); // 실제 DB 조회가 필요한 시점
  • getName() 메서드가 호출된 시점이 실제 DB 조회가 필요한 시점
  • 현재는 EmployeeProxy가 비어있으므로 PersistenceContext에 초기화 요청
  • PersistenceContext가 DB를 조회하여 실제 Employee 엔티티 생성
  • 실제 Employee 엔티티를 EmployeeProxy에 연결시켜줌
  • EmployeeProxy의 getName() 호출

 

 

1.2 프록시의 특징

  • 프록시 초기화 과정을 보면 알 수 있다시피 프록시 객체는 처음 사용할 때 한 번만 초기화 (단, PersistenceContext flush, clear 할 경우 다시 초기화해야 함)
  • 프록시 객체를 초기화 시 프록시 객체를 통해서 실제 엔티티에 접근 가능 (프록시 객체가 엔티티로 바뀌는 것이 아님)
  • 프록시 객체는 원본 엔티티를 상속을 받음
    • 따라서, 타입 체크를 할 경우 == 비교 대신 instanceof 키워드를 통해 타입 비교를 해야 함
    • 아래 예제를 통해 설명할 예정
  • PersistenceContext에 이미 찾는 엔티티가 올라와 있을 경우 getReference() 메서드를 호출하더라도 프록시가 반환되는 것이 아니라 실제 엔티티 반환
    • 반면, 프록시로 먼저 조회할 경우 그 이후 find() 메서드를 통해 조회를 하더라도 프록시를 반환함 
    • 영속 상태일 때는 프록시와 엔티티 == 비교해도 동일
    • JPA에서는 같은 트랜잭션 내 즉, 같은 영속성 컨텍스트 내에서는 ==이 동일함을 보장
  • PersistenceContext를 flush, clear 한 상태에서 프록시 초기화 시도할 경우 LazyInitializationException 예외가 발생
    • 예제를 통해 설명할 예정

 

타입 체크를 instanceof로 해야 하는 이유를 설명하는 예제

 

 

영속 상태일 때, 실제 엔티티와 proxy 비교하는 예제

 

 

* 정리를 하자면 프록시와 엔티티를 먼저 조회한 쪽이 그 이후에도 조회가 되므로 개발을 진행할 때 반환되는 객체가 프록시든 엔티티든 상관없도록 개발하는 것이 중요 (예외처리를 잘하자)

 

LazyInitializationException 예외 발생하는 예제

 

 

1.3 프록시 관련 유용한 유틸 메서드

  • PersistenceUnitUtil.isLoaded(Object entity): 프록시 인스턴스의 초기화 여부 확인하는 메서드
  • org.hibernate.Hibernate.initialize(entity): 프록시 강제로 초기화하는 메서드

 

 

* JPA 공식 표준에는 강제 초기화가 없으므로 hibernate를 사용하지 않는 환경에서는 프록시의 메서드 아무거나 실행하는 방식으로 강제 초기화 진행 가능

 

2. 즉시 로딩/지연 로딩

 

2.1 지연 로딩 (Lazy Loading)

  • 앞선 예제에서 printEmployee() 메서드를 호출할 때는 Employee를 조회할 때 굳이 Employee 엔티티 내 Company 엔티티까지 조회할 필요 없음
  • 따라서, 성능을 극대화하기 위해서는 company 엔티티가 사용되는 시점에 쿼리를 실행하여 DB를 조회하는 것이 좋음
  • 지연 로딩은 프록시로 조회할 때, 위의 내용을 적용한 내용

 

엔티티 정의

 

 

LazyLoading 예제

 

 

2.2 즉시 로딩 (Eager Loading)

  • 앞선 예제에서 pringEmployeeAndCompany() 메서드를 호출할 때처럼 Employee를 조회할 때 Company도 같이 조회하는 빈도수가 높을 경우 즉시 로딩 사용
  • 즉시 로딩을 사용할 경우 JPA가 조인을 통해 SQL 쿼리를 실행해 한번에 조회
  • 하지만, 실무에서는 즉시 로딩은 가급적 사용하지 않아야 함

 

엔티티 정의

 

 

EagerLoading 예제

 

 

2.3 실무에서 지연 로딩만 사용해야 하는 이유

  • 실무에서 즉시 로딩 즉, Eager Loading 가급적 사용 X
  • Eager Loading은 JPQL에서 N+1 문제를 야기
    • 간단히 요약하자면 추가 쿼리가 나가는 문제
    • 실무에서는 하나의 엔티티에 다양한 연관관계가 존재할 텐데 이를 모두 JOIN문으로 조회 시 성능도 안 나올뿐더러 쿼리가 너무 많이 나감
    • Lazy Loading으로 방지 가능
  • @ManyToOne, @OneToOne 연관관계는 디폴트가 즉시 로딩이기 때문에 LAZY로 바꿔줘야 함
  • 정리를 하자면, 모든 연관관계를 LAZY로 깔고 가고, 같이 조회하는 케이스가 더 많은 관계에 대해서만 JPQL의 fetchJoin을 사용해서 조회 시 같이 조회하도록 설정
    • 혹은, JPQL의 엔티티 그래프 기능도 사용 가능

 

3. 영속성 전이: CASCADE

  • DDL 내 CASCADE 키워드와 동일한 개념
  • 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 적용하는 키워드
    • Employee 엔티티 저장할 때 Company 엔티티도 함께 저장
    • 이럴 경우 EntityManager의 persist 호출 횟수를 줄일 수 있음
  • 영속성 전이는 편리함을 제공할 뿐 연관관계 매핑과 전혀 무관한 개념
  • 연관관계 내 하나의 부모 엔티티가 있을 경우에만 사용해야 함
    • 부모 엔티티가 여러 개 있는 관계에서 사용하면 안 됨 

 

3.1 Cascade 종류

  • ALL: 모두 적용
  • PERSIST: 영속 (저장할 때만)
  • REMOVE: 삭제 (매우 위험)
  • MERGE: 병합
  • REFRESH
  • DETACH

 

* Cascade를 적용할 경우 PERSIST에만 적용할 것을 추천

 

Parent, Child 객체


 

예제


 

4. 고아 객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라 부름
  • orphanRemoval = true로 설정 시 고아 객체를 자동으로 제거
    • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 추정하고 삭제하는 기능이기 때문에 참조하는 곳이 하나일 때만 사용
    • 즉, 특정 엔티티가 개인 소유할 때만 고아 객체 자동 제거 기능 적용
    • @OneToOne, @OneToMany 연관관계에서 사용 가능
  • 개념적으로 봤을 때, orphanRemoval = true를 활성화했을 때 부모 객체가 삭제될 경우 자식 객체도 삭제되므로 CacadeType.REMOVE처럼 동작
    • 위험한 기능이므로 잘 사용해야 한다는 뜻
    • CascadeType.ALL 혹은 CascadeType.REMOVE 적용 시 orphanRemoval = true로 활성화시키지 않더라도 동일하게 작동

 

Parent, Child 객체

 

 

예제

 

 

* 위 예시처럼 CascadeType.PERSIST와 orphanRemoval = true 기능 둘 다 활성화할 경우 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음

* 이는 DDD의 Aggregate Root 개념을 구현할 때 사용됨

 

출처

자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)

반응형

'DB > JPA' 카테고리의 다른 글

[JPA] JPQL 간단 정리  (0) 2021.10.16
[JPA] 값 타입 정리  (0) 2021.09.29
[JPA] @MappedSuperclass  (0) 2021.09.07
[JPA] 상속관계 매핑  (2) 2021.09.07
[JPA] 다양한 연관관계 매핑  (0) 2021.08.31