프록시와 연관관계 정리

2023. 6. 25. 14:12jpa

프록시란?

JPA 에서 프록시는 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체를 의미합니다.

 

프록시 기초

 

em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회

em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

 

프록시 특징

실제 클래스를 상속 받아서 만들어짐

실제 클래스와 겉 모양이 같다.

사용하는 입장에서 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)

 

프록시 객체는 실제 객체의 참조를 보관한다.

프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

 

클라이언트가 getName() 메소드를 호출하면 프록시 객체에서 영속성 컨텍스트에 초기화 요청을 보낸다.

영속성 컨텍스트는 db를 조회하여 실제 entity를 생성한다.

프록시 객체가 실제 엔티티를 조회한다.

 

 

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush(); // db저장
em.clear(); // 1차 캐시 날리기

Member findMember = em.find(Member.class, 1L);

em.find()만 해주고 get을 값을 가져오지 않더라도 select문이 실행되는 것을 확인할 수 있다.

select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createBy as createby2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.TEAM_ID as team_id7_4_0_,
        member0_.USERNAME as username6_4_0_,
        team1_.TEAM_ID as team_id1_6_1_,
        team1_.createBy as createby2_6_1_,
        team1_.createdDate as createdd3_6_1_,
        team1_.lastModifiedBy as lastmodi4_6_1_,
        team1_.lastModifiedDate as lastmodi5_6_1_,
        team1_.name as name6_6_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

 

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush(); // db저장
em.clear(); // 1차 캐시 날리기

Member findMember = em.find(Member.class, 1L);
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getUsername() = " + findMember.getUsername());

get을 하면 select를 실행하고 해당되는 값을 출력해주는 것을 확인할 수 있다.

select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createBy as createby2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.TEAM_ID as team_id7_4_0_,
        member0_.USERNAME as username6_4_0_,
        team1_.TEAM_ID as team_id1_6_1_,
        team1_.createBy as createby2_6_1_,
        team1_.createdDate as createdd3_6_1_,
        team1_.lastModifiedBy as lastmodi4_6_1_,
        team1_.lastModifiedDate as lastmodi5_6_1_,
        team1_.name as name6_6_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.getId() = 1
findMember.getUsername() = hello

 

반면 em.getReference 메소드를 사용하면 

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush(); // db저장
em.clear(); // 1차 캐시 날리기

Member findMember = em.getReference(Member.class, 1L);

get으로 값을 가져오지 않으면 select문이 실행되지 않는다는 것을 확인할 수 있다.

 

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush(); // db저장
em.clear(); // 1차 캐시 날리기

Member findMember = em.getReference(Member.class, 1L);
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getUsername() = " + findMember.getUsername());

get을 해서 값을 가져오는 경우 select문이 실행되는 것을 확인할 수 있다. id의 값을 출력하고 select문을 실행한 후 username값을 출력하고 있는데 id의 값은 예시에서 값을 직접 넣어주었기 때문에 db를 조회할 필요없어 출력이 먼저 출력되고 username은 db에서 조회를 해야하기 때문에 select문이 실행된 후 username이 출력되는 것을 확인할 수 있다. 

 

findMember.getId() = 1
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createBy as createby2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.TEAM_ID as team_id7_4_0_,
        member0_.USERNAME as username6_4_0_,
        team1_.TEAM_ID as team_id1_6_1_,
        team1_.createBy as createby2_6_1_,
        team1_.createdDate as createdd3_6_1_,
        team1_.lastModifiedBy as lastmodi4_6_1_,
        team1_.lastModifiedDate as lastmodi5_6_1_,
        team1_.name as name6_6_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.getUsername() = hello

 

프록시의 특징

프록시 객체는 처음 사용할 떄 한번만 초기화한다.

여기서 초기화는 findMember.getUsername()을 할 때 발생한다.

밑에 예시를 보면 findMember.getClass의 이름이 동일한 것을 확인할 수 있다.

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush(); // db저장
em.clear(); // 1차 캐시 날리기

Member findMember = em.getReference(Member.class, 1L);
System.out.println("before findMember.getClass() = " + findMember.getClass());
System.out.println("findMember.getUsername() = " + findMember.getUsername());
System.out.println("after findMember.getClass() = " + findMember.getClass());

before findMember.getClass() = class hellojpa.Member$HibernateProxy$qunOUQ66
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createBy as createby2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.TEAM_ID as team_id7_4_0_,
        member0_.USERNAME as username6_4_0_,
        team1_.TEAM_ID as team_id1_6_1_,
        team1_.createBy as createby2_6_1_,
        team1_.createdDate as createdd3_6_1_,
        team1_.lastModifiedBy as lastmodi4_6_1_,
        team1_.lastModifiedDate as lastmodi5_6_1_,
        team1_.name as name6_6_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.getUsername() = hello
