본문 바로가기
Study/spring

자바 ORM 표준 JPA 프로그래밍(10) - 프록시와 연관관계 관리

by 유경호 2021. 1. 5.
반응형

프록시

  • 엔티티 조회 시 연관된 엔티티들이 항상 사용되는 것이 아님.

  • 프록시를 사용하면 연관된 엔티티를 처음부터 데이터베이스에 조회하는 게 아닌 실제 사용하는 시점에 데이터베이스에서 조회할 수 있음.

  • 자주 함께 사용하는 엔티티들은 조인을 사용해서 함께 사용하는 것이 효과적

  • JPA는 즉시 로딩(Eager Loading)과 지연 로딩(Lazy Loading)을 둘 다 지원함.

 

EntityManger.find()

이 메소드는 영속성 컨텍스트에 엔티티가 없다면 데이터베이스를 조회한다. 이렇게 조회한 엔티티는 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다.

 

EntityManager.getReferance()

이 메소드를 사용하면 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미룬다. 이 메소드가 호출될 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

 

프록시의 특징

아래 그림을 보면 프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다.

프록시 객체는 실제 객체에 대한 참조(target)을 보관한다. 그리고 프록시 객체의 메소드를 호출하면 프록시가 실제 객체의 메소드를 호출한다.

프록시 객체의 초기화


Member member = em.getReference(Member.class, “id1”);

member.getName();

프록시 객체는 member.getNmae()과 같이 실제 사용되는 시점에 데이터베이스를 조회해서 실제 엔티티 객체를 생성하여 영속성 컨텍스트에 등록한다. 이와 같은 행위를 프록시 객체의 초기화라 한다.

 

프록시 객체의 초기화 과정

  1. 프록시 객체에 member.getName()을 호출하여 실제 데이터를 조회함

  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청함.

  3. 영속성 컨텍스트는 데이터베이스를 조회하여 실제 엔티티 객처를 생성함.

  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조 Member target 필드에 보관함.

  5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출하여 결과를 반환함.

 

프록시의 특징

  • 처음 사용되는 시점에 한 번만 초기화된다.

  • 초기화한다고 해서 프록시 객체가 실제 엔티티로 바뀌는 게 아님. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있는 것임.

  • 원본 엔티티를 상속받은 객체이므로 타입 비교 시 주의해서 사용하여야함(instanceof 사용 권장, == 비교 X)

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getRefernce()를 호출해도 프록시가 아닌 실제 엔티티를 반환함.

  • 프록시 객체의 초기화는 영속성 컨텍스트의 도움을 받아야 가능하기 때문에 준영속 상태의 프록시를 초기화하면 예외가 발생한다.

 

프록시와 식별자

프록시 객체는 getReference 호출 시에 key 값을 넘겨 받아 가지고 있는 상태로 반환되기 때문에 member.getId()를 호출해도 프록시 초기화가 되지 않는다. 단 엔티티 접근 방식이 @Access(AccessType.PROPERTY)로 설정돼있는 경우에만 해당하는데. @Access(AccessType.FIELD)로 설정하면 JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용하겠다는 건지 알지 못하여 프록시 객체를 초기화한다.

 

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인 PersistenceUnitUtil.isLoaded(Object entity)

  • 프록시 클래스 확인 방법 entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)

  • 프록시 강제 초기화 org.hibernate.Hibernate.initialize(entity);

  • 참고: JPA 표준은 강제 초기화 없음
    강제 호출: member.getName()

즉시 로딩과 지연 로딩

  • 즉시 로딩: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.

    • 설정 방법: @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩: 연관된 엔티티를 실제 사용되는 시점에 조회한다.

    • 설정 방법: @ManyToOne(fetch = FetchType.LAZY)

 

즉시 로딩

즉시 로딩(Eager Loading)을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.EAGER로 지정한다.

회원과 팀을 즉시 로딩으로 설정 하였다면 em.find(Member.class, "member1")로 회원을 조회하는 순간 팀도 함께 조회한다. 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 조인 쿼리를 사용한다. 즉 회원을 조회할 때 회원과 팀을 조인해서 쿼리 한 번으로 두 엔티티를 모두 조회한다.

 

지연 로딩

지연 로딩(Lazy Loading)을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.LAZY로 지정한다.

회원과 팀을 지연 로딩으로 설정 하였다면 em.find(Member.class, "member1") 호출 시 회원만 조회하고 팀은 조회하지 않는다. 대신에 위 그림과 같이 조회한 회원의 team 필드에 프록시 객체를 넣어둔다.

반환된 팀 객체는 프록시 객체다. 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다.

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용(특히 실무에서)

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생

  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

  • @ManyToOne, @OneToOne은 기본이 즉시 로딩

    → LAZY로 설정

  • @OneToMany, @ManyToMany는 기본이 지연 로딩

 

영속성 전이: CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고자 하면 영속성 전이(transitive persistence) 기능을 사용하면 된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공하는데, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다. 그래서 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만든다. 이럴 때 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.

 

CASCADE의 종류

  • ALL: 모두 적용

  • PERSIST: 영속

  • REMOVE: 삭제

  • MERGE: 병합

  • REFRESH: REFRESH

  • DETACH: DETACH

 

CASCADE 사용 시 주의점

게시판의 첨부파일이나 이미지파일 등에는 쓸만함, 자식 엔티티가 다른 엔티티와도 관계가 있다면 쓰기 곤란함. 굉장히 종속적인 관계일 때 사용하기를 권장(lifeCycle이 동일할 때)

 

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. 이것을 고아 객체(ORPHAN) 제거라 한다. 이 기능을 사용하면 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.


@Entity

public class Parent {

    @Id @GeneratedValue

    private Long id;



    @OneToMany(mappedBy = "parent", orphanRemoval = true)

    private List<Child> childeren = new ArrayList<>();
  • orphanRemoval = true로 설정

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로

 

보고 삭제하는 기능

  • 참조하는 곳이 하나일 때 사용해야함!

  • 특정 엔티티가 개인 소유할 때 사용

  • @OneToOne, @OneToMany만 가능

  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.

  • 고아 객체도 CASCADE와 같이 굉장히 종속적인 관계일 때 사용하는 것을 권장함

 

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

  • CascadeType.ALL + orphanRemovel=true

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거

  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음

  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

반응형