** 매우 중요한 부분
<개요>
class Member{
String username;
Team team;
}
class Team{
String name;
}
Member를 조회할 때 Team 도 함께 조회되어야 할까?
---- 만약 Member에 대한 정보와 Team 에 대한 정보를 한꺼번에 사용을 해야 할 때 : 한번에 쿼리로 가져오면 좋음
---- 반대로 Member에 대한 정보만 활용하면 될 때 : Team 쿼리 까지 굳이 나가는 것은 손해임.
(즉, 비즈니스 상황에 따라 다르긴 함)
1) 프록시
<<<<<<< 개요 >>>>>>>
em.find() // em.getReference() 란 녀석도 있음.
getRefrence() 란, DB 조회를 미루는 가짜 (Proxy) Entity 객체가 조회됩니다.
다음 예시
@Test
@DisplayName("Proxy 소개 : get Reference() 실험")
void test1() {
tx.begin();
try {
PracticeMember memberA = new PracticeMember();
memberA.setName("memberA");
em.persist(memberA);
em.flush();
em.clear(); // 쿼리가 나가게 하기 위함
//
// 일반 조회
// PracticeMember findMember = em.find(PracticeMember.class, memberA.getId());
// System.out.println("findMember.getId() = " + findMember.getId());
// System.out.println("findMember.getName() = " + findMember.getName());
// 프록시 조회
PracticeMember findProxyMember = em.getReference(PracticeMember.class, memberA.getId());
// System.out.println("findProxyMember.getId() = " + findProxyMember.getId());
// System.out.println("findProxyMember.getName() = " + findProxyMember.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
위 테스트에 명시해둔 상황과 같이, 일반적으로 em.find()로 조회할 시에는 select 쿼리가 날라가게 됩니다. 하지만, 아래 프록시 객체 findProxyMember 를 조회할 경우 (em.getRef() 활용시)에는 select 쿼리가 날라가지 않는 것을 콘솔창을 통해서 확인할 수 있습니다.
하지만 다음과 같이 findProxyMember 를 직접 활용할 시에, 바로 쿼리가 날라가는 모습을 확인할 수 있습니다.
...
// 프록시 조회
PracticeMember findProxyMember = em.getReference(PracticeMember.class, memberA.getId());
// System.out.println("findProxyMember = " + findProxyMember.getClass());
System.out.println("============================================================================ABOUT TO USE PROXY OBJECT");
System.out.println("findProxyMember.getId() = " + findProxyMember.getId());
System.out.println("findProxyMember.getName() = " + findProxyMember.getName());
...
}
emf.close();
}
}
출력값
...
============================================================================ABOUT TO USE PROXY OBJECT
findProxyMember.getId() = 1
16:51:09.737 [Test worker] DEBUG org.hibernate.SQL -
select
practiceme0_.member_id as member_i1_12_0_,
practiceme0_.created_by as created_3_12_0_,
practiceme0_.modified_at as modified4_12_0_,
...
from
practice_member practiceme0_
where
practiceme0_.member_id=?
...
16:51:09.742 [Test worker] DEBUG org.hibernate.engine.internal.TwoPhaseLoad - Attribute (`zipcode`) - enhanced for lazy-loading? - false
16:51:09.747 [Test worker] DEBUG org.hibernate.engine.internal.TwoPhaseLoad - Done materializing entity [jpa.demo.practice.PracticeMember#1]
findProxyMember.getName() = memberA
Id(가지고 있는 것으로 보임) 이후 실제 getName()을 사용하기 위해서 SQL을 날리는 모습들을 볼 수 있다.
그렇다면 바로 위에 주석을 해제해서, findMember를 먼저 soutprint로 찍어서, 정체가 뭔지 확인해보자.
findProxyMember = class jpa.demo.practice.PracticeMember$HibernateProxy$aqPGuErW
..?
<<<<<<< Proxy 란? >>>>>>>
Proxy 가 뭔가? 가짜 객체를 말한다. (Proxy 자체는 '대리'를 의미한다)
getRef 를 하면 진짜가 아닌 Hibernate가 만들어낸 PRoxy 가짜 Entity 객체를 반환한다. 껍데기 Entity 인데 Id만 가지고 있음. (프록시 강의 12분 대)
Proxy 객체는 실제 조회하려는 클래스를 상속받아서 만들어져서, 겉 모양, 내부 칼럼들이 동일하다. (Hibernate가 만들어줌), 겉모양이 같으므로, 사용하는 입장에서는 이 객체를 굳이 구별하지 않고 사용해도 됨.
Proxy 는 내부에 Entity target = null 을 보관중이다. 그리고 칼럼들을 조회하기 위한 getter들을 가지고 있음.
(14분 30초대 그림 집중)
em.getRef() 를 수행한 후, member.getName()을 수행함.
MemberProxy 는 이 명령을 받고, 영속성 컨텍스트에 요청을 한다. (야 실제 얘 Member (PK는 가지고 있음) 가지고 와!) 그러면 영컨은 DB 조회 후 실제 Entity 객체를 생성해준다. >> 그리고 영컨은 이 가짜 객체와 이 실제 객체를 연결시켜준다.
(이렇게 실제 DB에 요청하는 과정을 [초기화]라고 한다. 초기화 이후에는 진짜 객체가 연결되어 있으므로 자유롭게 가져옴)
>> 그래서 Proxy 객체는 진짜 member.getName()을 호출하게 되고, 그 반환값을 유저에게 반환해주게 된다.
<<<<<<< Proxy 의 특징 >>>>>>>
1>> 프록시는 초기화 한번만 한다
2>> 프록시 객체가 진짜 객체로 바뀌는게 아니다. Proxy 를 통해 [대리]로 실제 엔티티에 접근 가능한 것. findMember 한 이후에도 proxyMember.getClass()를 해도 같은 클래스 객체가 반환된다.
3>> instance of 로 사용하셈!
Member member1 = new Member();
member1.setName("memberA");
em.persist(member1);
Member member2 = new Member();
member2.setName("memberB");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
soutv("m1 == m2 " + (m1.getClass() == m2.getClass()); // 당연히 True 가 나옴.
Member m3 = em.getReference(Member.class, member2.getId();
soutv("m2 == m3" + (m1.getClass() == m2.getClass()); // Fail 이 나옴.
tx.commit();
4>> 영컨 1차캐시에 만약 그 값이 저장되어 있다면, em.getReference()를 해도 그냥 바로 그 객체와 연결이 된다.
Member member1 = new Member();
member1.setName("memberA");
em.persist(member1);
Member reference = em.getReference(Member.class, member1.getId());
soutv("reference is = " + reference.getClass()); // Proxy 를 반환할까 Member를 반환할까?
tx.commit();
정답은 Member 객체를 반환하게 된다.
(1) 1차 캐시에 이미 있는데 굳이 프록시로 가져올 이유가 없기 때문
(2) (진짜 이유) JPA에서는 한 트랜젝션 안에서 같은 객체 보장을 해줘야 함 (?)
:::: ref 가 Proxy 든, 실제 Member 든, 한 영컨 안에서 PK가 동일하면, JPA는 True 를 반환해줘야 함. 같은 객체이기 때문.
:::: 즉, PK가 같은 동일 객체이므로 같은 값으로 인지해야 하는 것이 JPA의 역할이다. 따라서 그 역할을 수행하기 위해 1차캐시에 있는 객체와 동일 객체임을 인지하고 반환.
>>>member m1 = em.getRef(member1.getId()), member member1 = em.getRef(mem1.getId()) 를 각각 다른 객체로 찾아도, 같은 Proxy 객체를 반환해 준다.(m1, member1 의 주소는 동일) > 이유: m1 == member1 이여야 하기 때문
=========================================
5>> JPA의 동일성 보장의 연장선
@Test
@DisplayName("프록시 객체 : Ref 와 Find를 각각 가져오면 JPA는 어떻게 할까?")
void test3() {
tx.begin();
try {
PracticeMember member1 = new PracticeMember();
member1.setName("member1");
em.persist(member1);
em.flush();
em.clear();
PracticeMember refMember = em.getReference(PracticeMember.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass()); // Proxy 호출
PracticeMember findMember = em.find(PracticeMember.class, member1.getId());
System.out.println("findMember.getClass() = " + findMember.getClass()); // 찐 객체 호출
System.out.println("refMember == findMember :: " + (refMember == findMember));
tx.commit();
} catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close();
}
위 결과에서 당연히
refMember 는 Proxy 를 가져올 것이고, findMEmber 는 당연히 찐 PracticeMember 객체를 가지고 올 것이다. Proxy 를 초기화 진행될 수 있도록 하지는 않았는데, 그렇다면 ref==find 가 false 가 나오지 않을까? 근데 둘이 PK 같고, 같은 영컨 안에 있는데?!
----> 결과를 보면 다음과 같다.
refMember.getClass() = class jpa.demo.practice.PracticeMember$HibernateProxy$fOrete5H
// ..... SQL 들이 나감
findMember.getClass() = class jpa.demo.practice.PracticeMember$HibernateProxy$fOrete5H
refMember == findMember :: true
왜 그럴까?
Proxy 를 한 번 조회가 되면, em.find 에서도 Proxy 를 반환하게 해준다. (TRUE 를 맞추기 위함) // 뭐 이렇다는 건 꼭 알고 있으면 되고, 실제로 이런 것들을 제어해야할 일이 생기진 않는다.
=========================================================
6>> 실무에서 실질적으로 접할 법한 문제점 (영컨의 도움을 받을 수 없느 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다 LazyInitializationException)
@Test
@DisplayName("프록시 객체 : LazyInitException (Proxy 의 초기화 요청은 영컨을 통해 이루어진다)")
void test4() {
tx.begin();
try {
PracticeMember member1 = new PracticeMember();
member1.setName("member1");
em.persist(member1);
// 저장을 했음
em.flush();
em.clear();
// 다시 처음 영컨 상태
PracticeMember reference = em.getReference(PracticeMember.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass());
em.detach(reference); // 영속성 관리를 해제한다
// Proxy 가 영컨을 통해 초기화를 요청한다 // 하지만 Detach 시 영컨이 관리하지 않는다
System.out.println("reference.getName() = " + reference.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close();
}
초기화 요청은 영속성 컨텍스트가 진행한다. 따라서 해당 객체를 영컨이 더이상 관리해주고 있지 않을 때, name 이 출력되지 않는데, 그 이유는 Exception이 발생했기 때문이다 (e.printStackTrace() 가 출력되게 된다)
org.hibernate.LazyInitializationException: could not initialize proxy [jpa.demo.practice.PracticeMember#1] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176)
at jpa.demo.practice.PracticeMember$HibernateProxy$dCWvamNj.getName(Unknown Source)
at jpa.demo.practice.proxy.ProxyTest.test4(ProxyTest.java:142)
...
프록시를 영컨이 관리해주고 있지 않으니까 아무것도 할 수없을 때 발생하는 에러이다. 이 에러를 나중에 많이 보게 될 것이다. JPA 쓰면 반드시 만나는 Exception.
참고로 초기화 되었는지 확인하는 방법은
emf.getPersistenceUnitUtil.isLoaded(ENTITY); 이다 - TRUE : 초기화 됨 // FALSE : 아직 프록시임
.getUsername 같은 칼럼 조회 말고 일단 초기화를 진행시킬 수 있다. (Hibernate 방식. JPA는 강제 없음. 칼럼 조회해야함)
Hibernate.initialize(ENTITY) // 강제 초기화 방식
-----------------------------------------------
왜 프록시를 공부할까?? getReference 사실 쓸 일 없음. 하지만 프록시를 이해해야 즉시 로딩과 지연 로딩을 이해할 수 있기 때문. (우왕)
2) 즉시 로딩과 지연 로딩
연관되어 있는 객체
Member (N) : Team (1)
@Test
@DisplayName("Member get Team : fetchType = LAZY")
void test1() {
tx.begin();
try {
PracticeTeam practiceTeam = new PracticeTeam();
practiceTeam.setTeamName("team1");
em.persist(practiceTeam);
PracticeMember member1 = new PracticeMember();
member1.setName("member1");
member1.setTeam(practiceTeam);
em.persist(member1);
em.flush();
em.clear(); // DB input 위함
PracticeMember findMember = em.find(PracticeMember.class, member1.getId());
System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
} finally {
em.close();
}
emf.close();
}
Member 내
@ManyToOne (fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
로 선언을 한 다음에 다음 위 테스트를 실행해보자. 처음에 Member를 들고 올 때 Team 을 PROXY 객체로 들고 옴을 알 수 있다.
출력값 :
findMember.getTeam().getClass() = class jpa.demo.practice.PracticeTeam$HibernateProxy$plH6gS1k
즉, LAZY 를 Fetch Type 으로 하게 되면 getReference 와 같이 프록시 객체를 들고와서 초기화를 대기하는 상태이다. 그 이후 Team 을 더 touch 하면 query 가 발생하고 초기화를 진행하게 된다.
이거는 두 가지 상황에 대해서 알맞게 사용할 수 있다. 만약 비즈니스 로직 상 Member 조회시 Team 을 대부분 항상 사용한다면, EAGER로 한꺼번에 들고오는게 훨씬 낫다. 굳이 쿼리를 두번으로 나눌 필요가 없기 때문 (Team 에서 끝난다는 가정 하) 반대라면 Lazy 가 나을 것.
그렇다면 EAGER 로 할경우는 말했듯이 한번에 들고 온다.
EAGER를 할 경우 member와 team 을 id로 join 해서 한방에 다 가져오게 된다. 그리고 뒤에 findMEmber.getTeam.getName()을 할 때는 SQL은 나가지 않고, 다음과 같은 출력값을 확인할 수 잇다.
findMember.getTeam().getClass() = class jpa.demo.practice.PracticeTeam
==========================IS QUERY OCCURED? ==
findMember.getTeam().getTeamName() = team1
==========================
쿼리문은 발생하지 않았고, TEAM 객체도 일반 객체가 조회됨을 확인할 수 있다.
<<<<< 하지만 정답은 정해져 있다 : 실무에선 즉시로딩 사용 금지 >>>>>>
즉시 로딩 --> 예상하지 못한 SQL이 너무 많이 발생함. N+1 문제를 발생시킴.
(Member Team 처럼 일반적인 관계가 아니라, 보통 별의별 관계 ㅈㄴ 묶여있음. EAGER 여러개 사용시, 그냥 다 join 때려서 불러와버리는데 이건 필요 이상의 쿼리일 경우가 많음). 이따시만한 쿼리가 나감 ㅋㅋ
또한, 어떤 Group 이 있고, 그 Grou pMember들을 모두 불러와야 함
SQL 어떻게 짬 : select * from member wherer m.group_id = {group_id}
근데 멤버 200명임
근데 Team fetchType Eager 다? ㅋㅋ
200 개 Team 다 가지고옴.
TEam에서 뭐 하나라도 또 Eager 로 선언되어 있다? (팀이 다르다면 영컨도 활용 못함)
*N 만큼 더 불러옴.>>> 성능에 치명적
<N+1 문제 >
첫 쿼리 1 이 날렸는데, 그것 때문에 N개의 쿼리가 발생하는 문제
Team 이 다다르면 200개 (N) 개가 다 날라가는 문제를 말함.
> 그래서 Lazy 해야함. 그냥 해야함.
> Lazy 하면 Team 쿼리 안나감.
해결 방안 : 모든 것을 지연로딩으로 다 깜
FETCH JOIN (동적으로 원하는 애들을 선택해서 가져 오는 것) >> 그 때 그 때 내가 원할 때마다 다르게! (뒤에 자세히 배움) (그냥 join 이라고 보면 되는데, 영속화 관점에서 다름)
* 참고 - Default 설정 -> ManyToOne, OneToOne 두 개는 즉시 로딩으로 되어 있음. (LAZY 로 꼮 바꿔줘야 함)
OneToMany (ManyToMany) 두 개는 맞음.
이론적으로는 즉시 로딩이랑 분기해서 사용하는게 이상적이긴 하지만, 말했듯이 실무는 정말 상상 이상으로 복잡하다.
[즉시로딩과 지연로딩] 23분
보완하기 위해서 JPQL FETCH JOIN / 엔티티 그래프 기능을 사용하면 됨
******************** Lazy 로딩을 써야하는 이유는 진짜 간단하게 말하면 이거임
>> 쿼리는 직접 제어해야함. 쿼리는 개발자가 직접 제어해야 원하는 때, 원하는 칼럼들에 한해서, 원하는 조인, 원하는 내용들만을 제일 빠르게 가져와서 보내줄 수 있도록 고민하는게 중요한 백엔드 앱이다.
Eager 쓰면 join 을 해줌. 아주 간단한건 당연히 좋지. ㅎ ㅏ지만 실무에선 그러면 안됨. 무조건 쿼리는 직접ㅈ ㅔ어해야함. 그 옵션을 빼게 하는 거니까 쓰면 안됨 ㅇㅇ
프록시 반대에 대한 질문
처음에 무한 Loop 을 돌게 되는 이유 - 인프런 | 질문 & 답변
안녕하세요~ 강의 중 처음에 order 들을 조회하는 api 가 무한 루프를 도는 이유가 궁금해서 질문을 올리게 되었습니다 (6분 30초 ~ 7분 부분)우선, Order > Member > Order 조회 의 순으로 일어나기 때문에
www.inflearn.com
'Spring > JPA' 카테고리의 다른 글
[JPA] - 값 타입 (0) | 2022.12.29 |
---|---|
[JPA] 프록시와 연관관계 - 2 (0) | 2022.12.28 |
[JPA] 연관관계 매핑 -2 (0) | 2022.12.26 |
[JPA] - 엔티티 매핑 (1) | 2022.12.21 |
[JPA] JPA 개요와 영속성 컨텍스트 (0) | 2022.12.18 |