반응형
Notice
Recent Posts
Recent Comments
관리 메뉴

간단한 개발관련 내용

8장 프록시와 연관관계 관리 본문

IT 책/JPA (ORM 표준 JPA 프로그래밍)

8장 프록시와 연관관계 관리

vincenzo.dev.82 2024. 11. 19. 13:49
반응형
  • 프록시와 즉시로딩, 지연로딩
  • 영속성 전이와 고아 객체

8.1 프록시

  • 엔터티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연로딩이라 한다. 그런데 지연로딩 기능을 사용하려면 실제 엔터티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.

<aside> 💡

JPA 표준 명세는 지연 로딩의 구현 방법을 JPA 구현체에 위임했따. 따라서 지금부터 설명할 내용은 하이버네이트 구현체에 대한 내용이다. 하이버네이트는 지연로딩을 지원하기 위해 프록시를 사용하는 방법과 바이트코드를 수정하는 두 가지 방법을 제공하는데 바이트코드를 수정하는 방법은 설정이 복잡하므로 여기서는 별도의 설정이 필요 없는 프록시에 대해서만 알아보겠다.

</aside>

8.1.1 프록시 기초

  • em.find(Member.class, “member”)
  • em.getReference(Member.class, “member”)
    • 프록시 객체 반환
  • 프록시의 특징
    • 실제 객체에 대한 참조(target)를 보관한다
    • 프록시 객체의 메서드를 호출하면 프록시 객체가 실제 객체의 메서드를 호출한다
  • 프록시 객체의 초기화
    • 실제 사용될 때 데이터베이스를 조회하여 실제 엔터티를 생성하는데 이것을 프록시 객체의 초기화라 한다
  • 프록시의 특징
    • 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔터티에 접근할 수 있다.
    • 영속성 컨텍스트에서 찾는 엔터티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔터티를 반환한다.
    • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다. 하이버네이트는 org.hibernate.LayzyInitializationException 예외를 발생시킨다.
  • 준영속 상태와 초기화
    • em.close() 후
    • member.getName() 시도 시 LayzyInitializationException 예외발생

8.1.2 프록시와 식별자

  • 4.7.6 @Access
    • @Access(AccessType.FIELD), @Access(AccessType.PROPERTY)
  • 보통 엔터티 접근방식을 필드로 선언하는데 필드로 설정하면 JPA는 getId() 메서드가 id만 조회하는 메서드인지 다른 필드까지 활용해서 어떤 일을 하는 메서드인지 알지 못하므로 프록시 객체를 초기화한다.

8.1.3 프록시 확인

  • PersistenceUnitUtil.isLoaded(Object entity) 메서드를 사용하면 프록시 인스턴스의 초기화 여부를 확인 할 수 있다.

8.2 즉시 로딩과 지연 로딩

8.2.1 즉시로딩

  • 엔터티를 조회할 때 연관된 엔터티도 함께 조회한다
  • @ManyToOne(fetch = FetchType.EAGER)
  • JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.

<aside> 💡

NULL 제약조건과 JPA 조인전략

JPA는 외래키가 NULL인 상황을 고려하여 LEFT-OUTER-JOIN을 사용한다. 하지만 외부조인보다 내부조인이 성능과 최적화에 더 유리하다. 그럼 내부 조인을 사용하려면 어떻게 해야 할까? 외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장한다. 따라서 이때는 내부 조인만 사용해도 된다. JPA에게도 이런 사실을 알려줘야 한다. 다음 코드처럼 @JoinColumn에 nullable=false 를 설정해서 이 외래 키는 NULL값을 허용하지 않는다고 알려주면 JPA는 외부조인 대신에 내부조인을 사용한다.

.

nullable 설정에 따른 조인 전략

@JoinColumn(nullable = true): NULL 허용(기본값). 외부 조인 사용

@JoinColumn(nullable = false): NULL 허용하지 않음. 내부 조인 사용

.

또는 다음처럼 @ManyToOne.optional = false로 설정해도 내부 조인을 사용한다.

@ManyToOne(fetch = FetchType.EAGER, optional = false)

.

정리하자면 JPA는 선택적 관계면 외부조인을 사용하고 필수관계면 내부조인을 사용한다.

</aside>

8.2.2 지연로딩

  • 연관된 엔터티를 실제 사용할 때 조회한다.
  • @ManyToOne(fetch = FetchType.LAZY)

8.2.3 즉시로딩, 지연 로딩 정리

  • 결국 연관된 엔터티를 즉시 로딩하는 것이 좋은지 아니면 실제 사용할 때까지 지연해서 로딩하는 것이 좋은지는 상황에 따라 다르다.
  • 지연로딩: 연관된 엔터티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
  • 즉시로딩 : 연관된 엔터티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.

8.3 지연 로딩 활용

  • Member, Team, Order, Product는 각자의 상황에 맞체 즉시로딩 이나 지연로딩을 사용한다.

