[JPA] 스터디 2주차 - 8장 프록시와 연관관계 관리
JPA 스터디 : 8장 프록시와 연관관계 관리
2주차 연관관계와 매핑
- 8장 프록시와 연관관계 관리 : 288p-313p
8장. 프록시와 연관관계 관리
8.1 프록시
지연로딩을 사용하기 위해 실제 엔티티 객체 대신 프록시 객체
를 사용
- 지연 로딩 : 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연
8.1.1 프록시 기초
EntityManager.getReference()
: 엔티티를 실제 사용하는 시점까지 DB 조회 지연
- DB 접근을 위한 프록시 객체 반환
프록시 객체
- 상속 : 프록시 클래스는 실제 클래스를 상속
- 위임 : 프록시 객체는 실제 객체에 대한 참조 보관. 프록시 객체의 메소드 호출 시 프록시 객체는 실제 객체의 메소드를 호출
- 프록시 객체 초기화 : 실제 사용 때 DB에서 조회 후 실제 객체 생성하는 것을
프록시 객체 초기화
// MemberProxy 반환
Member member = em.getReference(Member.class, "member1");
member.getName(); // 1. getName();
class MamberProxy extends Member {
Member target = null; // 실제 엔티티 참조
public String getName() {
if (target == null) {
// 2. 초기화 요청
// 3. DB 조회
// 4. 실제 엔티티 생성 및 참조 보관
this.target = ...;
}
// 5. target.getName()
return target.getName();
}
}
프록시 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 교체되는 것 X.
- 프록시 객체는 원본 엔티티를 상속. 타입 체크 시에 주의해서 사용 필요
- getClass로 타입 비교 시 문제 발생 -> instanceof
- 영속성 컨텍스트에 찾는 엔티티가 이미 존재 시 em.getReference()를 호출해도 프록시가 아닌
실제 엔티티
반환 - 초기화는 영속성 컨텍스트의 도움을 받아야 가능.
- 준영속 상태 프록시 초기화 시 문제 발생 : org.hibernate.LazyInitializationException 발생
// MemberProxy 반환 Member member = em.getReference(Member.class, "id1"); transaction.commit(); em.close(); // 영속성 컨텍스트 종료 member.getName(); // 준영속 상태 초기화 시도 -> org.hibernate.LazyInitializationException 발생
- 준영속 상태 프록시 초기화 시 문제 발생 : org.hibernate.LazyInitializationException 발생
8.1.2 프록시 & 식별자
- 엔티티를 프록시로 조회 시 식별자 값을 파라미터로 전달. 프록시 객체는 식별자 값을 보관
- -> 식별자 값을 조회하는 .getId() 호출해도 프록시 초기화 X
- 단, 엔티티 접근 방식을
프로퍼티
@Access(AccessType.PROPERTY) 설정한 경우만 - 필드 @Access(AccessType.FIELD) 방식의 경우 프록시 객체 초기화
- 연관관계 설정 시 엔티티 접근 방식이 필드여도 프록시 초기화 X
8.1.3 프록시 확인
PersistenceUnitUtil.isLoaded(Object entity)
- 프록시 인스턴스 초기화 여부 확인 가능
- 클래스명에
..javassist..
라고 되어 있으면 프록시로 조회 의미
8.2 즉시 로딩과 지연 로딩
- 즉시 로딩 : 엔티티 조회 시 연관된 엔티티 함께 조회
- @ManyToOne(fetch = FetchType.EAGER)
- 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회
- @ManyToOne(fetch = FetchType.LAZY)
8.2.1 즉시 로딩
@Entity
public class Member {
//..
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
//..
}
- FetchType.EAGER
- 즉시 로딩을 처리하기 위해 가능하면 조인 쿼리 사용
- INNER JOIN vs OUTER JOIN
- 외래 키 null 가능할 경우 기본적으로 OUTER JOIN 사용
- @JoinColumn(nullable = true)
- INNER JOIN 사용하고 싶은 경우 : not null 조건 추가
- @JoinCloumn(nullable = false)
- 또는 @ManyToOne(optional = false) 적용
- 외래 키 null 가능할 경우 기본적으로 OUTER JOIN 사용
8.2.2 지연 로딩
- FetchType.LAZY
- 실제 데이터가 필요한 순간에 데이터베이스 조회 -> 프록시 객체 초기화
8.3 지연 로딩 활용
- 함께 자주 사용 되는 경우 : 즉시 로딩 / 그외 : 지연 로딩
@Entity
public class Member {
@Id
private String id;
//..
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders;
}
SELECT ...
FROM
MEMBER MEMBER0_
LEFT OUTER JOIN
TEAM TEAM1_ ON MEMBER0_.TEAM_ID=TEAM1_.ID
WHERE
MEMBER0_.ID="member1"
8.3.1 컬렉션 래퍼
- 엔티티에 컬렉션 존재 시 원본 컬렉션을 하이버네이트에서 제공하는 내장 컬렉션 (컬렉션 래퍼)으로 변경
- org.hibernate.collection.internal.PersistentBag
- member.getOrders() 호출해도 컬렉션 초기화 X
- member.getOrders().get(0) 처럼 실제 데이터를 조회할 때 초기화
8.3.2 JPA 기본 페지 전략
- 즉시 로딩 : @ManyToOne, @OneToOne
- 지연 로딩 : @OneToMany, @ManyToMany
- 모든 연관관계에 지연 로딩 사용 추천 -> 실제 사용 검토 후 꼭 필요한 곳에만 즉시 로딩 사용
8.3.3 컬렉션에 FetchType.EAGER 사용 시 주의점
- 컬렉션을 하나 이상 즉시 로딩하는 것은 권장 X
- 컬렉션 즉시 로딩은 항상 외부 조인을 사용
FetchType.EAGER
설정 & 조인 전략- @ManyToOne, @OneToOne
- (optional = false) : INNER JOIN
- (optional = true) : OUTER JOIN
- @OneToMany, @ManyToMany
- (optional = false) : OUTER JOIN
- (optional = true) : OUTER JOIN
- @ManyToOne, @OneToOne
8.4 영속성 전이 : CASCADE
- 영속성 전이를 사용하는 경우 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장 가능
- JPA에서 엔티티 저장 시 연관된 모든 엔티티는 영속 상태여야 함
8.4.1 영속성 전이: 저장
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
...
}
private static void saveWithCascade(EntityManager em) {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
child1.setParent(parent);
child2.setParent(parent);
parent.getChildren().add(child1);
parent.getChildren().add(child2);
// 부모 저장, 연관된 자식들 저장
em.persist(parent);
}
8.4.2 영속성 전이: 삭제
CascadeType.REMOVE
- 외래 키 제약 조건 고려 자식 삭제 -> 부모 삭제 순서
- CascadeType.REMOVE 설정 X, 부모 삭제 시
- 외래 키 제약 조건으로 DB 예외 발생
8.4.3 CASCADE 종료
public enum CascadeType {
ALL, // 모두 적용
PERSIST, // 영속
MERGE, // 병합
REMOVE, // 삭제
REFRESH, // REFRESH
DETACH // DETACH
}
- 여러 속성 함께 사용 가능
- cascade = {CascadeType.PERSIST, CascadeType.REMOVE}ㄴ
- flush 호출할 때 전이 발생
8.5 고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제됨
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
}
orphanRemoval = true
Parent parent1 = em.find(Parent.class, id); parent1.getChildren().remove(0);
DELETE FROM CHILD WHERE ID=?
- 참조하는 곳이 하나일 때만 사용 가능
- @OneTonOne, @OneToMany 에서만 사용 가능
- 부모 객체 제거 시 자식 객체도 함께 제거
- CascadeType.REMOVE 와 동일
8.6 영속성 전이 + 고아 객체, 생명주기
CascadeType.ALL + orphanRemoval = true 동시 사용
- 부모 엔티티를 통해서 자식의 생명주기 관기 가능
- DDD의 Aggregate Root 개념 구현 시 편의성 제공
참조
- 자바 ORM 표준 JPA 프로그래밍, 김영한 지음