SQL mapper & ORM/JPA

8. 프록시와 연관관계 관리

wooweee 2023. 10. 6. 03:56
728x90

1. 프록시

1.1. 프록시 기초

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

 

1.2. 프록시 특징1

  • 실제 class를 상속 받아서 만들어진다.
  • 실제 클래스와 겉 모양이 같다.
  • 실제 객체의 참조(target)을 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출

 

1.3. 프록시 객체의 초기화

MemberChap8 member = new MemberChap8();
member.setUsername("hello");
em.persist(member);

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

Member findMember = em.getReference(Member.class, "id");
findMember.getName();

 

  • 초기화 설명
    • flush()와 clear()로 영속성 context까지 싹 날린 상태
    • getReference()를 하면 영속성 컨텍스트를 이용해서 Member 엔티티 (객체)를 생성을 하여 초기화 진행을 수행
    • 현재는 영속성 컨텍스트를 아예 비운 상태이기 때문에 db로 접근하여 select 쿼리르 수행 후, 영속성 컨텍스트에 값 넣고, Member 엔티티도 생성
      - 만약 flush(), clear() 안하면 영속성 컨텍스트에 값이 존재하기 때문에 쿼리를 날릴 필요없이 영속성 컨텍스트를 이용해서 바로 Member Entity 생성
    • 이렇게 Member Entity 생성한 것을 이용해서 프록시 객체가 getName() 등의 해당 메서드를 수행한다.
    • 참고로 em.find()로 수행 시, 바로 Member 엔티티를 직접 사용하는 것이고 em.getReference()를 이용하면 proxy라는 가짜를 앞에 두고 뒤에 숨겨서 만든 Member 엔티티를 이용해서 값을 가져오는 것이다.
      - new Member()로 만들어서 사용하는데 왜 또 멤버 객체를 가져오는지 헷갈리지 말자

 

 

package hellojpa.chap8;

import hellojpa.Member;
import hellojpa.chap6.nByN.MemberNByN;
import hellojpa.chap7.MovieChap7;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.time.LocalDateTime;

public class JpaChap8 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            MemberChap8 member = new MemberChap8();
            member.setUsername("hello");
            em.persist(member);

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

            // 해당코드 사용여부 상관없이 select에 join까지 쿼리 날라감
            // MemberChap8 findMember = em.find(MemberChap8.class, member.getId());

            // 당장 해당 코드 사용 안할시, 쿼리 안날림, sysout 주석풀면 query 날라감
            MemberChap8 findMember = em.getReference(MemberChap8.class, member.getId());
            System.out.println("findMember = " + findMember.getClass()); // class hellojpa.chap8.MemberChap8$HibernateProxy$xtE84MKo (프록시 객체)
            // System.out.println("findMember.getId = " + findMember.getId()); // reference를 찾을 때 이미 id를 넣었기 때문에 쿼리 안날라감
            // System.out.println("findMember.getUsername = " + findMember.getUsername()); // 이 때 db로 부터 값을 가져와서 findMember에 값을 채우고 출력함

            // 프록시 객체는 값이 없으면 영속성 컨텍스트를 이용해서 실제 객체에 값을 넣고 다시 실제 객체를 통해서 가지고 오고, 실제 객체 값이 존재한 후부터는 영속성 컨텍스트에 접근하지 않고 바로 객체로 간다.
            System.out.println("findMember.getUsername = " + findMember.getUsername()); // 영속성 컨텍스트를 통해서 DB 조회 - 쿼리 날라감
            System.out.println("findMember.getUsername = " + findMember.getUsername()); // 실제 객체로 감 - 쿼리 안날라감

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

 

1.4. 프록시 특징2

  • 처음 사용할 때 한 번만 초기화. 이후로는 영속성 컨텍스트를 거치지 않고 실제 Entity를 직접적으로 사용한다.

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

  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
    - 프록시 객체랑 프록시 아닌 객체랑 type에 차이가 있기 때문

  •  영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 객체를 반환
    - 반대로 proxy 객체로 처음에 호출되면 이후에 em.find()를 해도 프록시 객체를 반환
    - 이유 : jpa는 동일 트렌젝션 안에서는 collection 처럼 동일 key로 value를 찾을 시 항상 동일한 value를 반환해야하기 때문에 type을 일치시킨다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

 

 

  • == 대신 instance of 사용하는 이유
