ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7. 프록시와 연관관계 관리
    Spring-Boot/자바 ORM 표준 JPA 프로그래밍 - 기본편 2022. 12. 21. 19:00

    1. 프록시

    - 프록시 기초

        ㆍem.find() vs em.getReference()

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

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

    Member2 findMember = em.getReference(Member2.class, member2.getId());           // 사용시점에 DB에 쿼리를 날림
    System.out.println("findMember = " + findMember.getClass());                    // class hellojpa.Member2$HibernateProxy$HYA9ddRQ : 프록시 클래스
    System.out.println("findMember = " + findMember.getId());                       // 위에서 이미 알고 있기에 쿼리문 X
    System.out.println("findMember.getUsername() = " + findMember.getUsername());   // DB 조회 쿼리 요청

    - 프록시 특징

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

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

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

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

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

     

        ㆍ프록시 객체는 처음 사용할 때 한번만 초기화

        ㆍ프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서

            엔티티에 접근 가능

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

        ㆍ영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환

    Member2 member1 = new Member2();
    member1.setUsername("member1");
    em.persist(member1);
    
    em.flush();
    em.clear();
    
    Member2 refMember = em.getReference(Member2.class, member1.getId());
    System.out.println("refMember = " + refMember.getClass());  //Proxy
    
    Member2 findMember = em.find(Member2.class, member1.getId());
    System.out.println("findMember = " + findMember.getClass());
    
    System.out.println("refMember == findMember: " + (refMember == findMember));    //true  // JPA에선 타입 == 을 true로 보장해야함

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

            (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

    - 프록시 확인

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

              º PersistenceUnitUtil.isLoaded(Object entity)

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    Member2 refMember = em.getReference(Member2.class, member1.getId());
    
    System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));   // 초기화 여부

        ㆍ프록시 클래스 확인 방법

              º entity.getClass().getName() 출력 (..javasist.. or HibernateProxy...)

    System.out.println("refMember = " + refMember.getClass());  //Proxy

        ㆍ프록시 강제 초기화

              º org.hibernate.Hibernate.initialize(entity);

    Hibernate.initialize(refMember);    // 강제초기화

        ㆍ참고: JPA 표준은 강제 초기화 없음

               강제호출 : member.getName()

     

    2. 즉시 로딩과 지연 로딩

    - 지연 로딩 LAZY을 사용해서 프록시로 조회

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="TEAM_ID", insertable = false, updatable = false)  // 읽기 전용
    private Team team;
    Member2 m = em.find(Member2.class, member1.getId());
    System.out.println("m = " + m.getTeam().getClass());    // proxy
    
    System.out.println("==============");
    m.getTeam().getName();  // 실제 team을 사용하는 시점에 초기화(DB 조회)
    System.out.println("==============");

    - 즉시 로딩 EAGER를 사용해서 함께 조회

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name="TEAM_ID", insertable = false, updatable = false)  // 읽기 전용
    private Team team;

        ㆍJPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회

     

    - 프록시와 즉시로딩 주의

        ㆍ가급적 지연 로딩만 사용(특히 실무에서)

        ㆍ즉시 로딩을 적용하면 예상하지 못한 SQL이 발생

        ㆍ즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

        ㆍ@ManyToOne, @OneToOne은 기본이 즉시 로딩 → LAZY로 설정

        ㆍ@OneToMany, @ManyToMany는 기본이 지연 로딩

     

    - 지원 로딩 활용 - 실무

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

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

        ㆍJPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라! ( 뒤에서 설명 )

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

    3. 영속성 전이(CASCADE)와 고아 객체

    - 영속성 전이: CASCADE

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

        ㆍ예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

    - 영속성 전이: 저장

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)

    - 영속성 전이: CASCADE - 주의!

        ㆍ영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음

        ㆍ엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐

    - CASCADE의 종류

        ㆍALL : 모두 적용

        ㆍPERSIST : 영속

        ㆍREMOVE : 삭제

        ㆍMERGE : 병합

        ㆍREFRESH : REFRESH

        ㆍDETACH : DETACH

     

    * 하나의 부모가 자식들을 관리할 때 O / 다른 엔티티에서도 관리할 때 사용X

       = 소유자가 하나일 때 사용할 것! , 단일 엔티티에 종속적일 때

        ㆍ1. parent 와 child의 라이프사이클이 거의 유사할 때

        ㆍ2. 단일 소유자일 때

     

    - 고아 객체

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

        ㆍorphanRemoval = true

        ㆍParent parent1 = em.find(Parent.class, id);

            parent1.getChildren().remove(0);

            // 자식 엔티티를 컬렉션에서 제거

        ㆍDELETE FROM CHILD WHERE ID=?

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    Parent findParent = em.find(Parent.class, parent.getId());
    findParent.getChildList().remove(0);
    delete 
            from
                Child 
            where
                id=?

    - 고아 객체 - 주의

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

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

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

        ㆍ@OneToOne, @OneToMany 만 가능

        ㆍ참고: 개념적으로 부모를 제거하면 자식은 고아가 된다.

                     따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다.

                     이것은 CascadeType.REMOVE 처럼 동작한다.

     

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

        ㆍCascadeType.ALL + orphanRemovel=true

        ㆍ스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거

        ㆍ두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음

        ㆍ도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

     

    4. 실전 예제5 - 연관관계 관리

    - 글로벌 페치 전략 설정

        ㆍ모든 연관관계를 지연 로딩으로

        ㆍ@ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연로딩으로 변경

     

    댓글

Designed by Tistory.