이번에는 JPA를 통해 객체 그래프 탐색을 할 때 (ex member.getTeam().getName()) 사용되는 즉시로딩과 지연로딩에 대해 알아보고,
연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체에 대해 알아보려 한다.
앞서서 객체 그래프 탐색을 할 때, 한 번에 연관된 모든 객체를 조회하는 것이 아니라,
연관된 엔티티를 실제 사용될 때까지 조회를 미루는 지연 로딩을 JPA는 지원해 준다고 했다.
이것을 가능하게 하려면, 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 🌸 🌸 프록시 객체🌸 🌸 라고 한다.
이 프록시에 대해 살펴보겠다.
Member member = entityManager.getReference(Member.class ,"member1");
위의 코드처럼 getReference() 메소드를 사용하면 엔티티를 실제로 사용할 때까지 조회를 미룰 수가 있다.
이때 JPA는 당연히 DB를 조회하지 않고 실제 엔티티 객체를 생성도 하지 않고,
대신에 DB 접근을 위임한 프록시 객체를 반환한다.
프록시는 실제 클래스를 상속 받아 만들어져, 실제 클래스와 겉 모양이 같다.
그리고 프록시 객체는 실제 객체에 대한 참조를 보관한다.
그래서 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 된다.
▶ 프록시 초기화
프록시 초기화 하는 과정은 아래와 같다.
1. member.getName()과 같이 엔티티가 실제로 쓰일 때 프록시 객체의 member.getName()을 호출해서 데이터를 조회한다.
2. 이때, 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다. 이를 초기화라고 한다.
3. 영속성 컨텍스트는 DB를 조회해서 실제 엔티티 객체를 생성한다.
4. 프록시 객체는 실제 엔티티 객체의 참조를 보관한다.
5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
▶ 프록시 특징
1. 처음 사용할 때 한 번만 초기화된다.
2. 초기화 되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있다.
3. 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시 주의가 요한다.
4. 영속성 컨텍스트에 이미 찾는 엔티티가 있으면 getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
5. 초기화는 영속성 컨텍스트의 도움을 받아야 가능하며, 따라서 준영속 상태의 프록시를 초기화하면 예외가 발생한다.
6. 엔티티를 프록시로 조회할 때 식별자(PK) 를 파라미터로 전달하는데, 따라서 team.getId()와 같은 경우 프록시를 초기화하지 않는다. team.getName()을 조회하는 것과 다른 경우이다. (식별자 값은 이미 전달받아 있으므로)
7. PersistenceUnitUtil.isLoaded method를 통해 프록시 인스턴스 초기화 여부를 확인할 수 있다. 혹은 클래스 명을 직접 출력해 보면, 프록시 객체는 클래스 명 뒤에 ..javassist..라 되어 있다.
즉시 로딩은 단어 그대로 한 엔티티를 조회할 때 연관된 엔티티도 한 번에 조회 해 오는 것이고,
지연 로딩은 연관된 엔티티가 실제로 사용될 때 조회하는 것이다.
▶ 즉시로딩 간단 정리
1. @ManyToOne 의 fetch 속성을 FetchType.EAGER 로 지정하면 된다
2. 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 통해 연관된 엔티티를 한 번에 조회한다.
3. 외부 조인보다 내부 조인이 성능과 최적화에서 유리하지만, 회원과 팀의 관계에서 팀에 소속하지 않은 회원과 팀을 내부 조인하면 회원, 팀 모두 조회가 안 되기에 적절히 설정하는 것이 필요하다.
외부 조인 / 내부 조인 설정법
@JoinColumn(name = "TEAM_ID", nullable = false)
와 같이 nullable = false를 통해 해당 외래 키는 NULL 값을 허용하지 않는다고 JPA에게 알려주면 내부 조인을 사용하게 된다. 따로 하지 않으면 기본적으로 외부 조인을 사용한다.
▶ 지연로딩 간단 정리
1. @ManyToOne 의 fetch속성을 FetchType.LAZY로 지정한다.
2. 위에서 설명한 것처럼, 지연로딩을 사용하면 실제 연관된 객체를 사용하기 전까지 연관된 객체에 JPA는 프록시 객체가 들어가게 된다. (ex 회원과 팀 관계에서 회원을 조회하면 member.getTeam()에서 반환되는 Team 엔티티는 프록시 객체임)
3. 조회 대상이 이미 영속성 컨텍스트에 있으면 실제 객체가 반환된다.
▶ 👊 즉시로딩 지연로딩 비교
어떨 때 어떤 전략을 사용하면 좋을까?
-> 당연히 함께 자주 사용되는 관계의 경우 즉시 로딩으로,
자주 사용되지 않는 경우 지연로딩으로 하는 것이 좋다.
컬렉션 래페와 관련해서 이 부분을 살펴보겠다.
▶ 컬랙션 래퍼
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
회원 객체 안에 주문 컬랙션이 있다.
하이버네이트는 엔티티를 영속 상태로 만들 때, 엔티티에 컬랙션이 있으면,
컬렉션을 추적하고 관리하기 위해 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데, 이를 컬랙션 래퍼 라고 한다.
fetch속성의 기본 설정값은,
@ManyToOne, @OntToOne 의 경우 즉시 로딩,
@OneToMany, @ManyToMany 와 같이 연관된 엔티티가 컬랙션인 경우 지연 로딩을 사용한다.
위의 코디에서 만약 한 회원안에 주문이 무수히 많은데, 즉시 로딩으로 설정하면
한 회원을 조회하는데도 무수히 많은 데이터가 로딩되게 된다.
이는 성능면에서 매우 좋지 않기에
모든 연관관계에 지연 로딩을 사용하고, 추후 개발이 완료단계에 이르렀을 때 사용도를 분석하여 필요한 곳에만 적절히 즉시 로딩을 설정해 최적화 시키는 것을 추천한다.
CASCADE 옵션을 통해 한 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 (=영속성 전이) 만들 수 있다.
JPA에서 엔티티를 저장할 때, 연관된 모든 엔티티는 영속 상태어야 하는데
따라서 회원에 연관된 주문들을 모두 저장하려면,
연관 관계 설정을 해 주고 (-> order.setMember(member) 와 같이)
모든 주문들을 entityManager.persist(order)와 같이 일일이 저장시켜야 한다.
하지만 CASCADE 옵션을 설정하면, 영속성 전이가 되어 연관관계 추가만 해 주고, entityManger.persist()를 각각 해주지 않아도 된다.
// 아래와 같이 속성을 추가 해 주면 된다.
@OneToMany(mappedBy = "member" , casecade = CascadeType.PERSIST)
private List<Order> orders = new ArrayList<>();
다만 영속성 전이는 연관관계 매핑과는 관련이 없기에 혼용해서 생각하면 안 된다.
▶ 영속성 전이 : 삭제
위와 유사하게 회원에 따른 주문들을 모두 삭제하려면, 하나씩 하나씩 remove()를 해주어야 한다.
하지만, Casecade 옵션을 설정하면
em.remove(member);
만 하여도 딸린 주문들까지 모두 삭제가 된다.
이때 외래 키 제약조건을 고려하여 주문을 먼저 삭제하고 회원을 삭제한다.
▶ CASCADE 종류
ALL : 모두 적용
PERSIST : 함께 영속화
MERGE : 병합될 때 함께 병합됨
REMOVE : 삭제될 때 함께 삭제됨
REFRESH : 함께 새로 고침 됨 (동기화)
DETACH : DETACH될 때 함께 영속성 컨텍스트에서 분리됨
▶ 고아 객체
부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동 삭제하는 기능을 고아 객체 제거라고 한다.
@OneToMany(mappedBy="member" , orphanRemoval = true)
위와 같이 하여 설정할 수 있다.
이렇게 설정한 경우
member.getOrder().remove(0) 을 했을 때,
자동적으로 해당 Order 엔티티에 해당하는 DB의 데이터도 삭제되게 된다.
(플러시 시점에서 DELETE SQL이 실행됨)
CascadeType.ALL 과 orphanRemoval = true를 동시에 사용하면
회원 엔티티를 통해 주문 엔티티의 생명주기를 관리할 수 있다.
(= 주문을 저장하려면 회원에 등록만 하면 되고 (=CASCADE),
주문을 삭제하려면 회원에게서 제거만 하면 된다 (=orphanRemoval))
다음에는 값 타입에 대해 알아보겠다.
참조: 자바 ORM 표준 JPA 프로그래밍 : 김영한