빡코

[JPA][개념] 프록시와 연관관계 정리 본문

Java/JPA

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

chris.djang 2023. 3. 23. 15:03

프록시란? 
•em.find() vs em.getReference() 
•em.find(): 데이터베이스를 통해서 실제엔티티객체조회 
•em.getReference():데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createBy, createDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
findMember = class hellojpa.Member$HibernateProxy$ZvtX5f8z
findMember = 1

겉은 같으나 속은 텅텅빈 상태임. 아이디 값만 들고 있는 가짜만 발급이 된다. 

 

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

•실제 클래스와 겉모양이 같다 
.•사용하는 입장에서는 진짜 객체인지 프록시객체인지 구분하지 않고 사용하면 됨(이론상)

 

프록시객체는 실제 객체의 참조(target)보관

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

 

프록시 객체의 초기화

 

 

프록시의 특징

 

•프록시 객체는 처음 사용할때 한번만 초기화
•프록시 객체를 초기화 할때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
•프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입체 크시 주의 해야 함 (== 비교실패, 대신 instance of 사용)
•영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출 해도 실제 엔티티 반환
영속성 컨텍스트의 도움을 받을수 없는 준영속 상태일 때, 프록시를 초기화 하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를터트림)

org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
 

 

프록시확인
•프록시 인스턴스의 초기화 여부 확인 
PersistenceUnitUtil.isLoaded(Object entity)
•프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist.. or HibernateProxy...)
•프록시 강제 초기화 
org.hibernate.Hibernate.initialize(entity);
•참고: JPA 표준은 강제 초기화 없음 
강제 호출: member.getName()

 

그럼 Proxy는 왜 사용하는 것일까? -> 즉시로딩과 지연로딩을 이해해가 위해서 필요한 개념임 

 

즉시로딩과 지연로딩 

//Member Class
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team; //proxy 객체로 조회
        
//Team Class 
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();

        
//Main Class      
        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 = " + m.getTeam().getClass() );
            
            //이 시점에 Team에 대한 쿼리가 나간다.
            System.out.println("================");
            m.getTeam().getName();
            System.out.println("================");

            tx.commit();//inset query가 나가는 시점
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.createBy as createby2_3_0_,
        member0_.createDate as createda3_3_0_,
        member0_.lastModifiedBy as lastmodi4_3_0_,
        member0_.lastModifiedDate as lastmodi5_3_0_,
        member0_.team_TEAM_ID as team_tea7_3_0_,
        member0_.USERNAME as username6_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
m = class hellojpa.Team$HibernateProxy$EPowgfRK
================
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.createBy as createby2_7_0_,
        team0_.createDate as createda3_7_0_,
        team0_.lastModifiedBy as lastmodi4_7_0_,
        team0_.lastModifiedDate as lastmodi5_7_0_,
        team0_.name as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
================

 

 

 

Member와 Team을 동시에 자주 사용 한다면? -> 즉시로딩 사용 

//Member
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team; //proxy 객체로 조회

//Log
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.createBy as createby2_3_0_,
        member0_.createDate as createda3_3_0_,
        member0_.lastModifiedBy as lastmodi4_3_0_,
        member0_.lastModifiedDate as lastmodi5_3_0_,
        member0_.team_TEAM_ID as team_tea7_3_0_,
        member0_.USERNAME as username6_3_0_,
        team1_.TEAM_ID as team_id1_7_1_,
        team1_.createBy as createby2_7_1_,
        team1_.createDate as createda3_7_1_,
        team1_.lastModifiedBy as lastmodi4_7_1_,
        team1_.lastModifiedDate as lastmodi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.team_TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
m = class hellojpa.Team

 

프록시와 즉시로딩 주의 
•가급적 지연로딩만사용(특히 실무에서) 
•즉시로딩을 적용하면 예상하지 못한 SQL이 발생 
•즉시로딩은 JPQL에서 N+1 문제를 일으킨다. 
•@ManyToOne, @OneToOne은 기본이 즉시로딩 -> LAZY로 설정 
•@OneToMany, @ManyToMany는 기본이 지연로딩

* 즉시로딩 사용시 연결된 모든 테이블이 조인되어 가져오면서 성능에 문제가 발생한다. 

 

 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_3_0_,
            team1_.TEAM_ID as team_id1_7_1_,
            member0_.createBy as createby2_3_0_,
            member0_.createDate as createda3_3_0_,
            member0_.lastModifiedBy as lastmodi4_3_0_,
            member0_.lastModifiedDate as lastmodi5_3_0_,
            member0_.team_TEAM_ID as team_tea7_3_0_,
            member0_.USERNAME as username6_3_0_,
            team1_.createBy as createby2_7_1_,
            team1_.createDate as createda3_7_1_,
            team1_.lastModifiedBy as lastmodi4_7_1_,
            team1_.lastModifiedDate as lastmodi5_7_1_,
            team1_.name as name6_7_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.team_TEAM_ID=team1_.TEAM_ID

 

영속성 전이 

부모 엔티티를 저장할 떄 자식 엔티티도 함께 저장(연관된 엔티티도 함께)

package hellojpa;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


}
package hellojpa;

import javax.persistence.*;

@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name= "parent_id")
    private Parent parent;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setParent(Parent parent) {
        this.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);

영속성전이: CASCADE - 주의! 
•영속성전이는 연관관계를 매핑하는 것과 아무 관련이 없음 
•엔티티를 영속화할 때연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐

 

CASCADE의 종류
•ALL: 모두적용
•PERSIST: 영속
•REMOVE: 삭제
•MERGE: 병합
•REFRESH: REFRESH
•DETACH: DETACH

 

* 단일 소유일 경우에는 CASCADE 사용 가능, BUT, 다른 테이블이 CHILD를 알게되는 경우 사용 지양 

* 완전히 종속적일 경우, 부모와 자식의 라이프 사이클이 동일하여 사용 가능 

 

고아객체 

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

•고아객체제거: 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제 
•Parent parent1 = em.find(Parent.class, id); parent1.getChildren().remove(0);//자식 엔티티를 컬렉션에서 제거
•DELETE FROM CHILD WHERE ID=?

 

•참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아객체로 보고 삭제하는 기능 
•참조하는 곳이 하나 일 때사용해야 함! 
•특정엔티티가 개인소유할 때사용 
•@OneToOne, @OneToMany 만 가능 
•참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.

 

영속성전이 + 고아객체, 생명주기 
•CascadeType.ALL + orphanRemoval=true 
•스스로 생명주기를 관리하는 엔티티는 em.persist()로영속화, em.remove()로제거 
•두 옵션을 모두 활성화하면 부모엔티티를 통해서 자식의 생명주기를 관리할 수 있음 
•도메인주도설계(DDD)의 Aggregate Root개념을 구현할 때 유용

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Java > JPA' 카테고리의 다른 글

[스프링 데이터 JPA] 기초 설정  (0) 2023.05.10
[JPA][개념] 값 타입  (0) 2023.03.23
[JPA][개념] 상속관계  (0) 2023.02.22
[JPA][개념]다양한 연관관계 매핑  (0) 2023.02.17
[JPA][개념] 엔티티 매핑  (0) 2023.02.14