본문 바로가기
JPA & Hibernate

JPA 프록시와 연관관계 관리

by Salt-Fn 2023. 3. 6.

인프런의 자바 ORA 표준 JPA 프로그래밍 - 기본편과 자바 ORM 표준 JPA 책을 공부하며 정리한 포스트입니다.

  • 프록시와 즉시로딩, 지연로딩: 객체는 객체 그래프로 탐색하지만 객체가 DB에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기 어려워 JPA구현체 들은 이 문제를 해결하기 위해 프록시라는 기술을 사용한다. 프록시를 사용하면 연관된 객체를 처음부터 DB에서 조회하는 것이 아니라, 실제 사용 시점에 DB에서 조회할 수 있다.
  • 영속성 전이와 고아 객체: JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 잇는 영속성 전이와 고아 객체 제거라는 편리한 기능 제공

프록시

엔티티를 조회할할 때 사용하지 않는 엔티티까지 데이터베이스에서 함께 조회해 두는 것은 효율적이지 않다. JPA는 이런 문제를 해결하기 위해 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다. 값을 실제 사용하는 시점에 데이터베이스에서 필요한 데이트를 조회한다. 지연로딩을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 잇는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.

프록시 기초

em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    이 메서드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

프록시 특징

  • 실제 클래스를 상속 받아서 만들어서 실제 클래스와 겉 모양이 같다. 따라서 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조를 보관하고 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

프록시 객체의 초기화

실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.

 

프록시 초기화 과정

  1. 프록시 객체의 get을 호출해서 실제 데이터를 조회한다.
  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
  3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 멤버변수에 보관한다.
  5. 프록시 객체는 실제 엔티티 객첵의 get을 호출해서 결과를 반환한다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라, 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음. 타입 체크시 == 대신 instance of 사용
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
  • 준영속 상태일 때, 프록시를 초기화 하면 org.hibernate.LazyInitializationException 예외 발생

프록시 확인

  • 프록시 인스턴스의 초기화 여부
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력
  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);
  • JPA 표준은 강제 초기화 없음
    강제 호출: entity.getName()

즉시 로딩과 지연 로딩

  • 즉시 로딩: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
    - 설정 방법: @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩: 연관된 엔티티를 실제 사용할 때 조회한다.
    - 설정 방법 @ManyToOne(fetch = FetchType.LAZY)

즉시 로딩

엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. 각각의 엔티티를 조회하므로 쿼리를 2번 실행할 것 같지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.

 

* 외래키가 null 값을 허용할 경우 inner join이 아닌 left outer join을 사용한다. @JoinColumn(nullable = false) 로 JPA에게 외래키가 null을 허용하지 않는다는 것을 알릴경우 inner join을 사용한다. nullable = true가 기본값이다.

지연 로딩

프록시 객체는 실제 사용될 때까지 데이터 로딩을 미뤄서 지연 로딩이라 한다. 이처럼 실제 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해서 프록시 객체를 초기화한다.

 

* 조회 대상이 영속성 컨텍스트에 이미 있으면 프록시 객체를 사용할 이유가 없으므로 실제 객체를 사용한다.

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용
  • 즉시 로딩을 적용하면 예상하지 못한 SQL 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩
    -> Lazy로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

지연 로딩 활용

* 멤버와 팀은 다대일, 멤버와 오더는 일대다, 오더와 프로덕트는 다대일의 관계다

  • 회원은 팀하나에만 소속할 수 있다. (N:1)
  • 회원은 여러 주문내역을 가진다. (1:N)
  • 주문내역은 상품정보를 가진다. (N:1)

로직 분석

  • 멤버와 연관된 팀은 자주 함께 사용되어 즉시 로딩으로 설정
  • 멤버와 연관된 오더는 가끔 사용되어 지연 로딩
  • 오더와 프로덕트는 자주 함께 사용되어 즉시 로딩

프록시와 컬렉션 래퍼

하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네티트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라 한다. 엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문 내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다. 컬렉션은 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화 한다.

ex) member.getOrders() 을 호출해도 컬렉션은 초기화 되지 않는다. member.getOrders().get(0) 처럼 실제 데이터 조회시 초기화한다.

FetchType.EAGER 설정과 조인 전략

  • @ManyToOne, @OneToOne
    - (optional = false): 내부 조인
    - (optional = true): 외부 조인
  • @OneToMany, @ManyToMany
    - (optional = false): 외부 조인
    - (optional = true): 외부 조인

영속성 전이: CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다. JPA는 CASCADE옵션으로 영속성 전이 기능을 제공한다.

 

영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없고 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.

 

영속성 전이는 엔티티를 삭제할 때도 사용할 수 있다. 삭제 순서는 외래 키 제약조건을 고려하여 자식을 먼저 삭제하고 부모를 삭제한다.

CASCADE의 종료

ALL(모두적용),PERSIST(영속),MERGE(병합),REMOVE(삭제),REFRESH,DETACH

*CascadeType.PERSIST, CascadeType.REMOVE는 em.persists(), em.remove()를 실행할 때 바로 전이가 발생하지 않고 플러시를 호출할 때 전이가 발생한다.

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 고아 객체(ORPHAN)제거라 한다. 이 기능을 활성화 시키려면 컬렉션에 orphanRemoval = true로 설정한다. 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유로 orphanRemovel은 @OneToOne, @OneToMany에만 사용할 수 있다.

 

'JPA & Hibernate' 카테고리의 다른 글

JPA 값 타입  (0) 2023.03.10
JPA 고급 매핑  (0) 2023.03.03
JPA 다양한 연관관계 매핑  (0) 2023.02.27
JPA 연관관계 매핑 기초  (0) 2023.02.23
JPA 엔티티 매핑  (0) 2023.02.22