목차

  • 프록시
  • 즉시 로딩과 지연 로딩
  • 지연 로딩 활용
  • 연속성 전이 : CASECADE
  • 고아 객체
  • 연속성 전이 + 고아 객체, 생명주기
  • 실전 예제 - 5. 연관관계 관리

 

 

프록시


Member를 조회할 때 Team도 함께 조회해야 할까?

결론적으로 비즈니스 로직에 따라 다를 것이다. 멤버를 가져왔을 때 팀을 100% 사용한다고 할 때는 함께 조회하는 게 이득일 수 있지만, 팀을 20% 사용한다고 할 때는 필요할 때 팀 데이터를 가져오는게 이득일 수 있다.

 

JPA에서는 위의 문제점을 지연로딩과 프록시로 기가막히게 처리한다고 한다.

 

 

프록시 기초

  • em.find() vs em.getReference()
  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    • DB 쿼리가 나가지 않는데 조회가 되는 것
    • 실제로 해보니 getReference 시점에 쿼리가 나가지 않고 해당 객체를 사용할 때 쿼리가 나감.

getReference로 가져온 객체는 .getClass()로 네이밍 조회 시에 Hibernate에서 제공해주는 프록시 객체인 것을 알 수 있습니다.

 

 

프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐 (hibernate가 내부적으로 프록시 라이브러리를 통해 만들어 냄)
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨. (이론상)

  • 프록시 객체는 실제 객체의 참조(targer)을 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출 (여기서 DB에 쿼리가 나감)

 

프록시 객체의 초기화

Member member = em.getReference(Member.class, "id1");
member.getName();

getName시 프록시 객체가 하는 작업

 

 

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
    • instanceof 사용법 : 객체 instanceof 클래스명 
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
    • getReference 전 find로 같은 객체를 찾아놓았다면 실제 엔티티 반환
    • getReference 전 getReference로 같은 객체를 찾아놓았다면 프록시 객체 반환
    • getReference -> find가 같은 데이터일 때 find에서도 프록시 반환
      • -->> 이렇게까지 하는 이유는 JPA에서는 어떻게든 ==비교 시에 같도록 나오게하려고 함.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazuInitializationException 예외를 터트림) - 영속성 컨텍스트에 없기 때문에 초기화할 수 없다는 뜻
    • em.detach(객체) or em.close()

 

프록시 확인

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

 

 

 

즉시 로딩과 지연 로딩


Member를 조회할 때 Team도 함께 조회해야 할까?

이것은 비즈니스 로직에 따라 다르지만, 단순히 member 정보만 조회하는 비즈니스 로직에서는 Team에 대한 쿼리문을 날릴 필요가 없다.

 

지연 로딩 LAZY를 사용해서 프록시로 조회

fetch = FetchType.LAZY로 조회하는 경우 Member를 조회할 때, Team에 대한 쿼리가 나가지 않는 것을 볼 수 있습니다.

 

findMember.getTeam을 해서 Team 객체를 가져오면 쿼리문을 보내지 않고, 프록시 객체를 가져오는 것을 볼 수 있습니다. 후에 프록시 객체의 접근해 데이터를 가져올 때 프록시 객체가 진짜 객체를 필요로 하기 때문에 쿼리문을 보내고, 데이터를 가져오는 것을 볼 수 있습니다.

 

지연 로딩

지연 로딩으로 했을 때

 

지연 로딩 LAZY를 사용해서 프록시로 조회

Team team = member.getTeam(); // 쿼리가 나가지 않음 (프록시 객체 할당)

team.getName(); // 실제 team을 사용하는 시점에 초기화 (쿼리가 나감)

 

 

즉시 로딩

Member를 가져올 때 Team을 자주 함께 사용할 때 사용하면 쿼리 한번으로 조회가 되기때문에 좋은 방법입니다.

EAGER

