티스토리 뷰
1. 프록시
2. 즉시 로딩과 지연 로딩
3. 영속성 전이: CASCADE
4. 고아객체
5. cascade 와 orphanRemoval
1. 프록시
em.find() : 실제 엔티티 조회
em.getReference() : 프록시 객체 조회
1) em.getReference() 동작 과정
JPA 는 실제 엔티티(Memeber)를 상속받는 프록시 객체(MemberProxy)를 사용한다.
프록시 객체는 실제 객체의 참조(Memeber target)를 보관한다.
em.getReference() 를 통해 엔티티를 호출하면 프록시 객체를 반환하고
member.getName() 을 호출할 때 처럼 실제 엔티티 객체의 정보가 필요한 경우
영속성 컨텍스트에 Member target 의 초기화를 요청한다.
DB를 조회해 실제 엔티티 객체(member)를 초기화 하고
Member target 에도 참조값을 초기화 한다.
프록시 객체를 통해 데이터를 조회하면 실제 엔티티 객체의 메소드가 호출된다.
2) 프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님,
- 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
JPA는 트랜잭션 내에서 객체간의 동일성을 보장하기 때문에
em.getReference(), em.find 를 통해 객체를 조회하면 먼저 조회한 객체(프록시 혹은 원본)가
이 후 조회할 때에도 동일하게 조회된다.
(1) 원본 엔티티 객체를 먼저 조회할 때
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getName() = " + findMember.getName());
Member referenceMember2 = em.getReference(Member.class, member.getId());
System.out.println("referenceMember2 = " + referenceMember2.getClass());
System.out.println("referenceMember2.getId() = " + referenceMember2.getId());
System.out.println("referenceMember2.getName() = " + referenceMember2.getName());
em.reference() 로 조회해도 원본 객체를 반환한다.
(2) 프록시 객체를 먼저 조회할 때
Member referenceMember1 = em.getReference(Member.class, member.getId());
System.out.println("referenceMember1 = " + referenceMember1.getClass());
System.out.println("referenceMember1.getId() = " + referenceMember1.getId());
System.out.println("referenceMember1.getName() = " + referenceMember1.getName());
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getName() = " + findMember.getName());
em.find() 로 객체를 호출해도 프록시 객체를 반환한다.
따라서, 객체 간의 타입을 비교할 때는 == 비교 대신 instance of 를 사용해야 한다.
(비교할 대상이 프록시 객체일 수 있음)
findMember instance of Member
referenceMember instance of Member
3) 프록시 확인
프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
프록시 클래스 확인 방법 : entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);
참고: JPA 표준은 강제 초기화 없음 : 강제 호출: member.getName()
2. 즉시 로딩과 지연 로딩
Member - Team 엔티티의 연관관계가 ManyToOne 으로 매핑되어 있을 때
Member 를 조회하면 Team 도 함께 조회하도록 할지 (즉시로딩)
Team 의 데이터를 조회할 때 쿼리를 실행해서 Team 데이터를 조회할지 (지연로딩)
연관관계의 FetchType 으로 설정할 수 있다.
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
1) 지연 로딩
Member 를 조회하면 Team 을 프록시 객체로 설정한다.
Team 을 실제 사용하는 시점에 조회 쿼리를 실행하고 Team 프록시 객체를 초기화 한다.
@ManyToOne(fetch = FetchType.LAZY)
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Team team1 = new Team();
team1.setName("team1");
Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team1);
em.persist(team1);
em.persist(member1);
em.flush();
em.clear();
Member findMember1 = em.find(Member.class, member1.getId());
Team findTeam = findMember1.getTeam();
System.out.println("team = " + findMember1.getTeam().getClass());
System.out.println("team name = " + findTeam.getName());
findMember1.getTeam() 에서 조회되는 객체가 프록시 객체이고
findTeam.getName() 시점에 프록시 객체 초기화가 일어난다.
2) 즉시 로딩
비즈니스 로직에서 대부분 Member와 Team 을 함께 사용한다면
즉시 로딩으로 설정해 Member 가 조회될 때 Team 도 함께 조회할 수 있도록 설정.
@ManyToOne(fetch = FetchType.EAGER)
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Team team1 = new Team();
team1.setName("team1");
Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team1);
em.persist(team1);
em.persist(member1);
em.flush();
em.clear();
Member findMember1 = em.find(Member.class, member1.getId());
Team findTeam = findMember1.getTeam();
System.out.println("team = " + findMember1.getTeam().getClass());
System.out.println("team name = " + findTeam.getName());
em.find(Member.class, member1.getId()) 시점에 TEAM 을 조인해서 조회한다.
findTeam 객체는 프록시가 아닌 실제 객체이다.
3) 프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용(특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
- @OneToMany, @ManyToMany는 기본이 지연 로딩
(1) 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
Team team1 = new Team();
team1.setName("team1");
Team team2 = new Team();
team2.setName("team2");
Team team3 = new Team();
team3.setName("team3");
Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team1);
Member member2 = new Member();
member2.setName("member2");
member2.setTeam(team2);
Member member3 = new Member();
member3.setName("member3");
member3.setTeam(team3);
em.persist(team1);
em.persist(team2);
em.persist(team3);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
JPQL 로 Member List 를 조회하면 SELECT MEMBER 쿼리가 실행된다.
이 후 Team 엔티티와 즉시 로딩으로 설정되어 있기 때문에
리스트에 들어있는 모든 Member 의 Team(영속성 컨텍스트에 없는 데이터) 을 각각 DB에서 조회한다.
4) 지연 로딩 활용
비즈니스 로직에 따라 즉시로딩을 설정할 수 있지만 실무에서는 지연로딩을 사용하자.
결론
모든 연관관계를 지연 로딩으로 설정한다.
필요한 경우에만 JPQL의 fetch join 이나 엔티티 그래프를 사용하자.
3. 영속성 전이: CASCADE
연관관계 매핑과 관계없이 casecade 를 설정한 필드에 저장된 객체에 영향을 준다.
1) OneToMany 에 설정
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
Parent 클래스에서 childList 에 cascade 를 All 로 설정하면
Parent 객체를 영속화할 때 childList 에 저장되어 있는 객체들을 함께 영속화 하겠다는 의미.
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
@Entity
@NoArgsConstructor
@Getter
@Setter
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);
}
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
//em.persist(child1);
//em.persist(child2);
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.remove(parent);
2) ManyToOne 에 설정
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "PARENT_ID")
private Parent parent;
Child 객체를 영속화할 때 Child.parent 에 저장되어 있는 객체도 함께 영속화
PARENT 가 부모 테이블이기 때문에 먼저 insert
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
//em.persist(parent);
em.persist(child1);
//em.persist(child2);
3) CASCADE 유의사항
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
Parent 와 Child 가 완전히 종속적인 경우, life cycle 이 동일한 경우, 단일 소유자(Parent 만 Child 를 소유할 때)인 경우
CASCADE 설정이 관리하기에 편리하지만 (ALL, PERSIST 정도 사용)
그렇지 않은 경우 사용하지 말자
4. 고아객체
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함.
- 특정 엔티티가 개인 소유 할 때만 사용해야 함.
- @OneToOne, @OneToMany만 가능
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고
아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께
제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
childList 에서 remove 되는 객체를 DB 에서 delete 되도록 설정
CascadeType.REMOVE처럼 동작
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(child1);
em.persist(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
findParent.getChildList().remove(0) 시점에 고아 객체가 DB에서 delete 되어야 하지만
실제 delete 쿼리가 실행되지 않음.
해당 내용을 검색해보니 하이버네이트 구현체에서는 이 동작에 버그가 있다고 한다.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
orphanRemoval 설정만으론 동작하지 않고
CascadeType.ALL 이나 CascadeType.PERSIST 를 함께 설정해 주어야 정상 동작한다.
하지만 실무에서 orphanRemoval 만 따로 적용하는 경우는 거의 없기 때문에 문제가 되진 않는다고 한다.
5. cascade 와 orphanRemoval
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Child 가 Parent 하나에만 종속적일 때(개인 소유)와 생명주기가 거의 같을때 사용하고
Parent 를 영속화 하면 childList 에 저장된 객체들을 함께 영속화 한다.
childList 에서 객체를 제거하면 고아 객체가 되기 때문에 DB 에서 삭제된다.
>> 부모 엔티티를 통해 자식 엔티티의 생명주기를 관리한다.
출처
https://www.inflearn.com/course/ORM-JPA-Basic 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)
'ORM > JPA' 카테고리의 다른 글
[JPA] 8. JPQL(1) (0) | 2022.03.29 |
---|---|
[JPA] 7. 값 타입 (0) | 2022.03.02 |
[JPA] 5. 고급 매핑 (0) | 2021.12.01 |
[JPA] 4. 다양한 연관관계 매핑 (0) | 2021.11.29 |
[JPA] 3. 연관관계 매핑 (0) | 2021.11.09 |
- Total
- Today
- Yesterday
- spring rest docs
- Ubiquitous Language
- MySQL
- 스프링 카프카 컨슈머
- 폴링 발행기 패턴
- H2
- clean code
- 마이크로서비스 패턴
- 계층형 아키텍처
- Stream
- 트랜잭셔널 아웃박스 패턴
- 클린코드
- kafka
- java8
- 육각형 아키텍처
- http
- 도메인 모델링
- mockito
- 학습 테스트
- ATDD
- Spring Data JPA
- HTTP 헤더
- Spring
- Spring Boot
- TDD
- 이벤트 스토밍
- JPA
- Git
- named query
- 스프링 예외 추상화
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |