이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.
💡 지연로딩
엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법
📕 참고
하이버네이트에서는 지연 로딩을 지원하기 위한 방법으로 프록시 사용과 바이트코드 수정 두 가지 존재
EntityManager.find()
EntityManager.getReferences()
프록시 객체가 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는 것을 의미
xxx.getxxx()
을 호출해서 실제 데이터를 조회getxxx()
을 호출해서 결과 반환구조
위임(delegate)
프록시 객체는 처음 사용할 때 한 번만 초기화 됨
프록시 객체를 초기화하면 프록시 객체를 통해 실제 엔티티에 접근 가능
프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 함
영속성 컨텍스트에 찾는 엔티티가 이미 존재할 경우 em.getReference()
를 호출해도 실제 엔티티를 반환함
why? 데이터베이스를 조회할 필요가 없기 때문
초기화는 영속성 컨텍스트의 도움을 받아야 가능하므로, 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
//MemberProxy 반환
Member member = em.getReferencce(Member.class, "id1");
transaction.commit();
em.close(); //영속성 컨텍스트 종료
member.getName(); //준영속 상태 초기화 시도,
//org.hibernate.LazyInitializationException 예외 발생
@Access(AccessType.PROPERTY)
)로 설정한 경우를 제외하고는 프록시를 초기화 함PersistenceUnitUtil.isLoaded(Object entity)
프록시 인스턴스의 초기화 여부 확인
클래스 명 출력
..javassist..
가 붙어있으면 프록시임을 확인 가능📕 참고
- 하이버네이트의
initialize()
메소드를 사용하여 프록시 강제 초기화 가능
org.hibernate.Hibernate.initialize(order.getMember());
- JPA 표준
- 프록시 강제 초기화 메소드 존재 x
- 초기화 여부만 확인 가능
- 프록시의 메소드 직접 호출
em.find(Member.class, "member1")
를 호출할 때, 회원 엔티티와 연관된 팀 엔티티도 함께 조회@ManyToOne(fetch = FetchType.EAGER)
📕 NULL 제약조건과 JPA 조인 전략
- 외래 키가 NULL 값을 허용할 때 발생하는 문제를 고려하여 JPA는 외부 조인을 사용
- 성능과 최적화 면에서는 외부 조인보다 내부 조인이 더 유리
- 내부 조인 사용 방법
- 테이블 : 외래 키에
NOT NULL
제약 조건 설정 → 값 존재 보장- JPA :
@JoinColumn
에nullable = false
설정
📕 nullable 설정에 따른 조인 전략
@JoinColumn(nullable = true)
- NULL 허용(기본값)
- 외부 조인 사용
@JoinColumn(nullable = false)
- NULL 허용 x
- 내부 조인 사용
→@ManyToOne(optional = false)
로도 가능
JPA는 선택적 관계면 외부 조인을 사용하고 필수 관계면 내부 조인을 사용함
member.getTeam().getName()
처럼 조회한 팀 엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티 조회@ManyToOne(fetch = FetchType.LAZY)
💡 내장 컬렉션
org.hibernate.collection.internal.PersistentBag
xxx.getxxx()
을 호출해도 컬렉션은 초기화되지 않음member.getOrders().get(0)
fetch
속성의 기본 설정값
@ManyToOne
, @OneToOne
: 즉시 로딩(FetchType.EAGER
)@OneToMany
, @ManyToMany
: 지연 로딩(FetchType.LAZY
)JPA의 기본 fetch 전략
모든 연관관계에 지연 로딩을 사용하는 것을 권장
애플리케이션 개발이 어느 정도 완료단계에 도달했을 때, 실제 사용 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화
FetchType.EAGER
사용 시 주의점📕
FetchType.EAGER
설정과 조인 전략
@ManyToOne
,@OneToOne
- (optional = false) : 내부 조인
- (optional = true) : 외부 조인
@OneToMany
,@ManyToMany
- (optional = false) : 외부 조인
- (optional = true) : 외부 조인
CASCADE
옵션으로 영속성 전이 제공@Enity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<Child>();
}
CascadeType.REMOVE
를 설정하면 부모 엔티티와 자식 엔티티 함께 제거 가능Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
public enum CascadeType {
ALL, //모두 적용
PERSIST, //영속
MERGE, //병합
REMOVE, //삭제
REFRESH, //REFRESH
DETACH //DETACH
}
cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
CascadeType.PERSIST
와 CascadeType.REMOVE
는 플러시를 호출할 때 전이 발생@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
...
}
parent1.getChildren().clear()
;orpahnRemoval
은 @OneToOne
, @OneToMany
에서만 사용 가능CascadeType.REMOVE
를 설정한 것과 같음EntityManager.persist()
를 통해 영속화EntityManager.remove()
를 통해 제거CascadeType.ALL
+ orphanRemoval = true
동시 사용//자식을 저장하려면 부모에 등록만 하면 된다(CASCADE)
Paraent parent = em.find(Parent.class, parentId);
parent.addChild(child);
//자식을 삭제하려면 부모에서 제거하면 된다(orphanRemoval)
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);
📕 참고
영속성 전이는 DDD의 Aggregate Root 개념을 구현할 때 사용하면 편리하다.