package hellojpa.chap8;

public class JpaChap8InstanceOf {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            // 프록시에 == 이 아닌 instance of 사용하는 이유
            MemberChap8 member1 = new MemberChap8();
            member1.setUsername("member1");
            em.persist(member1);

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

            MemberChap8 member3 = new MemberChap8();
            member3.setUsername("member3");
            em.persist(member3);

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

            MemberChap8 m1 = em.find(MemberChap8.class, member1.getId());
            MemberChap8 m2 = em.find(MemberChap8.class, member2.getId());
            MemberChap8 m3 = em.getReference(MemberChap8.class, member3.getId()); // proxy

            System.out.println("m1.getClass() == m2.getClass() = " + (m1.getClass() == m2.getClass())); // true
            System.out.println("(m1.getClass() == m3.getClass()) = " + (m1.getClass() == m3.getClass())); // false

            logic(m1, m3); // 이럴 경우 프록시 객체가 언제 넘어올지 모르느낀 instance of 비교 해야함


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void logic(MemberChap8 m1, MemberChap8 m3) {
        System.out.println(m1 instanceof MemberChap8);
        System.out.println(m3 instanceof MemberChap8);
    }
}

 

  • 동일 transaction 내에서 proxy, 엔티티 타입 일치
package hellojpa.chap8;

public class JpaChap8PC {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            // 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
            MemberChap8 member1 = new MemberChap8();
            member1.setUsername("member1");
            em.persist(member1);

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

            MemberChap8 m1 = em.find(MemberChap8.class, member1.getId()); // 영속성 컨텍스트에서 찾는다.
            System.out.println("m1 = " + m1.getClass()); // hellojpa.chap8.MemberChap8

            MemberChap8 reference = em.getReference(MemberChap8.class, member1.getId());
            System.out.println("reference = " + reference.getClass()); // hellojpa.chap8.MemberChap8 - 프록시가 안나온다!!
            // 이유
            // 1. 이미 1차 캐시에 담았는데 굳이 proxy를 사용할 이유가 없다.
            // 2. jpa에서 동일 tx 안에서는 Collection 처럼 동일 key로 가지고 온 값은 항상 동일해야한다.
            System.out.println("m1 == reference = " + (m1 == reference)); // 동일

            em.clear();

            // 반대로 proxy로 시작되면 find로도 proxy로 나온다.
            MemberChap8 proxyMember = em.getReference(MemberChap8.class, member1.getId());
            System.out.println("proxyMember.getClass() = " + proxyMember.getClass()); // class hellojpa.chap8.MemberChap8$HibernateProxy$C8QO027Z

            MemberChap8 classMember = em.find(MemberChap8.class, member1.getId());
            System.out.println("classMember.getClass() = " + classMember.getClass()); // class hellojpa.chap8.MemberChap8$HibernateProxy$C8QO027Z

            System.out.println("(proxyMember == classMember) = " + (proxyMember == classMember)); //true

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void logic(MemberChap8 m1, MemberChap8 m3) {
        System.out.println(m1 instanceof MemberChap8);
        System.out.println(m3 instanceof MemberChap8);
    }
}
 

 

  • 준영속 상태 + 프록시 메서드
    1. 프록시 인스턴스의 초기화 여부 확인 : emf.getPersistenceUnitUtil().isLoaded()
    2. 프록시 클래스 확인 방법 : entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
    3. 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);
      참고: JPA 표준은 강제 초기화 없음 강제 호출: member.getName()
package hellojpa.chap8;

public class JpaChap8Detach {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            // 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
            MemberChap8 member1 = new MemberChap8();
            member1.setUsername("member1");
            em.persist(member1);

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

