본문 바로가기

Spring/JPA

[JPA] 프록시와 연관관계 정리

728x90

** 매우 중요한 부분

<개요>

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 을 해줌.  아주 간단한건 당연히 좋지. ㅎ ㅏ지만 실무에선 그러면 안됨. 무조건 쿼리는 직접ㅈ  ㅔ어해야함. 그 옵션을 빼게 하는 거니까 쓰면 안됨 ㅇㅇ

 

프록시 반대에 대한 질문

 

https://www.inflearn.com/questions/748359/%EC%B2%98%EC%9D%8C%EC%97%90-%EB%AC%B4%ED%95%9C-loop-%EC%9D%84-%EB%8F%8C%EA%B2%8C-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

 

처음에 무한 Loop 을 돌게 되는 이유 - 인프런 | 질문 & 답변

안녕하세요~ 강의 중 처음에 order 들을 조회하는 api 가 무한 루프를 도는 이유가 궁금해서 질문을 올리게 되었습니다 (6분 30초 ~ 7분 부분)우선, Order > Member > Order 조회 의 순으로 일어나기 때문에

www.inflearn.com

 

 

728x90

'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