[JPA] Chapter 8. 프록시와 연관관계 관리

joyful·2021년 8월 9일
0

JPA

목록 보기
12/18

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


8.1 프록시

  • 엔티티 조회 시 연관된 엔티티들이 항상 사용되지는 않음
    → 이를 해결하기 위해 사용하는 방법 "지연 로딩"

    💡 지연로딩

    엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법

  • 프록시 객체 : 지연 로딩 기능을 사용하기 위해 사용하는 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체

📕 참고

하이버네이트에서는 지연 로딩을 지원하기 위한 방법으로 프록시 사용바이트코드 수정 두 가지 존재


8.1.1 프록시 기초

  • EntityManager.find()

    • JPA에서 식별자로 엔티티 하나 조회 시 사용하는 메소드
    • 영속성 컨텍스트에 엔티티 존재하지 않을 경우 데이터베이스 조회
    • 조회한 엔티티의 실제 사용 여부와는 관계 없이 데이터베이스 조회
  • EntityManager.getReferences()

    • 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미룸
    • 호출 시 JPA가 데이터베이스 조회 및 실제 엔티티 객체 생성 x
    • 데이터베이스 접근을 위임한 프록시 객체 반환

✅ 프록시 객체의 초기화

프록시 객체가 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는 것을 의미

  • 과정
    1. 프록시 객체에 xxx.getxxx()을 호출해서 실제 데이터를 조회
    2. 실제 엔티티가 생성되어 있지 않은 경우 영속성 컨텍스트에 실제 엔티티 생성 요청(프록시 객체 초기화)
    3. 영속성 컨텍스트가 데이터베이스를 조회해서 실제 엔티티 객체 생성
    4. 프록시 객체가 생성된 실제 엔티티 객체의 참조를 멤버변수에 보관
    5. 프록시 객체가 실제 엔티티 객체의 getxxx()을 호출해서 결과 반환

✅ 프록시의 특징

  • 구조

    • 프록시 클래스는 실제 클래스를 상속 받아 만들어짐
      → 실제 클래스와 겉 모양이 같음
      ∴ 사용하는 입장에서 어떤 객체인지 구분하지 않고 사용하면 됨
  • 위임(delegate)

    • 프록시 객체는 실제 객체에 대한 참조(target)를 보관
    • 프록시 객체의 메소드 호출 시 프록시 객체는 실제 객체의 메소드 호출
  • 프록시 객체는 처음 사용할 때 한 번만 초기화 됨

  • 프록시 객체를 초기화하면 프록시 객체를 통해 실제 엔티티에 접근 가능

  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 함

  • 영속성 컨텍스트에 찾는 엔티티가 이미 존재할 경우 em.getReference()를 호출해도 실제 엔티티를 반환함
    why? 데이터베이스를 조회할 필요가 없기 때문

✅ 준영속 상태와 초기화

초기화는 영속성 컨텍스트의 도움을 받아야 가능하므로, 준영속 상태의 프록시를 초기화하면 문제가 발생한다.

//MemberProxy 반환
Member member = em.getReferencce(Member.class, "id1");
transaction.commit();
em.close(); //영속성 컨텍스트 종료

member.getName();  //준영속 상태 초기화 시도,
		   //org.hibernate.LazyInitializationException 예외 발생      

8.1.2 프록시와 식별자

  • 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하며, 프록시 객체는 이 식별자 값을 보관함
  • 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우를 제외하고는 프록시를 초기화 함
  • 연관관계 설정
    • 식별자 값만 사용하므로 프록시 사용 시 데이터베이스 접근 횟수를 줄일 수 있음
    • 엔티티 접근 방식을 필드로 설정해도 프록시를 초기화하지 않음

8.1.3 프록시 확인

  • PersistenceUnitUtil.isLoaded(Object entity)

    프록시 인스턴스의 초기화 여부 확인

    • 아직 초기화되지 않은 경우 → false 반환
    • 이미 초기화되었거나 프록시 인스턴스가 아닌 경우 → true 반환
  • 클래스 명 출력

    • 조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인 가능
    • 클래스명 뒤에 ..javassist..가 붙어있으면 프록시임을 확인 가능

