반응형
프록시
- 엔티티를 조회할 때 연관 엔티티들이 항상 사용되는 것은 아니다.
- 예를 들어, 회원 엔티티와 연관된 바버샵 엔티티는 사용될 때도 있지만 그렇지 않을 때도 있다.
- 같은 회원 엔티티지만 바버는 연관된 바버샵 엔티티를 사용해 근무 중인 바버샵을 표시한다.
- 그러나 고객은 바버샵 엔티티와 연관관계 설정이 되어있지 않기 때문에 제공하지 않는다.
public class Member extends BaseEntity {
...
@ManyToOne
@JoinColumn(name = "WORK_PLACE_ID")
private BarberShop workPlace;
- 그런데 매번 회원 엔티티를 조회할 때마다 바버샵 테이블과 조인하는 것은 효율적이지 않다.
- JPA에서는 이런 문제를 해결하기 위해 엔티티가 실제로 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 지연 로딩이라 한다.
- 지연 로딩은 실제로 연관된 엔티티를 사용할 시점에 데이터베이스에서 해당 데이터를 조회하는 것이다.
- 그런데 지연 로딩을 사용하려면 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요하다. 이를 프록시 객체라 한다.
- 프록시 기초
- JPA에서 식별자를 이용해 엔티티 하나를 조회할 대는 EntityManager#find()메서드를 사용한다. 이렇게 엔티티를 직접 조회하면 조회한 엔티티를 사용하지 않더라도 데이터베이스를 조회한다.
- 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 EntityManager#getReference() 메서드를 사용하면 된다.
- 해당 메서드를 사용하면 데이터 베이스 접근을 위임한 프록시 객체를 반환한다.
- 프록시의 특징
- 프록시는 실제 객체를 상속받아 만들어지므로 실제 객체와 겉모습이 같다. 사용자 입장에서는 구분 없이 사용할 수 있다.
- 프록시 객체는 실제 객체에 대한 참조를 보관한다. 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 객체로 대체되는 것은 아니다. 내장된 타겟 객체에 실제 객체를 저장한다.
- 원본 엔티티를 상속받은 객체이기에 타입 체크 시 유의해야 한다.
- 영속성 컨텍스트에 실제 엔티티가 이미 있다면 굳이 데이터베이스를 조회할 필요가 없다. 따라서 getReference()를 호출해도 실제 객체를 반환한다.
- 프록시 객체 초기화를 위해서는 영속성 컨텍스트를 통해 실제 객체를 찾아야한다. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
- 프록시 객체의 초기화 : 프록시 객체는 참조하는 실제 객체가 사용될 때 데이터베이스를 조회해 실제 엔티티 객체를 생성한다. 이를 프록시 객체 초기화라 한다.
- 프록시 객체에 proxy.getName()을 호출해 실제 데이터를 조회한다.
- 프록시 객체는 실제 엔티티가 생성되어있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다(초기화)
- 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
- 프록시 객체는 생성된 실제 엔티티 객체의 참조를 멤버변수에 저장한다.
- 저장한 실제 엔티티 객체의 getName()을 호출하고 결과를 반환한다.
class MemberProxy extends Member {
Member target = null; // 실제 엔티티 참조
public String getName() {
if(target == null) {
// 초기화 요청
// DB 조회
// 실제 엔티티 생성 및 참조 보관
this.target = ...;
}
return target.getName();
}
}
- 준영속 상태의 초기화
- 영속성 컨텍스트를 종료한 프록시 객체에 대해 초기화를 시도하면 LazyInitializationException이 발생한다.
- 프록시와 식별자
- 엔티티를 프록시로 조회할 때는 식별자 값을 전달한다. 프록시는 이 식별자 값을 저장한다.
- 프록시는 식별자 값을 가지고 있다. 따라서, 실제 객체의 식별자 값을 요구하는 메서드를 호출해도 실제 객체를 생성하지 않는다.
- 단 엔티티 접근 방식을 프로퍼티로 설정한 경우에 한한다.
- 엔티티 접근 방식을 필드로 정하면 JPA는 getId()가 id만 조회하는 메서드인지, 다른 필드까지 활용하는지 알 수 없다. 따라서 프록시 객체를 초기화한다.
- 이러한 특징을 사용해 연관관계를 설정할 때 매우 용이하다. 연관관계를 설정할 때는 식별자 값만 사용하기 때문이다.
- 프록시 확인
- PersistenceUnitUil#isLoaded(Object entity)메서드를 사용하면 프록시 인스턴스의 초기화 여부를 알 수 있다.
- 초기화 되었거나 프록시가 아니라면 true, 그렇지 않다면 false를 반환한다.
즉시 로딩과 지연 로딩
- 프록시 객체는 주로 연관된 엔티티를 지연로딩할 때 사용한다.
- 즉시 로딩(EAGER LOADING) : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 지연 로딩(LAZY LOADING) : 연관된 엔티티를 실제로 사용할 때 조회한다.
- 즉시 로딩
- 즉시 로딩을 사용하고 싶다면 @ManyToOne의 fetch 속성을 FetchType.EAGER로 설정하면 된다.
public class Member extends BaseEntity {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "WORK_PLACE_ID")
private BarberShop workPlace;
- 이렇게 설정하면, Member객체를 find()할 때 연관된 바버샵 객체도 조회한다.
- 대부분의 JPA 구현체는 조회가 두 번 일어나는 것을 방지하기 위해 가능하면 조인 쿼리를 사용한다.
- 이 때, 연관 테이블의 외래 키에 null을 허용하면 외부 조인을, null을 허용하지 않으면 성능 향상을 위해 내부 조인을 사용한다.
- 지연 로딩
- 즉시 로딩을 사용하고 싶다면 @ManyToOne의 fetch 속성을 FetchType.LAZY로 설정하면 된다.
public class Member extends BaseEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "WORK_PLACE_ID")
private BarberShop workPlace;
- 이렇게 설정하면, Member 객체를 find()할 때, 연관된 바버샵 객체는 프록시 객체로 대체된다.
- 연관된 바버샵 프록시 객체를 사용할 때 데이터 로딩을 지연한다. 따라서 지연 로딩인 것이다.
- 만일 조회 대상이 영속성 컨텍스트에 있으면 당연히 실제 객체를 넣어준다.
- 즉시 로딩, 지연 로딩 정리
- 처음부터 모든 엔티티를 영속성 컨텍스트에 올리는 것도 별로고, 그렇다고 필요할 때마다 SQL을 매번 실행하는 것도 최적화 면에서 썩 좋지는 않다.
- 따라서 즉시 로딩이 좋을지, 지연 로딩이 좋을지를 판단해서 상황에 맞게 사용하는 것이 좋다.
- 일반적으로는 지연 로딩으로 설정하고, 로직 상 자주 사용되는 연관 객체라면 즉시 로딩으로 바꿔주는 편이 낫다.
지연 로딩 활용
- Member와 연관된 BarberShop은 자주 사용되지 않는다. 따라서 지연 로딩을 사용한다.
- 반면 Review는 자주 사용된다. 따라서 즉시 로딩을 사용한다.
public class Member extends BaseEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "WORK_PLACE_ID")
private BarberShop workPlace;
@OneToMany(mappedBy = "writer", fetch = FetchType.EAGER)
private List<Review> reviews = new ArrayList<>();
- 회원을 조회할 때, 바버샵을 조회하는 SQL은 실행되지 않는다. 하지만 하이버네이트는 조인 쿼리를 사용해 리뷰는 같이 제공한다.
- 프록시와 컬렉션 래퍼
- 만약, 바버샵과 일대다로 연관된 List<Schedule> 정보를 지연 로딩한다고 생각해보자.
- 스케쥴 내역을 조회하는 barberShop.getSchedules() 메서드를 호출하면 어떤 값을 반환할까?
- 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 이를 컬렉션 래퍼라 한다.
- 위의 getSchedules()은 PersistentBag 클래스를 반환한다.
- 즉, getSchedules()을 호출한다고 컬렉션을 초기화하지 않는 것이다. 해당 컬렉션은 getSchedules().get(0)와 같이 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회하고 초기화된다.
- JPA 기본 fetch전략
- @ManyToOne, @OneToOne : 즉시 로딩
- @OneToMany, @ManyToMany : 지연 로딩
- 기본적으로 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다. 컬렉션 로딩에 더 많은 비용이 발생하기 때문이다.
- 일단 모든 연관관계에 지연 로딩을 사용하도록 설정하고, 필요하면 즉시 로딩으로 전환하자.
- 컬렉션에 즉시 로딩 사용 시 주의점
- 컬렉션을 하나 이상 즉시 로딩하지 말자. N 크기의 컬렉션과 M 크기의 컬렉션을 동시에 즉시 로딩하면 N * M만큼의 비용이 발생한다.
- 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다. 만약 컬렉션이 비어있다면 내부 조인이 불가능하기 때문이다.
영속성 전이 : CASCADE
- 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.
- JPA에서 엔티티를 저장하려면 연관된 모든 엔티티가 영속 상태여야한다. 따라서 부모 엔티티를 먼저 저장한 상태에서 자식 엔티티를 저장할 수 있다.
class Parent {
@OneToMany(mappedBy = "parent")
List<Child> children = new ArrayList<>();
}
class Child {
@ManyToOne
@JoinColumn(name = "PARENT_ID)
Parent parent;
}
//////////
Parent parent = new Parent();
em.persist(parent);
Child child = new Child();
child.setParent(parent);
parent.addChild(child);
em.persist(child);
- 영속성 전이 : 저장
class Parent {
@OneToMany(mappedBy = "parent", cascade = "CascadeType.PERSIST)
List<Child> children = new ArrayList<>();
}
class Child {
@ManyToOne
@JoinColumn(name = "PARENT_ID)
Parent parent;
}
//////////
Parent parent = new Parent();
Child child = new Child();
child.setParent(parent);
parent.addChild(child);
em.persist(parent);
- 영속성 전이를 활성화하는 CASCADE 옵션을 적용했다.
- 해당 옵션을 통해 부모를 영속화 할 때 자식도 함께 영속화된다.
- em.persist(child)를 통해 자식 객체를 별도로 영속화하지 않았다.
- 영속성 전이 : 삭제
- 기본적으로는 연관객체를 삭제하고 싶다면 자식 객체를 모두 삭제하고 부모 객체를 삭제해야한다. 그렇지 않으면 무결성 예외가 발생한다.
em.remove(child1);
em.remove(child2);
em.remove(parent);
- 그러나 삭제의 영속성을 설정하면 부모 객체를 삭제하면 연관된 자식 객체는 모두 삭제된다.
class Parent {
@OneToMany(mappedBy = "parent", cascade = "CascadeType.REMOVE)
List<Child> children = new ArrayList<>();
}
class Child {
@ManyToOne
@JoinColumn(name = "PARENT_ID)
Parent parent;
}
//////////
Parent parent = em.find(Parent.class, 1L);
em.remove(parent);
- CASCADE의 종류
- ALL : 모두 적용
- PERSIST : 저장
- MERGE : 병합
- REMOVE : 삭제
- REFRESH : refresh
- DETACH : detach
- 여러 개를 조합해서 사용할 수 있다.
- cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
고아 객체
- JPA는 부모 객체와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. 이를 고아 객체 제거라 한다.
- 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
class Parent {
@OneToMany(mappedBy = "parent", orphanRemoval = true)
List<Child> children = new ArrayList<>();
}
class Child {
@ManyToOne
@JoinColumn(name = "PARENT_ID)
Parent parent;
}
//////////
Parent parent = em.find(Parent.class, 1L);
parent.getChildren().remove(0); // 0 번째 child객체 삭제 (DELETE FROM CHILD WHERE ID = ?)
- 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
- 따라서 해당 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 내 자식을 남이 죽이는 건 맞지 않다.
- 즉, 특정 엔티티가 개인 소유면 가능하다.
- 개념적으로 부모가 사라지면 자식은 고아가 된다. 따라서 CascadeType.REMOVE를 설정한 것과 같다.
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL + orphanRemoval = true를 함께 설정하면, 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
- 부모에 자식을 넣어주면 자식도 저장되고, 부모를 삭제하면 자식도 삭제된다.
반응형
'Programming > JPA 프로그래밍' 카테고리의 다른 글
07. 고급 매핑 (0) | 2023.11.13 |
---|---|
06. 다양한 연관관계 매핑 (1) | 2023.11.06 |
05. 연관관계 매핑 기초 (0) | 2023.10.12 |
03. 영속성 관리 (0) | 2023.08.18 |
01. JPA 소개 (0) | 2023.08.15 |
댓글