8.3.1 프록시와 컬렉션 래퍼

  • 하이버네이트는 엔터티를 영속 상태로 만들 때 엔터티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션래퍼라고 한다. List<Order>의 출력 결과를 보면 컬렉션래퍼 org.hibernate.collection.internal.PersistentBag이 반환된 것을 확인할 수 있다.
  • 엔터티를 지연로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문 내역과 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.

8.3.2 JPA 기본 패치 전략

  • ManyToOne 과 OneToOne : 즉시로딩 FetchType.EAGER
  • OneToMany 와 ManyToMany : 지연로딩 FetchType.LAZY
  • JPA의 기본패치 전략은 연관된 엔터티가 하나면 즉시로딩을 , 컬렉션이면 지연로딩을 사용
    • 컬렉션은 비용이 많이 드니까
  • 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다
  • 즉시로딩은 나중에 최적화 할 때 진행해도 늦지 않는다

8.3.3 컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션(리스트)을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
  • 컬렉션 즉시 로딩은 항상 외부 조인(OUTER-JOIN)을 사용한다. 예를 들어 다대일 관계인 회원테이블과 팀테이블을 조인할 때 회원 테이블의 외래 키에 not null 제약조건을 걸어두면 모든 회원은 팀에 소속되므로 항상 내부 조인을 사용해도 된다 .반대로 팀 테이블에서 회원 테이블로 일대다 관계를 조인할 때 회원이 한 명도 없는 팀을 내부 조인하면 팀까지 조회되지 않는 문제가 발생한다. 데이터베이스 제약조건으로 이런 상황을 막을 수는 없다. 따라서 JPA는 일대다 관계를 즉시로딩할 때 항상 외부조인을 사용한다.

8.4 영속성 전이 : CASCADE

  • 특정 엔터티를 영속 상태로 만들 때 연관된 엔터티도 함께 영속 상태로 만들고 싶으면 영속성 전이(transitive persistence) 기능을 사용하면 된다.
  • 대전제 : JPA에서 엔터티를 저장할 때 연관된 모든 엔터티는 영속 상태여야 한다.

8.4.1 영속성 전이 : 저장

  • @OneToMany(mappedBy = “parent”, cascade = CascadeType.PERSIST)
  • 부모만 영속화하면 CascadeType.PERSIST로 설정한 자식 엔터티까지 함께 영속화해서 저장한다.
    • em.persist(parent)
  • 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다. 단지 엔터티를 영속화할 때 연관되 엔터티도 같이 영속화하는 편리함을 제공할 뿐이다.
  • flush 시점에 영속성 전이가 일어난다

8.4.2 영속성 전이 : 삭제

  • CascadeType.REMOVE
  • em.remove(parent) : parent만 삭제해도 알아서 삭제된다
    • CascadeType.REMOVE 설정없이 삭제하면 키 제약조건 에러를 보게 된다

8.4.3 CASECADE의 종류

  • ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH

8.5 고아 객체

  • OneToOne, OneToMany 일 때, orphanRemoval = true 값으로 부모 엔터티와 연관관계가 끊어진 자식 엔터티를 자동으로 삭제하는 기능을 제공하고 이것을 고아 객체 제거라고 한다.
  • 이 기능을 이용해서 부모엔터티 컬렉션에서 자식 엔터티의 참조만 제거하면 자식 엔터티가 자동으로 삭제

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

  • 부모 엔터티를 통해서 자식의 생명주기를 관리할 때,
  • CascadeType.ALL, orphanRemoval = true 를 동시에 사용한다
  • 또는 함께 처리해야할 비즈니스일 경우

8.7 정리

  • JPA 구현체들은 객체 그래프를 마음껏 탐색할 수 있도록 지원하는데 이때 프록시 기술들을 사용한다.
  • 객체를 조회할 때 연관된 객체를 즉시 로딩하는 방법을 즉시 로딩이라 하고, 연관된 객체를 지연해서 로딩하는 방법을 지연 로딩이라 한다.
  • 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수 있는데 이것을 영속성 전이라 한다.
  • 부모 엔터티와 연관관계가 끊어진 자식 엔터티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용하면 된다.

실전 예제 | 5. 연관관계 관리


 

PersistentBag


PersistentBag는 Hibernate에서 사용하는 지연 로딩(대부분 Lazy Loading) 컬렉션 타입 중 하나로, JPA 엔티티에서 **@OneToMany, @ManyToMany**와 같은 다대다 또는 일대다 관계에서 사용하는 컬렉션을 관리하는 역할을 합니다.

Hibernate의 PersistentBag

PersistentBag은 java.util.List 인터페이스를 구현한 클래스로, 지연 로딩이 적용된 상태에서 엔티티 컬렉션을 프록시로 감싸서 관리합니다. 이는 Hibernate의 내부 컬렉션 래퍼 중 하나이며, 엔티티의 관계에 사용된 컬렉션을 데이터베이스와 연결해주는 역할을 합니다.