📕 참고

  • 하이버네이트의 initialize() 메소드를 사용하여 프록시 강제 초기화 가능
    org.hibernate.Hibernate.initialize(order.getMember());
  • JPA 표준
    • 프록시 강제 초기화 메소드 존재 x
    • 초기화 여부만 확인 가능
    • 프록시의 메소드 직접 호출


8.2 즉시 로딩과 지연 로딩

  • 프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용
  • JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 방법 제공

8.2.1 즉시 로딩(EAGER LOADING)

  • 엔티티를 조회할 때 연관된 엔티티도 함께 조회
    ex) em.find(Member.class, "member1")를 호출할 때, 회원 엔티티와 연관된 팀 엔티티도 함께 조회
  • 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
  • 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용함
    → 두 테이블을 조인해서 쿼리 한 번으로 두 엔티티를 모두 조회

📕 NULL 제약조건과 JPA 조인 전략

  • 외래 키가 NULL 값을 허용할 때 발생하는 문제를 고려하여 JPA는 외부 조인을 사용
  • 성능과 최적화 면에서는 외부 조인보다 내부 조인이 더 유리
  • 내부 조인 사용 방법
    • 테이블 : 외래 키에 NOT NULL 제약 조건 설정 → 값 존재 보장
    • JPA : @JoinColumnnullable = false 설정

📕 nullable 설정에 따른 조인 전략

  • @JoinColumn(nullable = true)
    • NULL 허용(기본값)
    • 외부 조인 사용
  • @JoinColumn(nullable = false)
    • NULL 허용 x
    • 내부 조인 사용
      @ManyToOne(optional = false)로도 가능

JPA는 선택적 관계면 외부 조인을 사용하고 필수 관계면 내부 조인을 사용함


8.2.2 지연 로딩(LAZY LOADING)

  • 연관된 엔티티를 실제 사용할 때 조회
    ex) member.getTeam().getName()처럼 조회한 팀 엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티 조회
  • 설정 방법 : @ManyToOne(fetch = FetchType.LAZY)
  • 같이 조회한 엔티티의 멤버변수에 프록시 객체를 넣어둠
    → 실제 사용될 때까지 데이터 로딩을 미룸
  • 실제 데이터가 필요할 때 데이터베이스를 조회해서 프록시 객체 초기화

8.2.3 즉시 로딩, 지연 로딩 정리

  • 지연 로딩(LAZY)
    • 연관된 엔티티를 프록시로 조회
    • 프록시를 실제 사용할 때 초기화하면서 데이터베이스 조회
  • 즉시 로딩(EAGER)
    • 연관된 엔티티 즉시 조회
    • 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회
  • 연관된 엔티티를 로딩하는 시점을 결정하는 것은 상황에 따라 다름


8.3 지연 로딩 활용

8.3.1 프록시와 컬렉션 래퍼

  • 컬렉션 래퍼 : 하이버네이트가 엔티티영속 상태로 만들 때, 엔티티에 컬렉션존재하면 컬렉션을 추적 및 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는 것

    💡 내장 컬렉션

    org.hibernate.collection.internal.PersistentBag

  • 지연 로딩
    • 엔티티 → 프록시 객체를 통해 수행
    • 컬렉션 → 컬렉션 래퍼가 처리
  • xxx.getxxx()을 호출해도 컬렉션은 초기화되지 않음
    → 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화
    ex) member.getOrders().get(0)

8.3.2 JPA 기본 페치 전략

  • fetch 속성의 기본 설정값

    • @ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)
    • @OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)
  • JPA의 기본 fetch 전략

    • 연관된 엔티티가 하나인 경우 : 즉시 로딩
    • 연관된 엔티티가 컬렉션인 경우 : 지연 로딩
      → 컬렉션 로딩은 큰 비용을 수반하기 때문
      ex) 특정 회원이 연관된 컬렉션에 데이터를 수만 건 등록
      → 즉시 로딩일 경우 해당 회원을 로딩하는 순간 수만 건의 데이터도 함께 로딩
  • 모든 연관관계에 지연 로딩을 사용하는 것을 권장

  • 애플리케이션 개발이 어느 정도 완료단계에 도달했을 때, 실제 사용 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화


