프록시와 연관관계 관리

개발자·2022년 1월 15일
0

JPA

목록 보기
7/10
post-thumbnail

Member를 조회할 때 Team도 함께 조회해야 할까?
=> 비즈니스 로직에 따라 다르다. 항상 Team을 함께 조회할 필요는 없다.

프록시

em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회 -> 바로 쿼리가 나감
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회 -> 필요 시점에 쿼리가 나감

특징

  • 실제 클래스를 상속 받아서 만들어짐
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

💡 프록시 객체의 초기화

Member member = em.getReference(Member.class, “id1”); 
member.getName(); // 프록시 초기화!!
  1. getName() 호출시 target이 비어있으면 영속성 컨텍스트에 초기화를 요청함
  2. 영속성 컨텍스트가 DB를 조회해 실제 엔티티 객체(Member)를 생성해 target에 진짜 객체를 연결해줌.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함(== 비교 실패, 대신 instance of 사용)
Member m1 = em.getReference(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass())); // false
System.out.println("m1 instanceof: " + (m1 instanceof Member)); // true
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference: " + reference.getClass()); // Member 반환
  • reference 호출 후 find 호출 시에도 같은 객체 반환.
    한 트랜잭션 내에서는 reference == find
    아래 예시에서는 둘 다 프록시가 반환됨
Member refMember = em.getReference(Member.class, member1.getId());
Member findMember = em.find(Member.class, member1.getId());
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
    ❗️ 실무에서 보통 트랜잭션과 영속성 관리를 맞추는데 트랜잭션 범위 밖에서 프록시 객체를 조회하려 할 때 발생함
Member refMember = em.getReference(Member.class, member1.getId());
em.detach(refMember); // 준영속 상태로 전환
refMember.getName();

확인

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


즉시 로딩과 지연 로딩

지연 로딩

지연 로딩을 사용해 프록시로 조회
-> Team 객체를 조회하는 시점에 쿼리가 나간다.

Member

@Entity
public class Member {
  private String name;
  
  @ManyToOne(fetch = FetchType.LAZY) //**
  @JoinColumn(name = "TEAM_ID")
  private Team team;

}

Main

Team 객체의 메소드 실행시 쿼리 발생

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setName("member1");
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getId());
m.getTeam().getName(); // 이 시점에 프록시 조회(지연로딩LAZY)

즉시 로딩

Member와 Team을 JOIN해 한 번에 가져옴.

Member

@Entity
public class Member {
  private String name;
  
  @ManyToOne(fetch = FetchType.EAGER) //**
  @JoinColumn(name = "TEAM_ID")
  private Team team;

}

Main

em.find() 할 때 Member와 Team을 다 가져옴.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setName("member1");
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getId()); // 이 때 Member, Team 다 가져옴
m.getTeam().getName(); // 프록시가 아닌 진짜 호출

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생(ex. join)
    • 실무에는 테이블이 훨씬 많음. 성능 ↓
  • ❗️❗️즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
    • N+1 문제란 처음 쿼리 이외의 쿼리가 N개 발생하는 것
    • em.find()는 PK를 찍어 DB에서 가져오기 때문에 JPA 내부에서 최적화를 할 수 있다. But, JPQL에선 입력받은 쿼리가 그대로 SQL로 변환된다.
    • Member를 가져왔을 때(select m from Member m) Team이 즉시 로딩(EAGER)으로 되어있으면 Team도 가져온다.(SELECT 쿼리 2번)
    • 즉, Member가 10개라면 Team도 10개만큼 즉시 가져온다.
      -> 지연 로딩을 사용하자. 그 후에 JPQL의 fetch join을 사용해 한방 쿼리로 가져온다. 이 외에도 엔티티 그래프나 어노테이션, 배치 사이즈 등으로 푸는 방법이 존재한다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

지연 로딩 활용

Member와 Team은 자주 함께 사용 -> 즉시 로딩
Member와 Order는 가끔 사용 - > 지연 로딩
Order와 Product는 자주 함께 사용 -> 즉시 로딩
  • 이렇게 생각해 설정할 수 있지만 실무에서는 전부 지연 로딩을 사용하는 것이 좋다.
  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라! 즉시 로딩은 상상하지 못한 쿼리가 나간다.
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!



영속성 전이

CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만듦
ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

  • cascade=CascadeType.PERSIST
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
private List<Child> childList = new ArrayList<>();

주의

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐임.
  • 한 엔티티에서 관리할 때(부모가 하나)만 쓰는 것이 좋음.
    ex) 게시판-첨부파일의 관계에 사용. 하지만 파일을 여러 엔티티에서 관리하면 사용 X

종류

  • ALL : 모두 적용
  • PERSIST : 영속(저장할 때만 맞추고 싶을 때)
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

고아 객체

고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

  • orphanRemoval = true
@OneToMany(mappedBy="parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거

=> DELETE FROM CHILD WHERE ID=?

주의

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

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

  • CascadeType.ALL + orphanRemovel=true
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
    (부모 저장시 자식도 함께 저장, 부모 삭제시 자식도 함께 삭제)
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용



Ref.

[인프런] 자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한)

profile
log.info("공부 기록 블로9")

0개의 댓글