after findMember.getClass() = class hellojpa.Member$HibernateProxy$qunOUQ66

 

프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다.

 

프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야 한다. ( == 비교 실패, 대신 instanceof를 사용)

밑에 예시를 보면 em.find()를 할 경우에는 == 비교를 할 경우 true이다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

em.flush();
em.clear();

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());

System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("m2.getClass() = " + m2.getClass());
System.out.println("(m1.getClass() == m2.getClass()) = " + (m1.getClass() == m2.getClass()));

m1.getClass() = class hellojpa.Member
m2.getClass() = class hellojpa.Member
(m1.getClass() == m2.getClass()) = true

 

반면 em.find()한 객체와 em.getRerence()를 한 객체를 == 비교하면 false가 나온다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

em.flush();
em.clear();

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("m2.getClass() = " + m2.getClass());
System.out.println("(m1.getClass() == m2.getClass()) = " + (m1.getClass() == m2.getClass()));

m1.getClass() = class hellojpa.Member
m2.getClass() = class hellojpa.Member$HibernateProxy$vnxOI2qz
(m1.getClass() == m2.getClass()) = false

 

즉 instance of를 활용해서 비교해야 한다.

System.out.println("(m1 instanceof Member) = " + (m1 instanceof Member));   // 엔티티끼리 타입체크
System.out.println("(m2 instanceof Member) = " + (m2 instanceof Member));   // 엔티티와 프록시 객체와 타입체크

 

영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다. 밑의 예시를 보면 알 수 있다. jpa는 하나의 트랜잭션 안에서 같은 id에 대해서 동일한 것을 보장을 해주는 메커니즘이 있기 때문이다. 같은 id에 대해 em.find()를 해서 영속성 컨텍스트에 저장이 되어있는데 프록시보다는 실제 엔티티 값을 사용하는 것이 더 좋기 때문이다.

Member m1 = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member1.getId());

System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("reference.getClass() = " + reference.getClass());
System.out.println("(m1 == reference) = " + (m1 == reference));

m1.getClass() = class hellojpa.Member
reference.getClass() = class hellojpa.Member
(m1 == reference) = true

 

밑에 예시를 보면 신기한 점은 같은 id값에 getReference를 먼저 실행하고 find를 실행하면 프록시 타입이 반환되는 것을 알 수 있습니다. jpa가 == 할 때 동일하다는 것을 보장해주기 위해 find일 때도 프록시 타입을 반환해줍니다.

Member reference = em.getReference(Member.class, member1.getId());
Member m1 = em.find(Member.class, member1.getId());

System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("reference.getClass() = " + reference.getClass());
System.out.println("(m1 == reference) = " + (m1 == reference));

m1.getClass() = class hellojpa.Member$HibernateProxy$6A71mI7p
reference.getClass() = class hellojpa.Member$HibernateProxy$6A71mI7p
(m1 == reference) = true

 

영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.

밑에 예시를 보면 오류가 발생하는 것을 알 수 있다.

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass());

em.clear();

reference.getUsername();

reference.getClass() = class hellojpa.Member$HibernateProxy$M2p318rv
org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:170)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at hellojpa.Member$HibernateProxy$M2p318rv.getUsername(Unknown Source)
at hellojpa.JpaMain.main(JpaMain.java:52)

 

프록시 확인

 

프록시 인스턴스의 초기화 여부 확인

밑에 예시보면 프록시를 초기화하지 않았기 때문에 false가 나오는 것을 확인할 수 있다.

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass());
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(reference));

reference.getClass() = class hellojpa.Member$HibernateProxy$1KxL4JmC
isLoaded = false

 

밑에 예시에서는 프록시를 초기화 해주었기 때문에 true가 나오는 것을 확인할 수 있습니다.

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass());
reference.getUsername(); //프록시 초기화
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(reference));

isLoaded = true

 

프록시 클래스 확인 하는 방법은

System.out.println("reference.getClass() = " + reference.getClass());

 

프록시 강제 초기화하는 방법은 Hibernate.initialize()를 사용하면 됩니다. select문이 나오므로 초기화가 됬다는 것을 확인할 수 있습니다.

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass());
Hibernate.initialize(reference); // 강제 초기화

 

실제로는 getReference메소드를 잘 사용하지는 않는다. 즉시로딩, 지연로딩에 대해서 이해도를 높이기 위해 학습을 한다고 생각하면 될 거 같습니다.

 

지연로딩

 

Member 클래스

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getId());
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // 프록시 객체가 나옴