            // MemberChap8 refMember = em.find(MemberChap8.class, member1.getId()); // 영속성 컨텍스트에 의존하지 않는다.
            MemberChap8 refMember = em.getReference(MemberChap8.class, member1.getId()); // 영속성 context에 의존한다.

            System.out.println("refMember.getClass() = " + refMember.getClass()); // .getClass()는 proxy초기화 필요 없이 수행
            System.out.println("proxy 초기화 확인 여부 메서드 = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

            Hibernate.initialize(refMember); // 강제 초기화
            System.out.println("proxy 초기화 확인 여부 메서드 = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

            // System.out.println("refMember.getUsername() = " + refMember.getUsername()); // 이때는 프록시 초기화가 되기 때문에 이후부터는 객체랑만 소통해서 준영속 영향 안받음
            // System.out.println("proxy 초기화 확인 여부 메서드 = " + emf.getPersistenceUnitUtil().isLoaded(refMember));


            // em.clear();
            // em.detach(refMember);
            em.close();

            System.out.println("-> refMember.getUsername() = " + refMember.getUsername());


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void logic(MemberChap8 m1, MemberChap8 m3) {
        System.out.println(m1 instanceof MemberChap8);
        System.out.println(m3 instanceof MemberChap8);
    }
}

 

2. 즉시로딩과 지연 로딩

  • @ManyToOne(fetch = FetchType.LAZY) : 지연로딩을하면 연관관계 참조 값들을 배제하고, 가지고와야할 경우 proxy로 가지고 온다.
package hellojpa.chap8;

@Entity
public class MemberChap8 {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    // @ManyToOne(fetch = FetchType.LAZY) // 지연로딩을하면 연관관계 참조 값들을 배제하고, 가지고와야할 경우 proxy로 가지고 온다.
    @ManyToOne(fetch = FetchType.EAGER) // 조인해서 한번에 가지고 온다.
    @JoinColumn(name = "TEAM_ID")
    private TeamChap8 team;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

 

  • 지연로딩을 할 시, proxy가 동작해서 필요한 시점에 연관관계가 존재하는 엔티티를 조인해서 값을 가지고 온다.
  • team을 사용할 경우 쿼리를 한번 더 날려야하기 때문에 쿼리가 2번 나가게 된다.

 

  • member와 team이 함께 사용되는 빈도가 높을 시, join해서 한 번에 가지고 오는 즉시로딩을 수행해야한다. (프록시를 사용하지 않는다.)
    - fetch = FetchType.EAGER

2.1. 프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용 (특히 실무) - 해외 가이드에서도 권장
  • 이유
    1. 즉시 로딩 적용시, 예상하지 못한 SQL이 발생
    2. 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. (어떻게 그런지 좀 더 찾아보기...)
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 이므로 LAZY 설정 필요
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

 

2.2. 지연 로딩 활용

  • 모델링 - 실습은 Member와 Team으로만 수행

 

  • Member
package hellojpa.chap8;

@Entity
public class MemberChap8 {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

//    @ManyToOne(fetch = FetchType.LAZY) // 지연로딩을하면 연관관계 참조 값들을 배제하고, 가지고와야할 경우 proxy로 가지고 온다.
    @ManyToOne(fetch = FetchType.EAGER) // 조인해서 한번에 가지고 온다.
    @JoinColumn(name = "TEAM_ID")
    private TeamChap8 team;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

 

  • Team
package hellojpa.chap8;

@Entity
public class TeamChap8 {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<MemberChap8> members = new ArrayList<>();
}

 

  • 지연로딩
package hellojpa.chap8;

public class JpaChap8Lazy {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            TeamChap8 team = new TeamChap8();
            team.setName("teamA");
            em.persist(team);

            MemberChap8 member1 = new MemberChap8();
            member1.setUsername("member1");
            member1.setTeam(team);

            em.persist(member1);

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

            MemberChap8 refMember = em.find(MemberChap8.class, member1.getId()); // lazy로 인해서 join 안된 상태로 member만 select
            System.out.println("refMember.getClass() = " + refMember.getTeam().getClass()); // team은 proxy type으로 초기화 대기중
            System.out.println("=== getTeam()을 사용시, proxy 초기화 시작 ===");
            System.out.println(refMember.getTeam().getName()); // 프록시 초기화 하면서 쿼리가 날라감  * flush(), clear() 안하면 안날라감. 학습용으로 쿼리보려구 이렇게 설정
            System.out.println("=== getTeam()을 사용시, proxy 초기화 끝 ===");


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

2.3. 지연 로딩 활용 - 실무

  • 모든 연관관계에 지연 로딩을 사용
  • 실무에서 즉시 로딩 사용 말 것
  • JPQL fetch 조인, 엔티티 그래프 기능 사용

 

3. 영속성 전이 : casecade

  • 즉시로딩, 지연로딩과 전혀 관계 없다.
  • 부모 저장시 연관된 자식 호출하고 싶을 경우 사용 - persist나 remove 여러번 치기 싫을 경우 사용

 

  • 하나의 부모가 자식들을 관리하는 경우 사용 - 게시판과 댓글, 첨부파일 경로
  • 파일을 여러군데에서 관리할 시 사용하면 안됨
  • 결론
    1. parent와 child의 life cycle이 동일 하고
    2. 단일 소유자 일 경우 사용

 

  • 상황 : 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 경우
  • ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

 

3.1. 주의

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함 제공이 목적

 

  • parent
package hellojpa.chap8;

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    //cascdeType,ALL : parent를 persist할 때, list 내부의 값들도 persist 해준다는 의미
    //orphanRemoval true : 고아 객체, 부모의 childlist에서 관리를 안하게 될 경우, 자식 엔티티를 삭제 시켜버림
    private List<Child> childList = new ArrayList<>();

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

 

  • child
package hellojpa.chap8;

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
}

 

  • run
package hellojpa.chap8;

public class JpaChap8Cascade {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);

            System.out.println("=== persist시, sequence Object next value 가져옴 ===");
            em.persist(parent);
            System.out.println("=== persist시, sequence Object next value 가져옴 ===");

            // em.persist(child1);
            // em.persist(child2);

            /* cascade
            * 내가 지금 parent를 중심으로 개발을하고 있는데 child를 일일히 persist 하는게 불편...
            * 그렇다고 em.persist(child)를 그냥 지우면 안다. child가 persist가 안되기 때문
            * 이걸 해결해주는 것이 cascade
            * */

            // 고아객체 - 객체측에서 관계를 끊게 될 시, db 측에서 해당 row를 날려버림

            System.out.println("=== flush, clear 시작 ===");
            em.flush();
            em.clear();
            System.out.println("=== flush, clear 끝 ===");

            System.out.println("===");
            System.out.println("parent.getId() = " + parent.getId());
            // sequence 전략으로 id를 가지고 올 때, 영속성 컨텍스트와 실제 객체에 값을 넣음 - flush, clear 영향 없음
            System.out.println("===");

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

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

3.2. CASCADE 종류

  • ALL: 모두 적용          (많이 사용)
  • PERSIST: 영속         (많이 사용)
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH

 

 

 

4. 고아 객체

  • 객체의 입장에서 연관관계가 끊어질 경우 db에서 삭제가 된다.

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

 

package hellojpa.chap8;

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    //cascdeType,ALL : parent를 persist할 때, list 내부의 값들도 persist 해준다는 의미
    //orphanRemoval true : 고아 객체, 부모의 childlist에서 관리를 안하게 될 경우, 자식 엔티티를 삭제 시켜버림
    private List<Child> childList = new ArrayList<>();

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

 

 

4.1. 고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 주의
  • 단일 소유시에만 사용
    • 참조하는 곳이 하나일 때 사용해야함!
    • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능 - cf> 영속성 정의는 논리적으로 들어가야 할 곳 아무곳이나 다 들어가진다.

  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.

 

5. 영속성 전이 + 고아 객체, 생명주기

CasecadeType.All + orphanRemoval=true

 

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