주요 역할과 특징

  1. 지연 로딩 관리
    • 엔티티의 컬렉션이 데이터베이스에서 실제로 로딩되지 않은 상태에서는 PersistentBag이 프록시로 동작하여, 해당 컬렉션에 접근할 때 데이터베이스에서 필요한 데이터를 지연 로딩할 수 있도록 합니다.
    • 예를 들어, user.getPosts()처럼 컬렉션에 접근할 때, Hibernate는 아직 데이터를 로딩하지 않은 상태일 수 있습니다. 이 경우 PersistentBag은 컬렉션에 첫 번째로 접근하는 순간 데이터베이스 쿼리를 실행하고 데이터를 로딩합니다.
  2. 프록시로서의 역할
    • PersistentBag은 프록시 객체로서, 컬렉션의 실제 데이터를 읽어올 때까지 지연시킵니다. 이를 통해 불필요한 데이터베이스 접근을 피할 수 있습니다.
    • 예를 들어, 데이터베이스에서 User 엔티티를 조회했을 때, 관련된 Post 엔티티들은 바로 로딩되지 않고, PersistentBag이라는 프록시로 감싸져 대기 상태에 있게 됩니다.
  3. 변경 추적
    • PersistentBag은 컬렉션에 대한 변경 사항을 추적합니다. 엔티티 컬렉션에 요소를 추가하거나 삭제하면, 이를 영속성 컨텍스트에 반영하여 나중에 데이터베이스에 동기화합니다. 이는 Hibernate가 엔티티의 상태 변경을 관리하는 데 중요한 역할을 합니다.
    • PersistentBag은 변경 사항을 추적하여 컬렉션이 실제로 수정될 때만 데이터베이스에 업데이트를 반영합니다.
  4. 중복 허용
    • PersistentBag은 기본적으로 중복을 허용하는 컬렉션입니다. Set과 달리, 동일한 객체가 여러 번 추가될 수 있습니다.
    • 따라서 PersistentBag은 List처럼 순서를 보장하지만, 중복된 객체를 허용하는 특징을 가집니다.

 

언제 PersistentBag이 반환되나?

Hibernate가 엔티티의 관계를 관리할 때, 컬렉션 타입의 필드에 대해 기본적으로 PersistentBag을 반환합니다. 다음과 같은 경우에 PersistentBag이 반환됩니다:

  • @OneToMany 관계에서 List를 사용하는 경우
  • @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List<Post> posts; // 이때, PersistentBag이 반환됨
  • @ManyToMany 관계에서 List를 사용하는 경우
  • @ManyToMany(fetch = FetchType.LAZY) private List<Tag> tags; // 이때도 PersistentBag이 반환될 수 있음

이때, Hibernate는 컬렉션의 실제 구현체 대신 PersistentBag을 반환하여, 데이터가 필요할 때까지 컬렉션의 내용을 데이터베이스에서 로드하지 않습니다.

다른 컬렉션 래퍼

PersistentBag 외에도 Hibernate는 다양한 컬렉션 래퍼를 제공합니다. 관계 유형에 따라 다른 컬렉션 래퍼가 사용될 수 있습니다.

  • PersistentSet: Set 타입의 컬렉션을 다루기 위한 지연 로딩 컬렉션 래퍼.
  • PersistentList: List 타입의 컬렉션을 위한 래퍼, PersistentBag과 유사하지만 순서를 더 엄격하게 관리.
  • PersistentMap: Map 타입의 컬렉션을 다루기 위한 지연 로딩 컬렉션 래퍼.

결론

Hibernate의 **PersistentBag**은 List 인터페이스를 구현한 지연 로딩 컬렉션으로, 엔티티의 컬렉션 필드가 데이터베이스와 연결된 상태에서 데이터베이스 접근을 제어하고, 변경을 추적하며, 필요할 때 데이터베이스에서 데이터를 로딩하는 역할을 합니다. 이를 통해 성능 최적화 및 효율적인 데이터 관리를 할 수 있게 해줍니다.

 

 

페이지 요약


프록시와 로딩 전략

  • 프록시는 지연 로딩을 구현하기 위해 사용되며, 실제 엔티티 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체이다
  • 즉시 로딩은 엔티티를 조회할 때 연관된 엔티티도 함께 조회하고, 지연 로딩은 연관된 엔티티를 실제 사용할 때 조회한다
  • JPA의 기본 페치 전략은 @ManyToOne, @OneToOne은 즉시 로딩, @OneToMany, @ManyToMany는 지연 로딩이다

영속성 전이와 고아 객체

  • 영속성 전이(CASCADE)를 사용하면 연관된 엔티티를 함께 영속 상태로 만들거나 삭제할 수 있다
  • 고아 객체 제거 기능(orphanRemoval)을 사용하면 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있다
  • CascadeType.ALL과 orphanRemoval = true를 동시에 사용하면 부모 엔티티를 통해 자식의 생명주기를 완전히 관리할 수 있다

컬렉션 래퍼

  • 하이버네이트는 엔티티의 컬렉션을 관리하기 위해 컬렉션 래퍼(예: PersistentBag)를 사용한다
  • PersistentBag은 지연 로딩, 변경 추적, 중복 허용 등의 기능을 제공하여 JPA의 효율적인 데이터 관리를 지원한다
반응형