System.out.println("==================");
m.getTeam().getName();  //초기화
System.out.println("==================");

select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createBy as createby2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.TEAM_ID as team_id7_4_0_,
        member0_.USERNAME as username6_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
m.getTeam().getClass() = class hellojpa.Team$HibernateProxy$LCw385le
==================
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_6_0_,
        team0_.createBy as createby2_6_0_,
        team0_.createdDate as createdd3_6_0_,
        team0_.lastModifiedBy as lastmodi4_6_0_,
        team0_.lastModifiedDate as lastmodi5_6_0_,
        team0_.name as name6_6_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
==================

 

즉시로딩

 

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getId());
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // 엔티티가 나옴

System.out.println("==================");
m.getTeam().getName();  //초기화
System.out.println("==================");

Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createBy as createby2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.TEAM_ID as team_id7_4_0_,
        member0_.USERNAME as username6_4_0_,
        team1_.TEAM_ID as team_id1_6_1_,
        team1_.createBy as createby2_6_1_,
        team1_.createdDate as createdd3_6_1_,
        team1_.lastModifiedBy as lastmodi4_6_1_,
        team1_.lastModifiedDate as lastmodi5_6_1_,
        team1_.name as name6_6_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
m.getTeam().getClass() = class hellojpa.Team
==================
==================

 

프록시와 즉시로딩 주의

가급적 지연 로딩만 사용해야 한다.

즉시 로딩을 적용하면 예상하지 못한 sql이 발생할 수 있다.

@XXXToOne은 기본이 즉시로딩 -> lazy로 설정하자

@XXXToMany는 기본이 즉시로딩

즉시 로딩은 jpql에서 n+1 문제를 일으킨다.

 

List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

// SQL: select * from member;
// SQL: select * team where TEAM_ID = xxx;

위 JPQL을 그대로 쿼리로 번역하게 되면 Member를 가져오기 위한 쿼리 수행 이후 바로 Member 내부의 Team을 가져오기 위한 쿼리를 다시 수행하게 된다 → N+1(1개의 쿼리를 날리면 +N개의 쿼리가 추가수행된다)

 

n + 1 문제 해결방법 : 지연로딩 +  fetch join 사용

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();

/* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.MEMBER_ID as member_i1_4_0_,
            team1_.TEAM_ID as team_id1_6_1_,
            member0_.createBy as createby2_4_0_,
            member0_.createdDate as createdd3_4_0_,
            member0_.lastModifiedBy as lastmodi4_4_0_,
            member0_.lastModifiedDate as lastmodi5_4_0_,
            member0_.TEAM_ID as team_id7_4_0_,
            member0_.USERNAME as username6_4_0_,
            team1_.createBy as createby2_6_1_,
            team1_.createdDate as createdd3_6_1_,
            team1_.lastModifiedBy as lastmodi4_6_1_,
            team1_.lastModifiedDate as lastmodi5_6_1_,
            team1_.name as name6_6_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.TEAM_ID

 

지연 로딩 활용 - 실무

모든 연관관계에서 지연 로딩을 사용해라

실무에서 즉시 로딩을 사용하지마라

jpql fetch 조인이나 엔티티 그래프 기능을 사용해라

즉시 로딩은 상상하지 못한 쿼리가 나간다.

 

영속성 전이 : cascade

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티로 함께 영속 상태로 만들고 싶을 때

ex ) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

 

Parent

@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();

public void addChild(Child child) {
    childList.add(child);
    child.setParent(this);
}

Child

@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
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);

/* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)

 

persist를 각각 해줘야 insert 쿼리문이 날아간다.

 

CascadeType.ALL을 이용하며 부모만 persist를 해주면 자식도 같이 persist를 해줄 수 있다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

cascade의 종류

all : 모두 적용

persist : 영속

remove : 삭제

merge : 병합

refresh : refresh

detach : detach

 

영속성 전이(cascade)는 언제 사용하는게 좋을까?

전이 대상이 한군데에서 사용될 때만 사용 가능하다.

하지만 해당 엔티티(Child)가 특정 엔티티(Parent)에 종속되지 않고 여러군데서 사용된다면 사용하지 않아야 한다.

즉 라이프 사이클이 동일하거나 단일 소유자 관계일 때 사용하면 된다.

 

고아 객체

고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

밑에 예시에서 보면 list에서 삭제된 객체를 고아 객체라고 보면 된다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);

고아 객체 주의

참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능

참조하는 곳이 하나일 때 사용해야함

특정 엔티티가 개인 소유할 때 사용

@OneToOne, @OneToMany만 가능

부모를 제거하면 자식도 함께 제거된다.