프록시와 연관관계 관리

정민기·2021년 8월 26일
0

Java ORM JPA

목록 보기
7/9

프록시

프록시

  • 프록시 조회 : em.getReference() 를 통해 데이터 베이스 조회를 미루는 가짜(프록시) Entity 객체를 조회한다.
    (데이터 베이스를 통한 실제 Entity 객체 조회는 em.find())

프록시 객체

  • 실제 클래스를 상속 받아서 만들어진다. 따라서 실제 클래스와 겉 모양이 같다.
  • 프록시 객체는 실제 객체의 참조(target) 을 보관한다. 따라서 프록시 객체를 호출하면 참조(target)에 있는 실제 객체의 메소드를 대신 호출해 준다.

프록시 객체의 초기화

  • 초기화 과정
//프록시 객체 조회
Member member = em.getReference(Member.class, "id1");
member.getName();

위 코드가 실행되면 다음과 같은 과정이 진행된다.
1. Client가 프록시 객체의 메소드를 요청한다.
2. 아직 프록시 객체는 초기화 되지 않아 프록시 객체 내의 target이 없기 때문에 JPA가 영속성 컨텍스트에 실제 객체를 가져오도록 초기화를 요청한다.
3. 영속성 컨텍스트는 DB를 조회해서 원하는 실제 객체를 조회한다.
4. 실제 객체를 생성한다.
5. target이 실제 객체와 연결되어 target.getName()을 실행할 수 있게 된다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 Entity로 바뀌는 것이 아니라 프록시 객체를 통해 실제 Entity에 접근 가능하다.
  • 프록시 객체는 원본 Entity를 상속받기 때문에 타입 체크시 주의해야한다.
    (== 비교는 실패하기 때문에 instanceof를 사용하자)
  • 영속성 컨텍스트에 찾는 Entity가 이미 있으면 em.getReference()를 호출해도 실제 Entity를 반환한다.
    (이미 영속성 컨텍스트에 있기 때문에 프록시로 반환해봐야 이점이 없고, JPA는 같은 영속성 컨텍스트에서 가져오고 PK가 같으면 항상 같은 것을 반환해주는 것을 보장해주기 때문이다. 이러한 원칙 때문에 em.getReference()를 먼저 호출하고 em.find()를 호출하면 em.find()에도 프록시 객체를 반환한다.)
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
    (org.hibernate.LazyInitializationException 예외를 터트림)

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력
  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);

즉시 로딩과 지연 로딩

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

단순히 member 정보만 사용하는 비지니스 로직일 경우 member를 조회 할 때 team도 같이 조회하게 된다면 불필요한 cost가 발생한다. 이때 team을 프록시로 조회하게 된다면 DB에 접근하는 cost를 줄일 수 있을 것이다.

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

@Entity
public class Member {
  ...
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  ...
}
  • 내부 메커니즘

    Member member = em.find(Member.class, 1L); Member를 부를 때 Team은 프록시 객체로 생성된다.

    team.getName(); 이 코드와 같이 실제 team을 사용하는 시점에 DB를 조회하여 프록시 객체가 초기화 된다.

즉시 로딩 EAGER를 사용해서 함께 조회

만약 Member와 Team이 모두 필요한 경우 지연 로딩을 사용하면 네트워크를 2번 따로 접근하기 때문에 오히려 cost가 증가한다. 이럴 경우는 EAGER를 사용해서 Member와 Team을 함께 실제 객체로 조회하는 것이 이득일 것이다.

@Entity
public class Member {
  ...
  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  ...
}
  • 내부 메커니즘

    Member를 로딩할 때 조인하여 Team까지 같이 조회한다. JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.

프록시와 즉시 로딩 주의점

  • 가급적 지연 로딩만 사용하자 (특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
    아래 코드가 실행 되면 JPQL은 SQL로 번역되어 실행된다. 아래 SQL이 그대로 실행되면서 Member만 조회하게 되고, Member를 조회하면서 EAGER이기 때문에 Team을 조회하는 SQL이 추가적으로 실행된다.
    이때 다른 Team일 경우 영속성 컨텍스트에 없기 때문에 추가적으로 조회하게 된다. 따라서 1개의 select문이 10개의 Member를 조회하게 되면 Team을 조회하는 SQL이 10번 실행되게 된다.
    이처럼 1개의 select문이 N개의 select문을 추가로 부르기 때문에 N+1 문제라고 한다.
List<Member> members = em.createQuery("select m from Member m", Member.class)
	.getResultList();

이 문제를 해결하기 위해서는 LAZY로 변경한 후 fetch join을 사용해서 해결할 수 있다.

List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class)
	.getResultList();
  • @ManyToOne, @OneToOne 즉시 로딩이 Default이다. 따라서 LAZY로 변경해 줘야 한다.
  • @OneToMany, @ManyToMany는 지연 로딩이 Default이다.

지연 로딩 활용 - 실무

  • 모든 연관관계에 지연 로딩을 사용하자!!
  • 실무에서 즉시 로딩을 사용하지 말자!!
  • JPQL fetch 조인이나, Entity 그래프 기능을 사용하자!!
  • 즉시 로딩은 상상하지 못한 쿼리가 나가게 된다.

영속성 전이 : CASCADE

CASCADE

  • 연관 관계나 즉시 로딩, 지연 로딩과 전혀 상관이 없다.
  • 특정 Entity를 영속 상태로 만들 때 연관된 Entity도 함께 영속 상태로 만들고 싶을 때 사용한다.
    (Ex. parent Entity를 저장할 때 child Entity도 저장하고 싶을 때)

주의점

  • 영속성 전이는 연관관계 매핑이나, 즉시 로딩, 지연 로딩과 전혀 관련이 없다.
  • 단지 Entity를 영속화할 때 연관된 Entity도 함께 영속화하는 편리함을 제공할 뿐이다.
  • 하위 Entity가 하나의 상위 Entity(단일 소유자)에만 연관되어 있다면 사용할 수 있다. 하지만 여러 상위 Entity와 연관되어 있다면 CASCADE사용하면 운영이 매우 힘들어지므로 사용하지 않는 것이 좋다.

CASCADE의 종류

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

고아 객체

고아 객체

  • 고아 객체 제거 : 부모 Entity와 연관 관계가 끊어진 자식 Entity를 자동으로 삭제하는 기능이다.
  • orphanRemoval = true
  • 자식 Entity를 컬렉션에서 제거하면 자동으로 DB에 DELETE 쿼리가 나가서 DB에서도 삭제된다.
  Parent parent1 = em.find(Parent.class, id);
  parent1.getChildren().remove(0);
  //자식 Entity를 제거 -> DELETE 쿼리 전송

주의점

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

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

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



[Reference]

Inflearn 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

0개의 댓글

관련 채용 정보