1. 지연 로딩
즉시 로딩과 지연 로딩의 차이는 내 생각엔 필요할 떄 조회하는 것 같다.
지연 로딩은 필요할 때 찾고, 즉시 로딩은 처음부터 다 찾는거다.
그리고 지연 로딩을 하는 LAZY 옵션을 사용하면,
앞에서 배운 프록시 클래스가 생성이 되는 것을 알 수 있다.
<java />
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("findMember.Team = " + findMember.getTeam().getClass());
System.out.println("여기 시점에 team을 조회하므로 쿼리가 나감");
System.out.println(findMember.getTeam().getName());
여기서 주의해야 할 점은
<code />
System.out.println(findMember.getTeam().getName());
여기 부분에서 호출이 되는 것이다.
처음에는 findMember.getTeam().getClass()에서 조회하는 쿼리가 나갈 줄 알았다.
하지만 이 부분은 Team에 있는 속성을 사용하지 않아 쿼리가 나가지 않게 된다.
앞에서 프록시 초기화는 해당 객체에 있는 속성을 사용할 때 초기화가 이뤄진다고 했다.

이 경우는 Member를 Team에 비해 많이 조회했을 때 효율적이다.
2. 즉시 로딩
지연 로딩에서 Team과 Member를 자주 같이 사용한다면 select 쿼리가 2개씩 나가게 되므로 비효율적이다.
이 때는 EAGER 즉시 로딩을 사용하면 된다.

@ManyToOne의 default 값은 EAGER이다.
즉시 로딩을 사용하게 되면 Member를 조회할 때 Team까지 한 번에 같이 조회한다.
그래서 예상하지 못한 SQL이 발생할 수 있다.
소규모 프로젝트에서는 조인 한, 두개 더한다고 해서 성능이 저하되지 않는다.
하지만 Member와 연관된 테이블이 10개 이상 이렇게 되어버린다면,
Member를 조회할 때마다 연관된 테이블들을 모두 조회하기 때문에 성능 저하가 우려된다.
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
즉, member에 연관관계 매핑이 되어있는 객체가 2개가 있다고 생각해보자. (team, address)
그리고 member의 수가 총 3명으로, member1, member2, member3이 있다.
member 조회 ( 1번 )
member -> team ( 3번 )
member -> address ( 3번 )
총 7번의 쿼리가 실행이 된다. 그래서 N+1 문제라고 한다.
<java />
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();

처음에는 Member만 select하게 되는데 Team이 조회가 되있다.
왜?
즉시 로딩은 가져올 때 무조건 값이 다 들어가있어야한다.
그러면 member 쿼리가 나가고 Team을 조회하는 쿼리가 나가야한다.
<java />
1. SQL : select * from Member;
2. SQL : select * from Team where TEAM_ID = xxx
1번이 실행됐는데, 어 ? Team 이 없네 하고 2번을 호출하는 것이다.
또 다른 예를 들어보자.
MemberA는 TeamA, MemberB는 TeamB에 속해있다.
1번이 실행이 되면 Member A 가 조회된다.
하지만 Memeber A에 TeamA를 조회해야하므로 이 때도 쿼리가 나간다.
MemberB가 조회된다.
위와 같은 이유로 TeamB가 조회된다.
따라서 1번이 실행되면 MemberA, MemberB가 조회되고(Member는 한 번에 조회)
2번이 실행되면 각각 TeamA이 조회, TeamB가 조회된다.
즉, 쿼리가 3번 나가게 된다.
하지만 지연 로딩을 사용하게 되면 Team을 사용하지 않기 때문에 쿼리가 나가지 않는다!
3. 영속성 전이

현재 child와 parent는 다대일 관계이다.
만약에 우리가 parent에 child1과 child2를 매핑해주려면 위 코드를 작성해야한다.
특히 persist를 3번이나 해야한다.
하지만 반복이 싫은 프로그래머들은 parent 위주의 코드를 짜보자. 혹은
parent가 child를 관리해주면서 parent를 persist하면 child가 자동으로 persist 되면 안되나 라는 생각을 한다.
이 때 쓰는 것이 cascade, 즉, 영속성 전이다.
말 그대로 전이, 옮긴다는 것이다.
<java />
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
parent가 persist 되면 cascade 옵션이 있는 아래 childList를 모두 persist한다는 뜻이다.
그래서 위와 같은 코드가 해당 옵션으로 인해 아래와 같은 코드로 변경된다.
<java />
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);

잘 매핑되있는 것을 확인할 수 있다.
하지만 cascade를 무작정 사용할 수 없다.
cascade는 둘의 관계가 단일 연관관계 일 때 사용해야한다.
예를 들어 Child는 다른 연관관계를 가지지 않고 Parent만 연관관계를 가지고 있다.
반대로 Child가 Member와 Parent에 연관관계를 가지고 있다고 생각해보자.
Parent에서 Child를 persist하는데 이 때 Member에도 Child를 매핑해주어야한다.
한 두개가 아니고 여러 개의 테이블이 매핑되어있다면 엄청 복잡해진다..
4. 고아 객체
부모 엔티티와 관계가 끊어지면 자식 엔티티는 delete 쿼리가 나가게 된다.
<java />
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
<java />
childList.remove(0);


아래 경우는 부모 엔티티가 사라질 경우에 자식 엔티티가 모두 삭제된다.
<java />
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
<code />
em.remove(findParent);

<java />
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
해당 코드도 위와 같은 결과를 얻는다.
다만 차이점은 cascade, orphanRemoval 옵션의 유무이다.
cascade는 영속성 전이 특징을 생각해보면 아래의 List에게 영향을 준다했다.
그렇다면 remove했다면 당연히 orphanRemoval 옵션이 없어도 List에 영향을 줄 것이다.
cascade가 없고 orphanRemoval 옵션만 있을 경우에도 부모 엔티티를 잃어버린 고아 엔티티(자식 엔티티)는 삭제된다.
5. Reference
자바 ORM 표준 JPA 프로그래밍 (김영한 지음)
'Spring 강의 > JPA - 기본편' 카테고리의 다른 글
객체지향 쿼리 언어1 (1) ~ 프로젝션 (0) | 2021.12.14 |
---|---|
값 타입 (0) | 2021.12.12 |
프록시와 연관관계 관리 (1) 프록시 (0) | 2021.12.10 |
고급 매핑 (0) | 2021.12.07 |
다양한 연관관계 매핑 (0) | 2021.12.02 |