Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 서블릿
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch2
- 게시글 목록 api
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch4
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc2 - 타임리프
- @Aspect
- 자바의 정석 기초편 ch12
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 검증
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch14
- 스프링 입문(무료)
- 자바의 정석 기초편 ch9
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch5
Archives
- Today
- Total
나구리의 개발공부기록
프록시와 연관관계 관리, 프록시, 즉시 로딩과 지연 로딩, 영속성 전이(CASCADE)와 고아 객체, 실전 예제 - 연관관계 관리 본문
인프런 - 스프링부트와 JPA실무 로드맵/자바 ORM 표준 JPA 프로그래밍 - 기본편
프록시와 연관관계 관리, 프록시, 즉시 로딩과 지연 로딩, 영속성 전이(CASCADE)와 고아 객체, 실전 예제 - 연관관계 관리
소소한나구리 2024. 10. 13. 18:19출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
** 실전예제 전까지는 DB URL 설정을 test로 설정하여 실습
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
** 실전예제는 jdbc.url 설정을 jpashop으로 변경해서 진행
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpashop"/>
1. 프록시
1) 프록시 기초
(1) em.getReference()
- em.find() 메서드는 DB를 통해서 실제 엔터티 객체를 바로 조회하는 것
- em.getReference() 메서드는 DB조회를 미루고 가짜(프록시) 엔터티 객체를 조회, 프록시에 값이 없으면 DB에서 값을 조회해옴
- getReference()메서드를 호출하는 순간 DB와 무관하게 영속성 컨텍스트에 프록시 객체가 생성됨
(2) 로그 확인
- 새로운 멤버를 생성하여 persist()로 영속성 컨텍스트에 저장
- em.flush(), em.clear()로 쿼리를 DB에 반영 후 영속성 컨텍스트를 초기화
- 이 후에 em.getReference를 호출하여 2번째 인자의 member.getId()로 원본 객체를 상속받아 프록시 객체를 생성(이때 Id값만 프록시 객체에 저장되고 다른 값은 모르기 때문에 비어있음)
- 그래서 id값은 프록시에 값이 있으므로 select 쿼리없이 값이 바로 출력됨
- 그러나 username은 프록시에 값이 없기 때문에 DB에 select 쿼리를 날려서 값을가져온 뒤에 출력됨
Member member = new Member();
member.setUsername("user");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("member.getId() = " + refMember.getId());
System.out.println("member.getUsername() = " + refMember.getUsername());
2) 프록시 특징
- 프록시 객체는 실제 클래스(엔터티)를 상속 받아서 만들어져서 실제 클래스와 겉 모양이 똑같아 이론상으로는 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(몇가지 주의점이 있음)
- hibernate가 내부적으로 여러 라이브러리를 가지고 프록시 객체를 생성함
- 프록시 객체는 실제 객체의 참조(target)를 보관하고 프록시 객체를 호출하면 프록시 객체가 실제 객체의 메소드를 호출함
(1) 프록시 객체의 초기화(원본 객체를 불러옴) 프로세스
- em.getReference로 프록시 객체를 호출한 뒤 프록시 객체에 없는 getName()을 프록시 객체에 요청하게 되면 프록시 객체가 영속성 컨텍스트에 초기화를 요청함
- 영속성 컨텍스트가 DB에서 값을 조회 후 실제 Entity를 생성하고 프록시가 가지고 있는 target에 실제 객체를 연결해줌(실제 객체의 참조를 보관함)
- 프록시가 target.getName()으로 실제 객체의 멤버의 이름을 조회해서 반환됨
- 이제 프록시 객체는 동일한 요청이오면 프록시에서 값을 반환하고, 프록시에 값이 없는값이 요청이오면 위와 같은 프로세스로 다시 동작하여 값을 반환함
- 즉 프록시 객체에 실제객체의 값이 없을 때 프록시가 초기화를 실행함
(2) 프록시가 실제 객체로 바뀌는 것이 아님
- 프록시 객체를 초기화 하면 프록시 객체가 실제 객체로 바뀌는 것이아니라 프록시 객체를 통해서 실제 엔터티에 접근하는 것임
- 생성한 멤버 객체를 프록시로 조회한 참조변수의 값의 class정보를 확인해보면 DB조회 전,후 모두 프록시 객체에서 값이 조회가 되고있음
- ...$HibernateProxy$임의값 : 프록시 객체의 참조값
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("before refMember.getClass() = " + refMember.getClass());
System.out.println("member.getUsername() = " + refMember.getUsername());
System.out.println("after refMember.getClass() = " + refMember.getClass());
(3) 타입체크 시 주의사항
- 프록시객체와 실제객체를 == 비교를 하면 각각 고유의 객체이기 때문에 false가 나옴
- instanceOf로 실제 비교하고자하는 엔터티와 맞는지 비교해야함
- 자주 사용하진 않지면, 어떤 것이 프록시의 객체인지 찾기가 쉽지 않을 수 있기 때문에 instanceOf로 값을 비교해야 함
- 당연히 jpa안에서 실제객체 == 실제객체, 프록시객체 == 프록시객체로 비교하면 true가 나옴
Member findMember = em.find(Member.class, member1.getId()); // db에서 member1 조회
Member refMember = em.getReference(Member.class, member2.getId()); // 프록시에서 member2 조회
// 다른 객체를 조회
System.out.println("findMember == refMember:" + (refMember == findMember)); // false
System.out.println("refMember == Member:" + (refMember instanceof Member)); // true
System.out.println("findMember == Member:" + (findMember instanceof Member)); // true
(4) 영속성 컨텍스트에 프록시가 찾는 엔터티가 있다면 실제 엔터티가 반환됨
- em.find()로 이미 값을 조회하여 영속성 컨텍스트값이 저장되어있고, 그이후에 같은 대상을 em.getReference()로 조회하면 프록시가아니라 실제 엔터티에서 값이 반환됨
- 반대로 먼저 em.getReference()로 값을 조회하여 이미 프록시 객체에 값이 있는 대상을 em.find()로 조회하면 실제 엔터티가아니라 프록시객체에서 값이 반환됨
/**
* em.find() 조회 후 같은 대상을 em.getReference()로 조회
*/
Member findMember = em.find(Member.class, member1.getId()); // em.find()
System.out.println("findMember.getClass() = " + findMember.getClass());
Member refMember = em.getReference(Member.class, member1.getId()); // em.getReference()
System.out.println("refMember.getClass() = " + refMember.getClass());
System.out.println("refMember == findMember = " + (refMember == findMember));
// 출력 결과
// findMember.getClass() = class jpabook.jpashoptest.domain.Member
// refMember.getClass() = class jpabook.jpashoptest.domain.
// refMember == findMember = true
/**
* em.getReference() 조회 후 같은 대상을 em.find()로 조회
*/
Member refMember = em.getReference(Member.class, member1.getId()); // em.getReference()
System.out.println("refMember.getClass() = " + refMember.getClass());
Member findMember = em.find(Member.class, member1.getId()); // em.find()
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("refMember == findMember = " + (refMember == findMember));
// 출력 결과
// findMember.getClass() = class jpabook.jpashoptest.domain.Member$HibernateProxy$NKxxgTnS
// select 쿼리 생략 ...
// findMember.getClass() = class jpabook.jpashoptest.domain.Member$HibernateProxy$NKxxgTnS
// refMember == findMember = true
** JPA가 이렇게 동작하는 이유
- JPA에서는 한 트랜잭션안에서 동작하는 객체는 == 비교를 했을때 항상 true가 되어야 하는 메커니즘이 있음(자바 컬렉션을 다루는 것처럼 하는 것이 핵심이기 때문에 동일한 객체임을 보장하게 설계되어있음)
- 즉, 한 트랜잭션 안에서는 처음 조회했을 때 프록시로 값을 가져왔으면 그 이후에 find()로 바로 조회하려고해도 프록시 객체를 통해서 실행되고, 처음 조회시 영속성 컨텍스트로 실제 객체에서 값을 가져왔으면 그 이후에 프록시로 조회하려고해도 실제 객체에서 조회하게 됨
- 그러므로 상황에따라 em.find()를해도 프록시에서 조회될 수 있고, em.getReference()를 해도 실제 객체에서 조회될 수 도 있는 상황이 있어서 어떤 객체로 값이 조회되든 문제가 없이 개발해야하는 것이 중요한데, JPA에서는 이걸 맞춰준다는 것이 포인트임
(5) 준영속 상태일 때 프록시를 초기화하면 문제 발생함
- 준영속 상태는 영속성 컨텍스트의 도움을 받을 수 없기 때문에 프록시를 초기화하면 예외를 터트림
- detach로 강제로 준영속상태로 만들거나 clear()로 영속성 컨텍스트를 초기화 후 프록시를 초기화 하려고 하면LazyInitializationException 예외가 발생함
- 이 문제는 JPA를 다룰 때 많이 마주하게되는 문제이기 때문에 왜 발생했는지 원인을 잘 기억해두는 것이 좋음(JPA를 다루다보면 특정 상황에서 준영속 엔터티를 다루는 상황이 발생 됨)
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
// 아래의 둘중에 하나를 적용해도 예외가 발생함
em.detach(refMember); // refMember를 준영속 상태로 만듦
em.clear(); // em.clear로 영속성 컨텍스트를 강제로 새로시작함
refMember.getUsername(); // LazyInitializationExcepion 예외 발생
3) 프록시 확인
(1) 프록시 인스턴스의 초기화 여부 확인
- emf.PersistenceUnitUtil.isLoaded(Object entity)
- 정확히 말하면, 실제 객체가 로드 되었을 경우에 true를 반환하는 메소드
- 즉, 최초에 영속성 컨텍스트에서 값이 조회가 된경우에는 이미 완전히 실제 객체가 로드된 상황이므로 항상 true이고, 최초에 프록시를 통해서 값이 조회된 상황인 경우, 프록시를 초기화 하지 않았을 때는 false가 초기화를 하여 실제 객체가 로드가 되어야 true가 나옴
(2) 프록시 클래스 확인 방법
- entity.getClass()로 직접 출력하여 프록시클래스로 동작하고 있는지 확인할 수 있음
(3) 프록시 강제 초기화
- Hibernate.initialize(entity);
- 하이버네이트가 지원하는 메서드로 강제로 프록시 객체를 초기화 할 수 있음
- 참고로 JPA 표준에는 프록시 강제 초기화가 없어서 refMeber.getUsername()로 프록시에 없는 값을 조회하는 형식으로 초기화 해야함
// ... 생성 코드
em.flush(); // DB에 SQL 반영
em.clear(); // 영속성 컨텍스트 리셋 -> 처음부터 다시 시작
// 프록시로 조회
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
// 둘중에 하나의 방법으로 프록시를 초기화
refMember.getUsername(); // 프록시에 없는값을 조회하여 프록시를 초기화
Hibernate.initialize(refMember); // refMember를 강제로 초기화
System.out.println("isLoaded(refMember) = " +
emf.getPersistenceUnitUtil().isLoaded(refMember)); // 초기화 여부 확인
// 출력 결과
// isLoaded(refMember) = true
// 만약 위의 초기화 코드들이 모두 주석인 상태라면 결과는 false가 됨
2. 즉시로딩과 지연로딩
1) 항상 Member를 조회할 때 Team도 함께 조회해야 할까?
- 비즈니스 로직상 Member를 조회할 때 Team정보를 가져와야 하는 상황이라면 문제 없음
- 그러나 단순히 멤버 정보만 사용하는 비즈니스 로직이 많은 상황이라고 가정하면 멤버를 조회할때 항상 join으로 팀까지 조인해서 가지고 오는 것은 최적화 입장에서는 손해라고 볼 수 있음
- 이러한 부분을 JPA는 프록시를 활용한 지연로딩으로 해결함
2) 지연 로딩
(1) 연관관계 매핑 옵션에 지연로딩(LAZY)을 사용
- Member와 Team과의 다대일 연관관계 매핑 애노테이션 옵션에 fetch = FetchType.LAZY로 적용하면, Member를 조회할 때 Team의 정보는 실제 객체가 아닌 초기화 되지 않은 프록시로 값을 조회함
- 예를 들어 find()로 Member만 조회하는 상황에서 Team은 프록시로 조회가되어 DB에 쿼리가 전송되지 않고 Team을 사용하는 순간(CRUD 등) 프록시가 초기화되어 DB에 쿼리를 전송하여 실제 객체에 쿼리 결과를 반영함
- 즉, Member만 조회할 때 항상 Team에 대한 정보를 조인으로 가져오는 것이 아니라 team.getName()처럼 Team에 대한 정보를 실제 사용할 때 프록시가 초기화를 수행하여 DB에 쿼리가 전송됨
@Entity
public class Member extends BaseEntity {
// ... 기존 코드 생략
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
3) 즉시 로딩
- 반대로, 대부분의 비즈니스 로직이 Member와 Team을 거의 함께 사용한다면 즉시로딩으로 함께 조회할 수 있음
(1) 연관관계 매핑 옵션에 즉시로딩(EAGER)을 사용
- 연관관계 매핑 애노테이션 옵션에 fetch = FetchType.EAGER를 사용하면, Member를 조회하는 즉시 Team에 대한 정보도 바로 join으로 조회함
- 즉, 두 객체 모두 프록시가아닌 실제 객체를 조회함
// ...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
4) 프록시와 즉시로딩 주의점 - 중요함
(1) 실무에서는 지연로딩만 사용할 것!
- 즉시 로딩을 적용하면 조회할때마다 조인 SQL이 계속 발생되는데, 간단한 데이터베이스 구조에서는 크게 성능 저하는 없을지라도 여러개의 테이블을 사용하는 실무에서는 수많은 관련된 여러 테이블에 조인이 발생하기 때문에 최적화 문제가 발생할 수 있음(DBA의 연락을 즉시 받게 될 것)
- 또한 실무에서는 복잡한 쿼리를 해결하기 위해 JPQL를 사용하는데 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
- jpql을 사용하면 작성한 jpql이 sql로 번역되어 실행 후 데이터를 가지고오는데, 가지고온 데이터가 즉시로딩으로 되어있으면 또 데이터를 가지고 오기위해 조회를 발생하게되어 쿼리가 한번 더 나가게 됨
- 만약 jpql로 가지고온 데이터에서 즉시로딩으로 한번 더 쿼리로 가져와야 할데이터가 N개라면(영속성 컨텍스트에 있는 데이터는 다시 발생하는 쿼리에서 제외가 됨) jpql에서 발생된 쿼리 1번, N개의 멤버에서 팀을 조회하는 쿼리 N번이 발생하는 N+1문제가 발생함
- 물론 조회한 팀을 다시 Loop로 돌리면 프록시가 초기화되어 쿼리가 계속 나가게 되는데, 이때는 jpql에서 사용하는 fetch join으로 한방에 쿼리를 가져오도록 하거나 @EntityGraph 애노테이션 혹은 배치사이즈(1+1쿼리로 문제해결)로 문제를 해결할 수 있음
(2) 연관관계 애노테이션 로딩 기본값
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY로 변경해서 사용(~One으로 끝나는 연관관계 매핑 애노테이션)
- @OneToMany, @ManyToMany는 기본이 지연로딩이므로 그대로 사용하면 됨(~Many로 끝나는 연관관계 매핑 애노테이션)
5) 지연 로딩 정리
(1) 이론적으로는
- 두개의 연관관계 매핑이된 엔터티가 함께 자주 사용된다면 즉시로딩을 적용
- 두개의 연관관계 매핑이된 엔터티가 가끔 사용된다면 지연 로딩을 적용
(2) 실무에서는
- 이론적인 내용은 상관없이 무조건 모든 연관관계에 지연 로딩을 사용하고 실무에서 즉시로딩은 절대 사용하지말 것
- 지연로딩으로 설정 후 발생되는 n+1문제는 이후에 JPQL fetch join, @EntityGraph기능을 사용해서 해결 -> 이후에 배움
- 즉시 로딩은 상상하지 못한 쿼리가 나감
3. 영속성 전이(CASCADE)와 고아 객체
- 즉시로딩, 지연로딩, 연관관계 세팅과 전혀 관계가 없음
1) 영속성 전이: CASCADE
- 특정 엔터티를 영속 상태로 만들 때 연관된 엔터티도 함께 영속 상태로 만들고 싶을 때 사용함
- 예를 들어 부모 엔터티를 저장할 때 자식 엔터티도 함께 저장하고자 하는 상황 등에서 사용함
(1) Parent 생성
- Child엔터티와 일대다 연관관계 매핑을 적용 후 연관관계의 주인을 parent로 설정
- cascade 옵션을 CascadeType.ALL로 적용하여 부모엔터티가 변경될 때 자식 엔터티도 항상 변경되도록 설정
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "parent_id")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
// 연관관계 편의 메서드
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
// ... getter, setter
}
(2) Child 생성
- Parent 엔터티와 다대일 연관과께 매핑을하고 외래키를 parent_id로 설정
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
// ... getter, setter
}
(3) JpaMain - cascade 적용 결과 확인
- 애플리케이션 적용 코드에서 Parent와 Child를 각각 생성하고 부모의 객체만 persist 하여 영속상태로 만들면 자식 객체들도 모두 DB에 반영되어있음
- 만약 cascade옵션을 적용하지 않았다면, 생성된 각각의 객체에 em.persist를 적용하여 3번 호출해야 DB에 생성한 값들이 반영됨
// ...
try {
Parent parent = new Parent();
Child child = new Child();
Child child2 = new Child();
parent.addChild(child);
parent.addChild(child2);
em.persist(parent);
tx.commit();
} // ...
** 주의사항
- 영속성 전이는 연관관계를 매핑하는 것과 아무런 관련이 없음 - 오해하는 경우가 종종 있음
- 엔터티를 영속화 할 때 연관된 엔터티를 편리하게 함께 영속화 할 수 있도록 기능을 제공할 뿐임
(4) CASCADE의 종류
- ALL과 PERSIST 두개의 옵션은 실무에서 자주 사용하는 편, 그 외의 옵션들은 거의 사용하지 않거나 전혀 사용하지 않음
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: 새로고침
- DETACH: 준영속
** 참고
- 일대다, 다대일의 상황에서 모두 cascade를 적용해야하는 것은 아님
- 단일 엔터티에 완전히 종속된 상태인 엔터티들관의 관계에서만 적용해야하며, 두 엔터티의 라이프사이클이 유사할 때 사용해야 함(단일 소유자의 관계에서만 적용)
- 예를들어 Parent와 Child가 연관관계로 맺어져있는데 Child가 Parent에만 종속되어있는 것이 아니라 다른 Member나 Order등에도 여러곳에서 쓰이게 되는 경우에는 cascade를 절대 사용하면 안됨
- 전혀 Parent와 관계없는 Member를 통해 값이 변경되면 전혀 상관없는 Parent도 값이 변경되면 운영이 불가능 하게 됨
2) 고아 객체 삭제
- 부모 엔터티와 연관관계가 끊어진 자식엔터티를 고아 객체라고 하는데, 이를 자동으로 삭제하는 옵션이 있음
(1) Parent
- @OneToMany 애노테이션 옵션에 orphanRemoval = true를 적용
- 해당 옵션을 적용하면 고아 객체를 자동으로 삭제하는 DELETE 쿼리가 전송됨
@Entity
public class Parent {
// ... 기존 코드 생략
@OneToMany(mappedBy = "parent" , cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
// ... 기존 코드 생략
}
(2) JpaMain - 자식 객체를 제거
- find로 조회한 엔터티에서 0번째 index의 자식의 객체를 삭제하면 delete쿼리가 전송되어 해당 객체를 DB에서 완전히 지움
// ...
try {
Parent parent = new Parent();
Child child = new Child();
Child child2 = new Child();
parent.addChild(child);
parent.addChild(child2);
em.persist(parent);
Parent parent1 = em.find(Parent.class, parent.getId());
parent1.getChildList().remove(0);
tx.commit();
} // ...
** 주의사항
- 참조가 제거된 엔터티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이므로 참조하는 곳이 하나일 때 사용해야함
- 즉 cascade와 마찬가지로 특정 엔터티가 개인소유할 때 사용해야 함
- @OneToOne, @OneToMany만 사용 가능함
- 개념적으로 부모를 제거하면 자식은 고아가되기 때문에 해당 옵션을 활성화 하면 활성화 하면 부모를 제거할 때 자식도 함께 제거되는CascadeType.REMOVE처럼 동작하게 됨
// Parent 클래스 - cascade 미적용
// ...
@OneToMany(mappedBy = "parent" , orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
// JpaMain 클래스 - 조회한 parent1 엔터티를 삭제
try {
Parent parent = new Parent();
Child child = new Child();
Child child2 = new Child();
parent.addChild(child);
parent.addChild(child2);
em.persist(parent);
em.persist(child);
em.persist(child2);
em.flush();
em.clear();
Parent parent1 = em.find(Parent.class, parent.getId());
em.remove(parent1);
tx.commit();
// ...
- 부모 객체를 삭제해버리니 자식 객체의 모두가 삭제되는 로그가 확인 됨
(3) 영속성 전이 + 고아 객체의 생명주기
@OneToMany(mappedBy = "parent" , cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
- 각각 스스로 생명주기를 관리하는 엔터티는 persist()로 영속화하고 remove()로 각각 제거할 수 있음
- 그러나 영속성 전이와 고아 객체를 함께 사용하면 부모 엔터티를 통해서 자식의 생명주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용함
4. 실전 예제 - 연관관계 관리
1) 글로벌 페치 전략 설정
- 모든 연관관계를 지연 로딩으로 설정
- 모든 엔터티에 적용된 @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연로딩으로 변경
// Category 클래스
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PARENT_ID")
private Category parent;
// Delivery 클래스
// ...
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
// Order 클래스
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
// OrderItem 클래스
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ORDER_ID")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
2) 영속성 전이 설정
- Order -> Delivery관계와 Order -> OrderItem관계를 CascadeType.ALL설정하여 주문정보가 CRUD가 되면 배송정보와 주문상품도 자동으로 적용 되도록 설정
// Order 클래스
// ...
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
** 참고
- 영속성 전이의 적용은 비즈니스 상황에따라 고민을 해보고 적용해야함
- 만약 딜리버리에 대한 생명주기를 따로 관리하고 싶다거나, 딜리버리의 로직이 복잡하거나하는 등의 여러가지 상황에서는 별도로 생명주리를 관리하여 persis를 하는 것이 맞을 수도 있음