출처
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
목차
프록시
다음과 같은 연관관계에서 Member를 조회할 때 Team도 함께 조회해야 할까요?
- 회원과 팀을 함께 출력하는 비지니스 로직이라면, em.find(Member.class, MemberId); 할 때 Member와 Team을 함께 찾아오는 것이 좋습니다.
- 하지만, 회원만 출력하는 로직이라면, DB에서 연관관계 매핑이 되어 있다고 Team 테이블을 조회하여 함께 가져오는 것은 손해일 수 있습니다. (사용하지 않는데 가져오는 것은 최적화가 되어 있지 않은 것)
⇒ 지연 로딩과 프록시를 사용하여 해결할 수 있습니다.
프록시 기초 (em.getReference())
- em.find() vs em.getReference()
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- em.getReference() 호출 시점에는 데이터베이스 Member 테이블에 SQL이 실행되지 않습니다.
- 이후 findMember.getName() 호출 시점에 SQL이 실행됩니다.
- System.out.println(findMember.getClass()); → hellojpa.Member$HibernateProxy$odcVHpjy ⇒ 프록시 객체
프록시 특징
- 실제 클래스를 상속 받아서 만들어집니다.
- 실제 클래스와 겉모양이 같지만, 내부는 비어있습니다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됩니다. (몇 가지만 조심해서 사용하면 됩니다.)
- 프록시 객체는 실제 객체의 참조(target)를 보관합니다.
- 프록시 객체를 호출하면, 프록시 객체는 실제 객체의 메소드를 호출합니다.
프록시 객체 초기화
Member member = em.getReference(Member.class, “id1”);
member.getName();
- em.getReference()하면, MemberProxy를 생성하는데, 해당 프록시에는 실제 객체의 참조(target)를 보관합니다.
- member.getName()이 호출이 될때, 프록시 객체는 영속성 컨텍스트에게 초기화를 요청합니다. 그리고 영속성 컨텍스트는 DB를 조회하여 실제 엔티티 객체를 생성합니다.
- 프록시는 target.getName()을 호출하여 값을 반환합니다.
프록시 정리 ⭐⭐⭐
- 프록시 객체는 처음 사용할 때 한 번만 초기화합니다. (이미 실제 엔티티를 가져왔다면, 가져온 엔티티를 사용합니다.)
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아닙니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능합니다.
- 프록시 객체는 원본 엔티티를 상속받습니다. 따라서 타입 체크 시 주의해야 합니다. (== 비교 실패, 대신 instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환합니다.
- 만약, 연속으로 프록시를 가져오면 같은 프록시가 반환이 됩니다. (프록시 객체 또한 영속성 컨텍스트에 저장되며, 같은 영속성 컨텍스트에서 가져온 객체는 == 비교 시 반드시 true를 반환합니다.)
Member m1 = em.getReference(Member.class, member1.getId());
System.out.println(m1.getClass()); //hellojpa.Member$HibernateProxy$odcVHpjy
Member m2 = em.getReference(Member.class, member1.getId());
System.out.println(m2.getClass()); //hellojpa.Member$HibernateProxy$odcVHpjy
//같은 영속성 컨텍스트에서 가져온 객체는 == 했을 때 True 여야 한다.
System.out.println("a == a: " + (m1 == m2)); //true
- 프록시 조회 후, em.find() 하면 어떻게 될까? em.find()도 프록시를 반환합니다.
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println(refMember.getClass()); //Proxy
Member findMember = em.find(Member.class, member1.getId());
System.out.println(findMember.getClass()); //Member가 아니라 Proxy 반환
System.out.println("refMember == findMember:" + (refMember == findMember)); //true
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생합니다. (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트립니다.)
Member refMember = em.getReference(Member.class, member1.getId());
em.detach(refMember);
//em.close();
//em.clear();
refMember.getUsername(); //LazyInitializationException 예외 발생
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println(emf.getPersistenceUnitUntil().isLoaded(refMember)); //false
refMember.getUsername(); //초기화 진행
System.out.println(emf.getPersistenceUnitUntil().isLoaded(refMember)); //true
- 프록시 클래스 확인
entity.getClass().getName(); //..javasist.. or HibernateProxy…
- 프록시 강제 초기화
Hibernate.initailize(refMember); //강제 초기화
참고 JPA 표준은 강제 초기화가 없습니다.
강제 초기화 호출 - member.getName()
즉시 로딩과 지연 로딩
다시 처음으로 돌아가서, 다음과 같이 단순히 member 정보만 사용하는 비지니스 로직에서 Member를 조회할 때 Team도 함께 조회해야 할까요?
println(member.getName());
프록시와 지연 로딩을 사용해서 Team이 필요한 경우에만 조회할 수 있습니다.
지연 로딩(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;
...
}
Member member = em.find(Member.class, 1L);
Team team = member.getTeam(); //getTeam()이 아닌 getName()할 때 초기화
team.getName(); // 실제 team을 사용하는 시점에 초기화 (DB 조회)
만약, Memer와 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;
...
}
즉시 로딩
- Member 조회 시 항상 Team도 함께 조회합니다.
- JPA 구현체는 가능하면 조인을 사용하여 SQL 한 번에 함께 조회합니다.
💡 프록시와 즉시 로딩 주의
가급적 지연 로딩만 사용해야 합니다. (특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생합니다.
Member m = em.find(Member.class, member1.getId());
- 위와 같이 Member만 조회하는 코드를 작성해도, 만약 즉시 로딩으로 Team이 연관되어 있다면 Team을 Join 하는 SQL이 발생합니다.
- 만약, 연관관계가 한 개가 아니라 수십 개라면? 그만큼 예상치 못한 SQL문 발생과 더불어, Join을 많이 하게 돼 성능이 나오지 않습니다.
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킵니다.
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
//SQL1: select * from Member; //JPQL은 작성된 SQL를 그대로 실행한다.
//SQL2: select * from Team where TEAM_ID = xxx //실행 후 EAGER로 되어 있는 각각 Member의 Team을 조회하기 위해 한번더 SQL이 발생한다.
//만약, 저장된 Member가 2개라면, Team을 조회하기 위한 SQL이 2번 추가로 발생한다.
- N+1이란, 최초 쿼리 1개 때문에 추가 쿼리 N개가 발생한다는 의미입니다.
- LAZY로 바꾸면, Team을 조회하기 위한 추가 쿼리가 발생하지 않습니다.
- 해결책 1
- 모든 연관관계를 지연로딩으로 설정합니다.
- Member만 가져와도 될 때는 Member만 가져오고, Member와 Team을 가져와야 할 때는 fetch join을 사용합니다.
- 패치 조인은, 런타임에 동적으로 원하는 데이터를 쿼리 한 개로 가져올 수 있습니다.
List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class)
.getResultList();
- 해결책 2 : 엔티티 그래프와 어노테이션 사용
- 해결책 3 : 배치 사이즈
- @ManyToOne, @OneToOne은 기본 값이 즉시 로딩입니다. → LAZY로 바꿔야 합니다.
- @OneToMany, @ManyToMany는 기본 값이 지연 로딩입니다.
지연 로딩 활용
- Member와 Team은 자주 함께 사용 → 즉시 로딩
- Member와 Order는 가끔 사용 → 지연 로딩
- Order와 Product는 자주 함께 사용 → 즉시 로딩
Member와 Team과 Order 로딩
Order와 Product 로딩
지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라
- 즉시 로딩은 상상하지 못한 쿼리가 나간다.
영속성 전이: CASCADE
- 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용합니다.
- 예: 부모 엔티티를 저장할 때, 연관된 자식 엔티티도 함께 저장합니다.
@OneToMany(mappedBy = "parent", cascade=CascadeType.PERSIST)
영속성 전이: 저장 실습
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade=CascadeType.PERSIST)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
[Cascade 사용 전]
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);
- parent, child1, child2 각각 따로 저장해야 합니다.
[Cascade 사용 후]
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
- parent만 저장해도 child1와 child2는 자동으로 저장이 됩니다.
💡영속성 전이: CASCADE 주의
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없습니다.
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐입니다.
- 한 개의 부모만이 자식들을 관리할 때 유용합니다 (단일 엔티티에 종속적일 때) 예) 게시판과 첨부파일들 관계
- 또, 부모와 자식의 라이프사이클이 동일할 때 사용해야 합니다.
CASCADE의 종류
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능입니다.
- orphanRemoval = true
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade=CascadeType.PERSIST, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거 → DELETE SQL 발생
DELETE FROM CHILD WHERE ID=?
💡고아 객체 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능입니다.
- 참조하는 곳이 하나일 때 사용해야 하빈다.
- 특정 엔티티가 개인 소유일 때 사용해야 합니다.
- @OneToOne, @OneToMany만 가능합니다.
- CascadeType.REMOVE 처럼 동작합니다.
영속성 전이 + 고아 객체의 생명주기
- CascadeType.ALL + orphanRemoval=true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화하고, em.remove()로 제거합니다.
- 두 옵션을 모두 활성화 하면, 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있습니다.
//parent를 통해서 child를 관리할 수 있다.
//parent의 생명주기는 영속성 컨텍스트가 관리하지만, child는 parent가 관리한다.
em.persist(parent);
em.remove(parent);
parent.getChildList().remove(0);
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용합니다.
실전 예제 - 5. 연관관계 관리
모든 연관관계를 지연 로딩으로 변경
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경해야 합니다.
@Entity
public class Category extends BaseEntity {
...
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "PARENT_ID")
private Category parent;
}
@Entity
public class Delivery extends BaseEntity {
...
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
}
@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
}
@Entity
public class OrderItem extends BaseEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ORDER_ID")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
}
영속성 전이 설정
- Order → Delivery를 영속성 전이 ALL 설정
- Order → OrderItem을 영속성 전이 ALL 설정
@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {
...
@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<>();
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
}
- Order를 저장하면, Delivery도 자동으로 저장됩니다.
- Order를 저장하면, orderItems도 자동으로 저장됩니다.
'Backend > JPA' 카테고리의 다른 글
[JPA] JPA의 다양한 쿼리 방법 소개 (0) | 2023.12.24 |
---|---|
[JPA] 값 타입 (0) | 2023.12.20 |
[JPA] 고급 매핑 (상속 관계 매핑, 공통 속성 매핑) (0) | 2023.12.20 |
[JPA] 다양한 연관관계 매핑 (0) | 2023.12.18 |
[JPA] 연관관계 매핑 (0) | 2023.12.18 |