[JPA] 스터디 2주차 - 8장 프록시와 연관관계 관리

JPA 스터디 : 8장 프록시와 연관관계 관리

2주차 연관관계와 매핑

  • 8장 프록시와 연관관계 관리 : 288p-313p

8장. 프록시와 연관관계 관리

8.1 프록시

지연로딩을 사용하기 위해 실제 엔티티 객체 대신 프록시 객체를 사용

  • 지연 로딩 : 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연

8.1.1 프록시 기초

EntityManager.getReference() : 엔티티를 실제 사용하는 시점까지 DB 조회 지연

  • DB 접근을 위한 프록시 객체 반환

프록시 객체

  • 상속 : 프록시 클래스는 실제 클래스를 상속
  • 위임 : 프록시 객체는 실제 객체에 대한 참조 보관. 프록시 객체의 메소드 호출 시 프록시 객체는 실제 객체의 메소드를 호출
  • 프록시 객체 초기화 : 실제 사용 때 DB에서 조회 후 실제 객체 생성하는 것을 프록시 객체 초기화
// MemberProxy 반환
Member member = em.getReference(Member.class, "member1");
member.getName(); // 1. getName();
class MamberProxy extends Member {

    Member target = null; // 실제 엔티티 참조
    
    public String getName() {
        if (target == null) {
            // 2. 초기화 요청
            // 3. DB 조회
            // 4. 실제 엔티티 생성 및 참조 보관
            this.target = ...;
        }

        // 5. target.getName()
        return target.getName();
    }
}

proxy

프록시 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 교체되는 것 X.
  • 프록시 객체는 원본 엔티티를 상속. 타입 체크 시에 주의해서 사용 필요
    • getClass로 타입 비교 시 문제 발생 -> instanceof
  • 영속성 컨텍스트에 찾는 엔티티가 이미 존재 시 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티 반환
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능.
    • 준영속 상태 프록시 초기화 시 문제 발생 : org.hibernate.LazyInitializationException 발생
        // MemberProxy 반환
        Member member = em.getReference(Member.class, "id1");
        transaction.commit();
        em.close(); // 영속성 컨텍스트 종료
      
        member.getName(); // 준영속 상태 초기화 시도 -> org.hibernate.LazyInitializationException 발생
      

8.1.2 프록시 & 식별자

  • 엔티티를 프록시로 조회 시 식별자 값을 파라미터로 전달. 프록시 객체는 식별자 값을 보관
    • -> 식별자 값을 조회하는 .getId() 호출해도 프록시 초기화 X
    • 단, 엔티티 접근 방식을 프로퍼티 @Access(AccessType.PROPERTY) 설정한 경우만
    • 필드 @Access(AccessType.FIELD) 방식의 경우 프록시 객체 초기화
  • 연관관계 설정 시 엔티티 접근 방식이 필드여도 프록시 초기화 X

8.1.3 프록시 확인

PersistenceUnitUtil.isLoaded(Object entity)

  • 프록시 인스턴스 초기화 여부 확인 가능
  • 클래스명에 ..javassist.. 라고 되어 있으면 프록시로 조회 의미

8.2 즉시 로딩과 지연 로딩

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

8.2.1 즉시 로딩

@Entity
public class Member {
    //..
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    //..
}
  • FetchType.EAGER
  • 즉시 로딩을 처리하기 위해 가능하면 조인 쿼리 사용
  • INNER JOIN vs OUTER JOIN
    • 외래 키 null 가능할 경우 기본적으로 OUTER JOIN 사용
      • @JoinColumn(nullable = true)
    • INNER JOIN 사용하고 싶은 경우 : not null 조건 추가
      • @JoinCloumn(nullable = false)
      • 또는 @ManyToOne(optional = false) 적용

8.2.2 지연 로딩

  • FetchType.LAZY
  • 실제 데이터가 필요한 순간에 데이터베이스 조회 -> 프록시 객체 초기화

8.3 지연 로딩 활용

  • 함께 자주 사용 되는 경우 : 즉시 로딩 / 그외 : 지연 로딩
@Entity
public class Member {
    @Id
    private String id;
    //..

    @ManyToOne(fetch = FetchType.EAGER)
    private Team team;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders;
    
}
SELECT ...
FROM 
    MEMBER MEMBER0_
LEFT OUTER JOIN 
    TEAM TEAM1_ ON MEMBER0_.TEAM_ID=TEAM1_.ID
WHERE
    MEMBER0_.ID="member1"

8.3.1 컬렉션 래퍼

  • 엔티티에 컬렉션 존재 시 원본 컬렉션을 하이버네이트에서 제공하는 내장 컬렉션 (컬렉션 래퍼)으로 변경
    • org.hibernate.collection.internal.PersistentBag
  • member.getOrders() 호출해도 컬렉션 초기화 X
    • member.getOrders().get(0) 처럼 실제 데이터를 조회할 때 초기화

8.3.2 JPA 기본 페지 전략

  • 즉시 로딩 : @ManyToOne, @OneToOne
  • 지연 로딩 : @OneToMany, @ManyToMany
  • 모든 연관관계에 지연 로딩 사용 추천 -> 실제 사용 검토 후 꼭 필요한 곳에만 즉시 로딩 사용

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

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장 X
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용
  • FetchType.EAGER 설정 & 조인 전략
    • @ManyToOne, @OneToOne
      • (optional = false) : INNER JOIN
      • (optional = true) : OUTER JOIN
    • @OneToMany, @ManyToMany
      • (optional = false) : OUTER JOIN
      • (optional = true) : OUTER JOIN

8.4 영속성 전이 : CASCADE

  • 영속성 전이를 사용하는 경우 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장 가능
  • JPA에서 엔티티 저장 시 연관된 모든 엔티티는 영속 상태여야 함

8.4.1 영속성 전이: 저장

@Entity
public class Parent {
    ...
    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
    ...
}
private static void saveWithCascade(EntityManager em) {
    
    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);

    // 부모 저장, 연관된 자식들 저장
    em.persist(parent);
}

8.4.2 영속성 전이: 삭제

  • CascadeType.REMOVE
  • 외래 키 제약 조건 고려 자식 삭제 -> 부모 삭제 순서
  • CascadeType.REMOVE 설정 X, 부모 삭제 시
    • 외래 키 제약 조건으로 DB 예외 발생

8.4.3 CASCADE 종료

public enum CascadeType {
    ALL,  // 모두 적용
    PERSIST, // 영속
    MERGE,  // 병합
    REMOVE, // 삭제
    REFRESH, // REFRESH
    DETACH // DETACH
}
  • 여러 속성 함께 사용 가능
    • cascade = {CascadeType.PERSIST, CascadeType.REMOVE}ㄴ
  • flush 호출할 때 전이 발생

8.5 고아 객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제됨
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}
  • orphanRemoval = true

      Parent parent1 = em.find(Parent.class, id);
      parent1.getChildren().remove(0);
    
      DELETE FROM CHILD WHERE ID=?
    
  • 참조하는 곳이 하나일 때만 사용 가능
    • @OneTonOne, @OneToMany 에서만 사용 가능
  • 부모 객체 제거 시 자식 객체도 함께 제거
    • CascadeType.REMOVE 와 동일

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

CascadeType.ALL + orphanRemoval = true 동시 사용

  • 부모 엔티티를 통해서 자식의 생명주기 관기 가능
  • DDD의 Aggregate Root 개념 구현 시 편의성 제공

참조

  • 자바 ORM 표준 JPA 프로그래밍, 김영한 지음