프록시
Member를 조회할 때 Team도 함께 조회해야 할까?
코드예시
회원과 팀 함께 출력
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
System.out.println("소속팀: " + team.getName());
}
member와 team을 같이 가져와서 사용하고 싶은 경우
회원만 출력
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
}
member만 가져오고 싶은 경우
프록시 기초
- em.find() vs em.getReference()
- JPA에는 em.getReference()라는 참조를 가져오는 메서드가 존재한다.
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
DB로 조회 쿼리를 안 보냈는데 객체가 조회된다.
getReference()를 통해 얻은 객체는 진짜 멤버 객체가 아니다.
하이버네이트가 본인 내부 라이브러리를 사용해서 가짜, 속칭 프록시라고 하는 가짜 엔티티 객체를 반환한다.
위 이미지를 보면 target이 진짜 엔티티 객체를 가리킨다.
프록시 특징
실제 엔티티 클래스를 상속 받아서 만들어진다.
그래서 실제 클래스와 겉모양이 같다.
하이버네이트가 내부적으로 여러 프록시 라이브러리를 사용하여 실제 엔티티 클래스를 상속 받은 프록시 객체를 생성한다. 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.(이론상)
프록시 객체는 어떻게 생겼는지 살펴보자.
프록시 객체는 실제 객체의 참조(target)를 보관한다.
예를 들어 getName()을 호출하면 target이 가리키는 실제 엔티티 객체의 getName()을 대신 호출한다.
프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드 호출
프록시 객체의 초기화
Member member = em.getReference(Member.class, “id1”);
member.getName();
getReference()를 통해 프록시 객체를 가져왔다.
getName() 호출 시 처음에는 Member target에 값이 없어 영속성 컨텍스트에 초기화를 요청한다.
예를들어 member.getName()을 호출하면 JPA가 영속성 컨텍스트에 진짜 member 객체를 가져오라고 요청한다.
그러면 영속성 컨텍스트는 DB를 조회해서 실제 엔티티 객체를 생성해서 준다.
그리고 target에 생성한 실제 엔티티 객체를 연결해 준다.
그래서 프록시의 getName()을 했을 때 target이 가리키는 진짜 Member 객체의 getName()을 호출한다.
코드예시
Member member = new Member();
member.setName("hello");
em.persist(member);
em.flush();
em.clear();
Member referenceMember = em.getReference(Member.class, member.getId());
System.out.println("referenceMember = " + referenceMember.getClass()) ;
System.out.println("referenceMember.id = " + referenceMember.getId());
System.out.println("referenceMember.name = " + referenceMember.getName());;
실행결과
referenceMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member$HibernateProxy$rGb4qv6n
referenceMember.id = 1
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
referenceMember.name = hello
프록시의 특징
프록시 객체는 처음 사용할 때 한 번만 초기화된다.
프록시 객체를 초기화할때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 거다.
코드예시
Member referenceMember = em.getReference(Member.class, member.getId());
System.out.println("before referenceMember = " + referenceMember.getClass()) ;
System.out.println("referenceMember.name = " + referenceMember.getName());
System.out.println("after referenceMember = " + referenceMember.getClass()) ;
실행결과
before referenceMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member$HibernateProxy$oFik5HkX
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
referenceMember.name = hello
after referenceMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member$HibernateProxy$oFik5HkX
실행결과를 보면 before나 after나 똑같은 프록시 객체인걸 확인할 수 있다.
프록시 객체는 원본 엔티티를 상속받는다.
따라서 타입체크 시 주의해야 한다. (== 비교 실패, 대신 instance of 사용)
Member member1 = new Member();
member1.setName("hello1");
em.persist(member1);
Member member2 = new Member();
member2.setName("hello2");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member referenceMember = em.getReference(Member.class, member2.getId());
System.out.println("m1 == referenceMember : " + (m1.getClass() == referenceMember.getClass()));
// 실행결과
// m1 == referenceMember : false
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
코드예시
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
Member m3 = em.find(Member.class, member1.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
System.out.println("m1 == m3 : " + (m1 == m3));
Member referenceMember = em.getReference(Member.class, member2.getId());
System.out.println("m1 == referenceMember : " + (m1.getClass() == referenceMember.getClass()));
System.out.println("m1 = " + m1.getClass());
System.out.println("referenceMember = " + referenceMember.getClass());
// 실행결과
// m1 == m2 : true
// m1 == m3 : true
// m1 == referenceMember : true
// m1 = class jpa_basic.ex1_hello_jpa.hellojpa.Member
// referenceMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member
멤버를 영속성 컨텍스트에 저장해서 1차 캐시에 존재 하는데 그걸 굳이 프록시로 가져오는거는 이점이 없다.
그리고 한 영속성 컨텍스트 안에서 똑같은 PK로 조회한 값에 대한 == 비교는 항상 true이다.
그래서 영속성 컨텍스트에 실제 객체가 존재할 경우 프록시가 아니라 실제 엔티티 객체를 반환한다.
위에서 한 영속성 컨텍스트 안에서 똑같은 PK로 조회한 값에 대한 == 비교는 항상 true이다라고 얘기 했다.
아래 예시를 살펴보자.
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy
Member findMember = em.find(Member.class, member1.getId()); // Member
System.out.println("findMember = " + findMember.getClass());
System.out.println("refMember == findMember: " + (refMember == findMember));
실행결과
refMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member$HibernateProxy$wKB86sSp
findMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member$HibernateProxy$wKB86sSp
refMember == findMember: true
refMember는 getReference()로 찾은 프록시고 findMember는 find()로 찾은 실제 member 객체다.
하지만 실행 결과를 보면 둘 다 프록시 객체다.
이유는 앞서 말한 true를 지키기 위해서다. 그래서 프록시를 한번 조회하면 em.find()를 해도 프록시를 반환한다.
정리하면 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
그리고 반대로 프록시를 이미 조회했으면 em.find()를 해도 프록시가 반환된다.
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
try {
Member member1 = new Member();
member1.setName("hello1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
em.detach(refMember);
refMember.getName();
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
실행결과
refMember = class jpa_basic.ex1_hello_jpa.hellojpa.Member$HibernateProxy$dbJag3BA
20:36:37.471 [main] INFO org.hibernate.orm.connections.pooling -- HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]
org.hibernate.LazyInitializationException: could not initialize proxy [jpa_basic.ex1_hello_jpa.hellojpa.Member#1] - no Session
프록시 초기화는 영속성 컨텍스트를 통해 초기화 작업을 진행하는데 중간에 영속성 컨텍스트에서 refMember가 준영속 상태가 되서 org.hibernate.LazyInitializationException 예외가 터진다.
보통 트랜잭션 시작하고 끝날 때 영속성 컨텍스트도 시작하고 끝나도록 맞춘다.
그러면 트랜잭션 끝나고 나서 프록시를 조회하면 위 예외가 발생한다.
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
- PersistenceUnitUtil.isLoaded(Object entity)
- 프록시 클래스 확인 방법
- entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
- 프록시 강제 초기화
- org.hibernate.Hibernate.initialize(entity);
- 참고: JPA 표준은 강제 초기화 없음
- 강제 호출: member.getName()
즉시 로딩과 지연 로딩
Member를 조회할 때 Team도 함께 조회해야 할까?
단순히 member 정보만 사용하는 비즈니스 로직인데 team 정보도 가져오면 손해다.
그래서 jpa는 지연 로딩 LAZY을 사용해서 프록시로 조회하는 방식을 제공한다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
테스트 코드
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setName("hello1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass());
실행결과
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.TEAM_ID
from
Member m1_0
where
m1_0.MEMBER_ID=?
m = class jpa_basic.ex1_hello_jpa.hellojpa.Team$HibernateProxy$zl2TI4Ed
em.find(Member.class, member1.getId());를 통해 member를 조회했다.
그리고 System.out.println("m = " + m.getTeam().getClass()); 코드를 실행하니 프록시 객체가 출력된 걸 볼 수 있다. 아래 코드를 추가하고 실행결과를 살펴보자.
System.out.println("==============");
m.getTeam().getName();
System.out.println("==============");
실행결과
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.TEAM_ID
from
Member m1_0
where
m1_0.MEMBER_ID=?
m = class jpa_basic.ex1_hello_jpa.hellojpa.Team$HibernateProxy$3Z1bYFjP
==============
Hibernate:
select
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
==============
현재 로딩 방식은 지연로딩이다.
그래서 em.find(Member.class, member1.getId()); 를 통해 첫 번째 select 쿼리가 DB로 전송된다.
그다음 지연로딩 방식이라 member을 조회하면서 member의 team에는 프록시 객체가 저장된다.
그러고 나서 m.getTeam().getName(); 할 경우 프록시 초기화가 발생한다.
이를 통해 DB에 team에 대한 데이터를 조회하는 쿼리가 전송된다.
정리하면 지연 로딩으로 세팅하면 연관된 것을 프록시로 가져온다.
그림으로 다시 한번 정리해보자.
내부 메커니즘
지연 로딩 LAZY을 사용해서 프록시로 조회
Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회)
em.find(Member.class, 1L);를 해서 member를 가지고 왔을 때 member와 team의 연관관계가 지연로딩으로 세팅되어 있으면 가짜 프록시 개체를 team에 저장한다. 그 다음 member에서 team을 가져와 team의 실제 데이터를 사용하는 시점에 team 데이터를 조회하는 쿼리가 발생한다. m.getTeam()과 같이 team 객체 자체를 가져올때가 아니라 m.getTeam().getName()처럼 team에 있는 데이터를 실제 사용할때 프록시 초기화가 발생한다.
Member와 Team을 자주 함께 사용한다면?
즉시 로딩 EAGER를 사용해서 함께 조회
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
테스트 코드
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setName("hello1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass());
실행결과
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
m = class jpa_basic.ex1_hello_jpa.hellojpa.Team
즉시 로딩을 사용할 경우 member만 조회했는데 연관관계가 있는 team까지 같이 join 해서 데이터를 가져온다.
그래서 System.out.println("m = " + m.getTeam().getClass()); 을 보면 프록시가 아닌 실제 team 객체가 출력된 걸 볼 수 있다. 실제 team 객체를 가져왔으니 team에 대한 프록시 초기화를 할 필요 없다.
member를 조회할 때 team까지 같이 조인해서 가지고 온다라고 이해하면 된다.
즉시 로딩(EAGER), Member조회 시 항상 Team도 조회
JPA 구현체는 가능하면 조인을 사용해서 SQL 한 번에 함께 조회
프록시와 즉시로딩 주의
가급적 지연 로딩만 사용(특히 실무에서), 실무에서는 즉시 로딩을 사용하지 말자.
즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
- 테이블이 1,2개면 상관없다. 근데 테이블이 수십 개가 되는 순간 얘기가 달라진다.
- 연관관계에 있는 테이블이 5개만 해도 join이 5번 나가서 데이터를 다 가져오게 된다.
- 그래서 예상치 못한 SQL이 발생한다.
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
@ManyToOne, @OneToOne 같이 @xToOne 들은 기본이 즉시 로딩 -> LAZY로 설정하자
@OneToMany, @ManyToMany는 기본이 지연 로딩
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다에 대해 좀 더 알아보자.
예시코드
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);
Team team2 = new Team();
team2.setName("teamA");
em.persist(team2);
Team team3 = new Team();
team3.setName("teamA");
em.persist(team3);
Member member1 = new Member();
member1.setName("hello1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member();
member2.setName("hello1");
member2.setTeam(team2);
em.persist(member2);
Member member3 = new Member();
member3.setName("hello1");
member3.setTeam(team3);
em.persist(member3);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
tx.commit();
실행결과
Hibernate:
/* select
m
from
Member m */ select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.TEAM_ID
from
Member m1_0
Hibernate:
select
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
Hibernate:
select
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
Hibernate:
select
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
우선 JPQL은 SQL로 번역된다.
현재 JPQL은 select m from Member m이다.
이게 SQL로 번역되면 select * from Member이고 이 쿼리가 DB로 전송된다.
이 쿼리가 전송되면 member 데이터를 다 가지고 오게 된다.
근데 member 코드를 보니까 아래와 같이 team과 연관관계에 있는데 즉시 로딩으로 설정되어 있다.
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
즉시로딩이라 member를 가져올 때 각 member에 있는 team에도 값이 저장되어야 한다.
그래서 team의 데이터를 조회하기 위한 select 쿼리문들이 추가로 전송된다.
근데 여기서 추가로 전송되는 쿼리의 개수가 N + 1이다.
예를 들어 member 데이터들은 한 번에 조회를 한다.
근데 조회한 member 객체의 개수가 10개면 각 member 객체마다 team을 찾는 select 쿼리가 발생한다.
N + 1에서 1은 처음 member를 조회하기 위한 최초 쿼리이고 N은 최초 쿼리 때문에 발생한 추가 쿼리 개수가 N개라는 의미다.
N + 1 해결법
모든 연관관계를 지연로딩으로 다 설정한다.
그다음 3가지 방법이 있다.
1. fetch 조인 사용 : 이 방식은 런타임에 동적으로 원하는 데이터들을 선택을 해서 조인 쿼리를 날려 한방에 가져오는 방식이다.
2. 엔티티 그래프 사용
3. 배치 사이즈 활용
지연 로딩 활용
- Member와 Team은 자주 함께 사용 -> 즉시 로딩
- Member와 Order는 가끔 사용 -> 지연 로딩
- Order와 Product는 자주 함께 사용 -> 즉시 로딩
member1 조회 시 member와 teamA는 EAGER라 한 번에 조인해서 조회한다.
List와는 LAZY라 주문내역은 프록시로 조회된다.
프록시를 사용 시 그때 상품 A를 조회해서 가져온다.
지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!(뒤에서 설명)
- 즉시 로딩은 상상하지 못한 쿼리가 나간다.
영속성 전이: CASCADE
cascade는 즉시 로딩, 지연 로딩, 연관관계 세팅과 전혀 관계없다.
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
영속성 전이: 저장
코드예시
@Entity
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
child.setParent(this);
childList.add(child);
}
}
@Entity
@Getter
@Setter
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
테스트 코드
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
실행결과
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Parent */insert
into
Parent (name, id)
values
(?, default)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Child */insert
into
Child (name, parent_id, id)
values
(?, ?, default)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Child */insert
into
Child (name, parent_id, id)
values
(?, ?, default)
cascade는 연관관계와 전혀 관계없다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
위 코드를 아래와 같이 해석하자.
parent를 persist() 할 때 cascade를 선언한 밑에 있는 애들(여기서는 컬렉션 안에 있는 child 요소들)도 다 persist() 한다는 의미다.
정리
모든 엔티티는 기본적으로 저장하고 싶으면 엔티티당 각각 무조건 persist()를 호출해야한다.
하지만 cascade의 ALL 옵션을 사용하면 특정 엔티티를 영속 상태(저장)로 만들 때 연관된 엔티티도(컬렉션에 있는 요소들) 같이 persist()를 호출한다.
ALL 옵션 사용 안하는 경우
persist(OrderItemA)
persist(OrderItemB)
persist(OrderItemC)
persist(Order)
ALL 옵션 사용하는 경우
persist(Order)
영속성 전이: CASCADE - 주의!
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
CASCADE의 종류
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
실무에서 많이 쓰이고 주로 ALL과 PERSIST 옵션을 사용한다.
라이프사이클을 완전히 맞춰야 하는 경우는 ALL 옵션을 저장할 때만 라이프사이클을 맞춰야 하는 경우는 PERSIST 옵션을 사용한다.
주로 사용하는 case
CASCADE 옵션은 하나의 부모만이 자식들을 관리할 때 사용하는 게 좋다.
자식 엔티티가 다른 부모 엔티티 말고 다른 엔티티와 연관이 있으면 안 된다.
부모 엔티티에 자식 엔티티가 완전히 종속적일 때 사용하면 무방하다.
- 부모와 자식의 라이프 사이클이 거의 유사할 때
- 부모가 자식에 대해 단일 사용자일 때
예를 들어 게시판과 첨부파일 같은 경우는 주로 사용한다.
왜냐면 첨부파일의 경로는 보통 한 게시판에서만 관리하기 때문이다.
쓰면 안 되는 case
첨부파일을 게시판만 관리하는 게 아닌 다른 엔티티에서 관리하는 경우에는 사용하지 않는 게 좋다.
고아 객체
고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
orphanRemoval = true
// parent
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
// 테스트 코드
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);
//자식 엔티티를 컬렉션에서 제거
실행결과
Hibernate:
/* delete for jpa_basic.ex1_hello_jpa.hellojpa.Child */delete
from
Child
where
id=?
위 코드 실행 시 DELETE FROM CHILD WHERE ID=? 쿼리가 발생한다.
고아 객체 - 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야 함!
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다.
따라서 고아 객체 제거 기능을 활성화 하면,부모를 제거할 때 자식도 함께 제거된다.
이것은 CascadeType.REMOVE처럼 동작한다.
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL + orphanRemoval=true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거할 수 있다.
- 라이프 사이클을 jpa의 영속성 컨텍스트를 통해서 한다는 의미다.
- 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
- parent는 jpa의 영속성 컨텍스트를 통해서 생명주기를 관리하고 있다.
- child의 생명 주기를 parent가 관리하고 있다.
- 이 의미는 child에 대해 dao나 repository가 없어도 된다는 의미다.
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
- DDD에는 repository는 Aggregate Root만 컨택하고 나머지는 repository를 만들지 않는 게 좋다라는 설명이 있다.
- Aggregate Root가 관리하는 밑에 애들은 repository를 따로 만들지 않고 Aggregate Root를 통해서 생명주기를 관리한다.
- 위 예시는 parent가 Aggregate Root고 child는 Aggregate Root가 관리하는 애다.
예시코드
@Entity
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
child.setParent(this);
childList.add(child);
}
}
@Entity
@Getter
@Setter
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
테스트 코드
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
// em.remove(findParent);
findParent.getChildList().remove(0);
실행결과
-- findParent.getChildList().remove(0); 일 경우
Hibernate:
/* delete for jpa_basic.ex1_hello_jpa.hellojpa.Child */delete
from
Child
where
id=?
-- em.remove(findParent); 일 경우
Hibernate:
/* delete for jpa_basic.ex1_hello_jpa.hellojpa.Child */delete
from
Child
where
id=?
Hibernate:
/* delete for jpa_basic.ex1_hello_jpa.hellojpa.Child */delete
from
Child
where
id=?
Hibernate:
/* delete for jpa_basic.ex1_hello_jpa.hellojpa.Parent */delete
from
Parent
where
id=?
CascadeType.ALL 과 orphanRemoval=true의 차이
CascadeType.All (PERSIST + REMOVE)는 엔티티간의 관계 단절 시 자식 엔티티를 삭제 하지 않는다.
반면 orphanRemoval = true 옵션은 관계 단절 시 자식 엔티티를 삭제한다.
관계 단절은 다음과 같은 코드를 말한다. -> parent.getChildList().remove(0);
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
'JPA > JPA 기본' 카테고리의 다른 글
10. 객체지향 쿼리 언어(JPQL) - 1 (0) | 2024.08.13 |
---|---|
9. 값 타입 (0) | 2024.08.12 |
7. 상속관계 매핑 (0) | 2024.08.10 |
6. 다양한 연관관계 매핑 (0) | 2024.08.10 |
5. 연관관계 매핑 기초 (0) | 2024.08.10 |