LAZY 테스트 할 때와 같은 코드로 EAGER일 때 실행해 보았습니다. 출력된 쿼리문으로 Member와 Team 객체를 한 번의 쿼리로 조회하는 것을 볼 수 있습니다. LAZY는 Member->Team의 플로우에서 두번 쿼리문을 날리는 것에 비해 효율적으로 보입니다. 가져온 Team의 객체는 프록시 객체가 아닌 진짜 객체를 가져온 것을 볼 수 있습니다.

 

즉시 로딩

Member 객체를 가져올 때 진짜 Team 객체를 가져옴

 

즉시 로딩(EAGER), Member조회 시 항상 Team도 조회

JPA 구현체는 가능하면 조인을 사용해서 SQL 한 번에 함께 조회

 

 

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용 (특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생하기 때문 (2개일 땐 별다른 차이가 없지만, 5개 아니 더 많은 구조들에서 문제가 생깁니다.)
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. (N+1문제란, 처음 쿼리 1개를 날렸는데 추가 쿼리가 N개가 나간다.)
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩
    -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

EAGER의 경우 Member를 조회할 때, Member마다 Team 쿼리를 또 날린다.
LAZY의 경우 JPQL으로 요청한 쿼리만 나간다.

 

지연 로딩 활용

  • Member와 Team은 자주 함께 사용 -> 즉시 로딩
  • Member와 Order는 가끔 사용 -> 지연 로딩
  • Order와 Product는 자주 함께 사용 -> 즉시 로딩
  • 위의 설명은 이론적인 것, 실무에서는 모두 지연 로딩으로 해야함.

 

지연 로딩 활용 - 실무

  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라! (뒤에서 설명)
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

 

 

영속성 전이 : CASCADE와 고아 객체


영속성 전이 : CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
  • 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

 

영속성 전이 : 저장

연관관계의 주인을 저장할 때, 연관된 객체를 모두 저장하는 것을 cascade라고 한다.

 

cascade가 없을 땐 연관관계의 주인을 저장할 때, 연관관계의 주인만 DB에 insert 쿼리문이 나가며 저장된다. 아래는 위의 코드로 실행했을 때의 출력되는 쿼리문.

 

하지만 cascade를 all로 설정하면 연관관계의 주인을 저장할 때에는 연관관계의 주인 뿐만이 아닌 하위 객체들 모두 DB에 insert 쿼리문이 나가며 저장된다. 아래는 위의 코드로 실행했을 때의 출력되는 쿼리문.

 

영속성 전이 : CASCADE - 주의!

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐 
  • 위의 경우처럼 parent -> child일 때, child와 다른 객체가 연관관계가 없다면 cascade를 사용해도 되지만 child에 또 다른 객체가 연관관계가 있다면 사용하면 안된다고 합니다. (이유는 알게되면 정리)

 

CASCADE의 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

 

 

고아 객체

  • 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • orphanRemoval = true
  • Parent parent1 = em.find(Parent.class, id);
    parent1.getChildren().remove(0);
    // 자식 엔티티를 컬렉션에서 제거
  • DELETE FROM CHILD WHERE ID = ?

 

고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야 함!
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • 참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서, 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascaseType.REMOVE처럼 동작한다.

 

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

  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용 (나중에 찾아 보자)
 

DDD, Aggregate Root 란?

이번에 알아볼 것은, DDD란? Domain이란? Domain Model이란? DDD가 필요한 이유 DDD와 Aggregate Root DDD의 특징과 이유 간단히 영속성 전이, 고아 객체와의 연관 DDD(Domain Driven Design)란? 도메인 중심으로..

eocoding.tistory.com

 

 

실전 예제 5 - 연관관계 관리


글로벌 패치 전략 설정

  • 모든 연관관계를 지연 로딩으로
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경

 

영속성 전이 설정

  • Order -> Delivery를 영속성 전이 ALL 설정
    • 주문을 생성할 때 배송정보도 함께 저장하겠다는 뜻
  • Order -> OrderItem을 영속성 전이 ALL 설정
    • 주문을 생성할 때 주문아이템도 함께 저장하겠다는 뜻

 

 

 

 

 

 

출처

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

+ Recent posts