JPA 프록시와 연관관계 관리
프록시
- 지연 로딩 : 엔티티가 실제 사용될 때까지 DB 조회를 지연하는 방법
- 프록시 객체 : 실제 엔티티 객체 대신 DB 조회를 지연하는 가짜 객체로 사용
프록시 특징
- 실제 클래스를 상속 받아서 만들어지므로 겉 모양이 동일하다.
- 사용하는 입장에서는 프록시 객체 여부를 구분하지 않고 사용한다.
- 프록시 객체는 실제 객체에 대한 참조를 보관하고 있다. 그래서 프록시 객체의 메서드를 호출하면 실제 객체의 메서드가 호출된다.
프록시 객체의 초기화
em.find()
: DB를 통해 실제 엔티티 객체 조회한다.em.getReference()
: DB를 조회하지 않고 객체도 생성하지 않는다. 프록시 객체를 반환한다.
Member member = em.getReference(Member.class, "id");
member.getName();
프록시 핵심
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화 후 프록시 객체를 통해 실제 엔티티에 접근하는 것이다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입체크시 주의해서 사용해야 한다.
- 영속성 컨텍스트에 찾고자 하는 엔티티가 존재하면
em.getReference()
를 호출해도 실제 엔티티가 반환된다. - 초기화는 영속성 컨텍스트의 도움을 받아야 한다. 그래서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하게 되면 문제가 발생한다. 하이버네이트의 예로
org.hibernate.LazyInitializationException
을 던진다.
프록시와 식별자
- 프록시 객체는 식별자(PK) 값을 보관한다.
- 엔티티 접근 방식을 프로퍼티(
@Access(AccessType.PROPERTY)
)로 설정한 경우는 프록시 객체가 초기화되지 않는다. 필드(@Access(AccessType.FIELD)
)로 설정해야 초기화된다. - 연관관계를 설정할 때는 엔티티 접근방식을 필드(
@Access(AccessType.FIELD)
)로 설정해도 프록시 객체가 초기화되지 않는다. - 프록시를 사용하면 DB 접근 횟수를 줄일 수 있다.
프록시 확인
PersistenceUnitUtil.isLoaded(Object entity)
메서드를 통해 프록시 초기화 여부를 확인할 수 있다.
boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);
클래스명을 직접 출력하여 조회한 엔티티가 진짜 엔티티인지 프록시인지 확인할 수 있다. 조회 결과가 ... javasist or HibernateProxy ...
(프록시를 생성하는 라이브러리에 따라 다름)로 출력된다면 프록시 객체로 판단하면 된다.
System.out.println(entity.getClass().getName());
하이버네이트 구현체를 사용할 경우 initialize()
를 사용하면 프록시를 강제로 초기화할 수 있다.
즉시 로딩과 지연 로딩
- 프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.
- JPA는 연관된 엔티티의 조회 시점을 선택할 수 있는 즉시 로딩, 지연 로딩을 제공한다.
즉시 로딩
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Member 엔티티를 조회할 때 Team 엔티티도 함께 조회된다. 이 때 대부분의 JPA 구현체들은 즉시 로딩을 최적화하기 위해 조인 쿼리를 사용한다.
즉시 로딩은 가급적 사용하지 않는 것이 좋다. 즉시 로딩을 적용하면 예상하지 못한 SQL(N+1문제)이 발생한다.
@ManyToOne, @OneToOne
은 기본 전략이 즉시 로딩이므로LAZY
로 설정해줘야 한다. (@OneToOne, @ManyToMany
는 기본 전략이 지연 로딩)
NULL 제약조건과 JPA 조인 전략
외부 조인보다 내부 조인이 성능과 최적화에서 유리하다. 외래키에 NOT NULL 제약 조건을 설정하면 내부 조인만 사용
@JoinColumn(nullable = true); // NULL 허용(기본값), 외부 조인 사용
@JoinColumn(nullable = false); // NULL 허용 안함, 내부 조인 사용
@ManyToOne(fetch = FetchType.EAGER, optional = false); // NULL 허용 안함, 내부 조인 사용
지연 로딩
지연 로딩을 사용하려면 @ManyToOne
의 fetch 속성을 FetchType.LAZY
로 지정해야 한다.
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
em.find(Member.class, "member")
가 호출됐을 때, Team은 프록시 객체로 대체된다. 실제로 Member 엔티티에서 Team을 호출할 때 DB에서 조회해온다. 조회 대상이 영속성 컨텍스트에 이미 있으면 프록시가 아닌 실제 객체를 사용한다.
대부분의 상황에서 가급적이면 지연 로딩을 사용하는 것이 좋다.
지연 로딩 활용
프록시와 컬렉션 래퍼
- 컬렉션 래퍼 : 하이버네이트가 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있는 경우, 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는 것.
- 지연 로딩에서 엔티티는 프록시 객체를 사용, 컬렉션은 컬렉션 래퍼(프록시 역할)을 사용.
JPA 기본 패치 전략
@ManyToOne, @OneToOne
:FetchType.EAGER
(즉시 로딩)@OneToOne, @ManyToMany
:FetchType.LAZY
(지연 로딩)
JPA 기본 패치 전략은 연관된 엔티티가 하나면 즉시 로딩, 컬렉션이면 지연 로딩을 사용한다. 모든 연관관계에 지연 로딩을 사용하는 것이 좋다.
컬렉션에 FetchType.EAGER
사용시 주의점
-
컬렉션을 하나 이상 즉시 로딩하는 것은 피하자. 컬렉션의 조인은 테이블로 보면 일대다 조인이다. 일대다 조인은 결과 데이터가 N쪽에 있는 수만큼 증가한다. A, B 두 테이블을 조인할 경우 A*B 개의 데이터가 반환된다.
-
컬렉션 즉시 로딩은 항상 외부 조인을 사용하자. 일대다 관계에서 내부조인을 사용하면 데이터가 조회되지 않을 수 있다.
애너테이션 | 속성 | 설명 |
---|---|---|
@ManyToOne, @OneToOne |
(fetch = FetchType.EAGER, optional = false) |
내부조인 |
@ManyToOne, @OneToOne |
(fetch = FetchType.EAGER, optional = true) |
외부조인 |
@OneToMany, @ManyToMany |
(fetch = FetchType.EAGER, optional = false) |
외부조인 |
@OneToMany, @ManyToMany |
(fetch = FetchType.EAGER, optional = true) |
외부조인 |
영속성 전이 CASCADE
- 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들기 위해 사용한다.
- 영속성 전이는 연관관계 매핑과는 아무 관련이 없다. 엔티티를 영속화할 때 연관 엔티티도 같이 영속화하는 편리함을 제공한다.
@Entity
public class Parent {
...
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
...
}
Child child = new Child();
Parent parent = new Parent();
chil.setParent(parent);
parent.getChildren().add(child);
// 부모, 연관된 자식 저장
em.persist(parent);
CascadeType 종류
속성 | 설명 |
---|---|
ALL | 모두 적용 |
PERSIST | 영속 |
MERGE | 병합 |
REMOVE | 삭제 |
REFRESH | Refresh |
DETACH | Detach |
PERSIST, REMOVE 는 em.persist()
나 em.remove()
가 실행될 때 바로 전이가 일어나지 않고, 영속성 컨텍스트를 플러시 할 때 전이가 발생한다.
고아 객체
- 고아 객체란 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 뜻한다.
- JPA는 고아 객체를 자동으로 삭제하는 기능을 제공한다. 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
...
}
Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(0);
// 실행 SQL
DELETE FROM CHILD WHERE ID = ?
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제를 수행한다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용된다. 이 때 DELETE SQL을 실행한다.
영속성 전이 + 고아 객체
CascadeType.ALL + orphanRemoval = true
옵션을 활성화하면 부모 엔티티를 통해 자식의 생명 주기를 관리할 수 있다.- DDD의 Aggregate Root 개념을 구현할 때 유용하다.
Comments