이번 포스팅에서 다룰 내용입니다.

  • 프록시와 즉시로딩, 지연 로딩
  • 영속성 전이와 고아 객체

8.1 프록시

엔티티가 실제 사용될 때 까지 데이터베이스 조회를 지연하는 방법을 지연 로딩이라고 합니다.
지연 로딩 기능을 사용하려면 실제 엔티티 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이것을 프록시 객체라고 합니다.

8.1.1 프록시 기초

JPA 에서 식별자로 엔티티 하나를 조회할 때는 EntityManager.find() 를 사용합니다. 이 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회합니다.

엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManager.getReference()를 사용하면 됩니다. 이 메소드를 호출하면, 실제 엔티티 객체를 생성하지 않고 데이터베이스 접근을 위임한 프록시 객체를 반환합니다.

  • 프록시의 특징
    • 프록시 객체는 실제 객체에 대한 참조(target) 을 보관합니다.
    • 프록시 객체의 메소드를 호출하면, 프록시 객체는 실제 객체의 메소드를 호출합니다.
    • 영속성 컨텍스트에 이미 찾는 엔티티가 있으면, 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 실제 엔티티를 반환합니다.
  • 프록시 객체의 초기화

    • 프록시 객체의 초기화란, member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티를 생성하는 것을 말합니다.
    • 과정
      • member.getName() 호출해서 실제 데이터 조회합니다.
      • 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성합니다.
      • 프록시 객체는 생성된 실제 엔티티 객체의 참조를 MemerProxy 의 멤버 변수로 보관합ㄴ디ㅏ.
      • 프록시 객체는 실제 엔티티 객체의 getName() 을 호출해서 결과를 반환합니다.
  • 준영속 상태의 초기화

    • 초기화는 영속성 컨텍스트의 도움을 받아야 가능합니다.
    • 준영속 상태의 프록시를 초기화하면 문제가 발생합니다.
    • Hibernate : LazyInitializationException 발생
8.1.2 프록시와 식별자

엔티티를 프록시로 조회할 때 식별자 값을 Parameter로 전달하는데 프록시 객체는 이 식별자 값을 보관합니다.구분 칼럼을 꼭 사용해야합니다.

으음..

8.1.3 프록시 확인

JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있습니다. 조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하려면, 클래스명을 직접 출력해보면 됩니다.

8.2 즉시 로딩과 지연 로딩

프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용합니다.

연관된 엔티티의 조회 시점을 선택할 수 있는 두 가지 방법이 있습니다.

  • 즉시로딩
  • 지연 로딩
8.2.1 즉시 로딩
1
em.find(Member.class, "member1")

로 회원을 조회하는 순간 팀도 함께 조회합니다. 즉시 로딩을 최적화하기 위해 가능하면 조인쿼리를 사용합니다.

8.2.2 지연 로딩

위의 코드를 실행하면, 회원만 조회되고 팀은 조회되지 않습니다. 대신에 조회한 회원의 team 맴버 변수에 프록시 객체를 넣어둡니다.

1
Team team = member.getTeam() // 프록시 객체

반환된 팀 객체는 프록시 객체로, 실제 사용될때 까지 데이터 로딩을 미룹니다. 실제 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해서 프록시 객체를 초기화합니다.

1
team.getName()

을 호출하면, 프록시 객체가 초기화 됩니다.

8.3 지연 로딩 활용

8.3.1 프록시와 컬렉션 래퍼

Hibernate 는 엔티티를 영속 상태로 만들 때, 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로, 원본 컬렉션을 Hibernate가 제공하는 내장 컬렉션으로 변경합니다. 이를 컬렉션 래퍼라고 합니다.

엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연로딩을 수행하지만, 주문 내역과 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해줍니다. 즉, 컬렉션 레퍼도 프록시 역할을 합니다.

member.getOrder()를 호출해도 컬렉션은 초기화되지 않습니다. member.getOrders().get(0) 처럼 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화합니다.

8.3.2 JPA 기본 Fetch 전략
  • @ManyToOne, @OneToOne
    • FetchType.EAGER
  • @OneToMany, @ManyToMany
    • FetchType.LAZY

추천하는 방버은 모든 연관관계에 지연 로딩을 사용하는 것입니다. 그리고, 실제 사용하는 상황을 보고 꼭 필요한 곳에 즉시 로딩을 사용하도록 최적화 하면 됩니다.

8.3.3 컬렉션에 FetchType.EAGER 사용시 주의점
  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않습니다.
  • 컬렉션 즉시 로딩은 항상 OUTER JOIN 을 사용합니다.

8.4 영속성 전이 : CASCADE

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

8.4.1 저장

부모만 영속화하면 CascadeType.PERSIST 로 설정한 자식 엔티티까지 함께 영속화해서 저장합니다.

8.4.2 삭제

CascadeType.REMOVE 설정하고 부모 엔티티만 삭제하면 연관된 자식 엔티티까지 함께 삭제됩니다.

8.5 고아 객체

JPA는 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데, 이를 고아 객체 제거라고 합니다. 이 기능은 참조하는 곳이 하나 일때만 사용해야합니다. 즉, 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야합니다.

다음 코드는, 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제됩니다.

1
2
3
4
5
6
7
@Entity
public class Paren {
...
@OneToMany(mappedBy="parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
...
}

다음 코드처럼,

1
2
Parent parent = em.find(Parent.class, id);
parent.getChildren.remove(0); // 자식 엔티티를 컬렉션에서 제거

컬렉션에서 첫 번째 자식을 제거하면, 데이터베잇의 데이터도 삭제됩니다. 주의할 점은, 고아 객체 제거 기능은 영속성 컨텍스트를 flush 할 때 적용되므로, flush 시점에 DELETE SQL 이 실행됩니다.

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

CascadeType.All + orphanRemove = true 를 동시에 사용하면?

일반적으로 엔티티를 em.persist()로 영속화되고, em.remove()로 제거됩니다. 이것은 엔티티 스스로 생명주기를 관리한다는 뜻입니다. 그런데 두 옵션을 활성화하면 부모 엔티티르 통해서 자식의 엔티티를 관리할 수 있습니다.

자식을 저장하려면,

1
2
Parent parent = em.find(Parent.class, id);
parent.addChild(child);

자식을 삭제하려면,

1
2
Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(child);