8.3.3 컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
    • 컬렉션과 조인 = 일대다 조인
    • 일대다 조인은 결과 데이터가 '다' 쪽에 있는 수만큼 증가
    • 서로 다른 컬렉션을 2개 이상 조인 시 애플리케이션 성능이 저하될 수 있음
      ex) A 테이블을 N, M 두 테이블과 일대다 조인
      → SQL 실행 결과가 N x M이 되면서 너무 많은 데이터 반환
    • JPA는 조회된 결과를 메모리에서 필터링해서 반환
  • 컬렉션 즉시 로딩은 항상 외부 조인(OUTER JOIN)을 사용한다.
    • 예를 들어, 팀 테이블에서 회원 테이블로 일대다 관계를 조인할 때, 회원이 한 명도 없는 팀을 내부 조인하면 팀까지 조회되지 않는 문제가 발생함
    • 데이터베이스 제약조건으로 이러한 문제를 예방할 수 없음

📕 FetchType.EAGER 설정과 조인 전략

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


8.4 영속성 전이: CASCADE

  • 영속성 전이(Transitive persistence) : 특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 경우 사용하는 기능
  • JPA는 CASCADE 옵션으로 영속성 전이 제공
  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 함
    → 부모 엔티티만 영속 상태로 만들면 연관된 자식 엔티티까지 한 번에 영속 상태로 만들 수 있음

8.4.1 영속성 전이: 저장

@Enity
public class Parent {
    ...
    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<Child>();
}
  • 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련 x
  • 엔티티를 영속화할 대 연관된 엔티티도 같이 영속화하는 편리함 제공

8.4.2 영속성 전이: 삭제

  • CascadeType.REMOVE를 설정하면 부모 엔티티와 자식 엔티티 함께 제거 가능
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
  • 외래 키 제약조건을 고려하여 자식을 먼저 삭제하고 부모를 삭제

8.4.3 CASCADE의 종류

💻 CascadeType 코드

public enum CascadeType {
    ALL,  //모두 적용
    PERSIST,  //영속
    MERGE,  //병합
    REMOVE,  //삭제
    REFRESH,  //REFRESH
    DETACH  //DETACH
}
  • 여러 속성을 같이 사용할 수 있음
    ex) cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
  • CascadeType.PERSISTCascadeType.REMOVE는 플러시를 호출할 때 전이 발생


8.5 고아 객체(ORPAHN)

  • 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
  • 부모 엔티티의 컬렉션에서 자식 엔티티의 참조 제거하면 자식 엔티티가 자동으로 삭제

💻 고아 객체 제거 기능 설정

@Entity
public class Parent {

    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<Child>();
    ...
}
  • 플러시 시점에 DELETE SQL 실행
  • 모든 자식 엔티티를 제거하려면 컬렉션을 비우면 됨
    ex) parent1.getChildren().clear();
  • orpahnRemoval@OneToOne, @OneToMany에서만 사용 가능
    • 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있음
      → 고아 객체 제거는 참조하는 곳이 하나일 때만 사용해야 함
  • 개념적으로 볼 때 부모를 제거하면 자식은 고아가 됨
    → 부모를 제거하면 자식도 같이 제거 됨
    CascadeType.REMOVE를 설정한 것과 같음


8.6 영속성 전이 + 고아 객체, 생명주기

  • 일반적으로 엔티티는 엔티티 스스로 생명주기를 관리함
    • EntityManager.persist()를 통해 영속화
    • EntityManager.remove()를 통해 제거
  • CascadeType.ALL + orphanRemoval = true 동시 사용
    → 부모 엔티티를 통해 자식의 생명주기 관리 가능

💻 예시

//자식을 저장하려면 부모에 등록만 하면 된다(CASCADE)
Paraent parent = em.find(Parent.class, parentId);
parent.addChild(child);

//자식을 삭제하려면 부모에서 제거하면 된다(orphanRemoval)
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);

📕 참고

영속성 전이는 DDD의 Aggregate Root 개념을 구현할 때 사용하면 편리하다.



8.7 정리

  • JPA 구현체들은 객체 그래프를 마음껏 탐색할 수 있도록 프록시 기술을 지원한다.
  • 객체를 조회할 때 연관된 객체를 즉시 로딩하는 방법을 즉시 로딩, 연관된 객체를 지연해서 로딩하는 방법을 지연 로딩이라 한다.
  • 영속성 전이를 통해 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수 있다.
  • 고아 객체 제거 기능을 사용하여 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있다.
profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글