ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프록시와 연관관계 관리
    JPA 2022. 8. 6. 06:02

    해당 내용은

    자바 ORM 표준 JPA 프로그래밍 - 기본편 (https://www.inflearn.com/course/ORM-JPA-Basic/dashboard)

    강의를 듣고 정리한 포스트입니다.

     

    프록시

    무언가를 사용할때는 이거를 써야되지 이게 굉장히 중요하다

        public static void main(String[] args) {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");


            EntityManager em = emf.createEntityManager();


            EntityTransaction tx = em.getTransaction();
            tx.begin();
            try {
                Member member = new Member();
                //printMemberAndTeam(member);
                printMember(member);
                tx.commit();
            } catch (Exception e) {
                tx.rollback();
            } finally {
                //close
                em.close();
                emf.close();
            }
        }


        private static void printMember(Member member) {
            System.out.println("member = " + member.getName());
        }
      // 기존에는 멤버와 팀의 정보가 필요했지만 현재는 로직이 변화하여
      // 멤버에 관한 정보밖에 쓰지않음 이런경우 데이터를 가져올 2개의 테이블을 참조하므로
      // 굉장히 낭비다
        private static void printMemberAndTeam(Member member) {
            String username = member.getName();
            System.out.println("username = " + username);


            Team team = member.getTeam();
            System.out.println("team = " + team.getName());
        }

    Member를 조회할 때 Team도 함께 조회해야 할까?

     

    회원과 팀 함께 출력

     

    회원만 출력

     

     

    프록시 기초

     

    • em.find() vs em.getReference()

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

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

     

     

    기존 코드

                Member member = new Member();
                member.setUsername("hello");
                em.persist(member);
                em.flush();
                em.clear();
                Member findMember = em.find(Member.class, member.getId());
                System.out.println("findMember = " + findMember.getId());
                System.out.println("findMember = " + findMember.getUsername());
                tx.commit();

     

    쿼리문을 보면 jpa 조인을 해서 한방에 가져옴

        select
            member0_.MEMBER_ID as member_i1_4_0_,
            member0_.INSERT_MEMBER as insert_m2_4_0_,
            member0_.createdDate as createdd3_4_0_,
            member0_.UPDATE_MEMBER as update_m4_4_0_,
            member0_.lastModifiedDate as lastmodi5_4_0_,
            member0_.TEAM_ID as team_id7_4_0_,
            member0_.USER_NAME as user_nam6_4_0_,
            team1_.TEAM_ID as team_id1_6_1_,
            team1_.INSERT_MEMBER as insert_m2_6_1_,
            team1_.createdDate as createdd3_6_1_,
            team1_.UPDATE_MEMBER as update_m4_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 getReference 사용할 경우

                Member member = new Member();
                member.setUsername("hello");
                em.persist(member);
                em.flush();
                em.clear();
                //Member findMember = em.find(Member.class, member.getId());
                Member findMember = em.getReference(Member.class, member.getId());
                System.out.println("findMember = " + findMember.getId());
         

    이렇게 경우 실제 가져와야할 DB 쿼리를 날린다.

     

     

    클래스 정보를 확인해보자

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

     

    출력결과

    findMember.getClass() = class hello.jpa.Member$HibernateProxy$T8BDlnF7

     

    하이버네이트가 만든 가짜 레퍼런스를 준다.

     

    프록시 특징

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

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

    • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고

      사용하면 됨(이론상)

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

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

    프록시 객체의 초기화

     

    프록시의 특징

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

    • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초

    기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

    • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비

    교 실패, 대신 instance of 사용)

    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해

    도 실제 엔티티 반환

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

    문제 발생

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



    Member refMember = em.getReference(Member.class,member1.getId());
    //영속성 컨텍스트를 끝내버리건, 해당 객체를 관리하지 않겠다는 로직호출 에러발생
    em.detach(refMember); //refMember 영속성 컨텍스트에서 관리하지 않겠다는 의미
    em.close(); //영속성 컨텍스트를 끝내버림
    em.clear(); //사라진건 아니지만 다시 시작한다.
    //호출 에러가 발생함 하이버네이트일경우 (LazyInitializationException  예외)
    refMember.getUserName();

    왜냐 기본 구조가 프록시는 영속성 컨텍스트를 이용하기 때문에

     

    • jpa  트랜잭션 안에서 영속성 컨텍스트 안의 객체는 보장을 해줘야된다.
    • 프록시로  == 비교 할때 true 된다. 실제 엔티티를 반환하게된다.

     

     

    프록시 확인

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

    PersistenceUnitUtil.isLoaded(Object entity)

    • 프록시 클래스 확인 방법

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

    HibernateProxy…)

    • 프록시 강제 초기화

    org.hibernate.Hibernate.initialize(entity);

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

    강제 호출: member.getName()

     

                Member refMember = em.getReference(Member.class, member.getId());
                System.out.println("refMember = " + refMember.getClass()); //Proxy
                //refMember.getUsername();
                //프록시 인스턴스의 초기화 여부 확인
                //System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
                //강제 초기화
                Hibernate.initialize(refMember);
                tx.commit();

     

     

    즉시 로딩과 지연 로딩

     

    Member를 조회할 때 Team도 함께 조회해야 할까?

     

    단순히 member 정보만 사용하는 비즈니스 로직

    println(member.getName());

     

     

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

    @Entity
    public class Member extends  BaseEntity{
        @Id @GeneratedValue
        @Column(name="MEMBER_ID")
        private Long id;
        @Column(name = "USER_NAME")
        private String username;


        @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());
                tx.commit();

     

    결과

    m.getTeam().getClass() = class hello.jpa.Team$HibernateProxy$5XWz8ywo

    팀은 프록시 객체로 생성된다.

     

     

    테스트2 : 프록시객체가 실제 값이 호출될 시에 쿼리를 날려 해당 값을 가져오는 것을 확인 하기 위한 테스트

                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("===========");

     

    결과 : 실제 값이 필요할 경우만 프록시를 사용해 값을 가져오는경우를 확인 있다.

    m.getTeam().getClass() = class hello.jpa.Team$HibernateProxy$GyYrFc84
    ===========
    Hibernate:
        select
            team0_.TEAM_ID as team_id1_6_0_,
            team0_.INSERT_MEMBER as insert_m2_6_0_,
            team0_.createdDate as createdd3_6_0_,
            team0_.UPDATE_MEMBER as update_m4_6_0_,
            team0_.lastModifiedDate as lastmodi5_6_0_,
            team0_.name as name6_6_0_
        from
            Team team0_
        where
            team0_.TEAM_ID=?
    ===========

     

    지연 로딩

    레이지 로딩 , 지연로딩이라고 한다.

     

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

     

     

    Member와 Team을 자주 함께 사용한다면?

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

    @Entity
    public class Member extends  BaseEntity{
        @Id @GeneratedValue
        @Column(name="MEMBER_ID")
        private Long id;
        @Column(name = "USER_NAME")
        private String username;


        //@ManyToOne(fetch = FetchType.LAZY)
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name="TEAM_ID")
        private Team team;

     

    결과

        select
            member0_.MEMBER_ID as member_i1_4_0_,
            member0_.INSERT_MEMBER as insert_m2_4_0_,
            member0_.createdDate as createdd3_4_0_,
            member0_.UPDATE_MEMBER as update_m4_4_0_,
            member0_.lastModifiedDate as lastmodi5_4_0_,
            member0_.TEAM_ID as team_id7_4_0_,
            member0_.USER_NAME as user_nam6_4_0_,
            team1_.TEAM_ID as team_id1_6_1_,
            team1_.INSERT_MEMBER as insert_m2_6_1_,
            team1_.createdDate as createdd3_6_1_,
            team1_.UPDATE_MEMBER as update_m4_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 hello.jpa.Team
    ===========
    ===========

    한번에 해당 정보를 가져온다.

     

     

    즉시 로딩

    만약에 90% 이상 member 사용시에 Team 정보를 사용한다.

    그렇다면 다음과 같이 두는

     

    즉시 로딩(EAGER), Member조회시 항상 Team도 조회

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

     

     

    프록시와 즉시로딩 주의

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

    • 왜냐? 실무에서는 관련된 테이블이 십수개된다. EAGER 설정하면 em.find 호출하는순간 수십개 테이블을 조인하게 된다.
    • EAGER 쓰면 괜찮겠다 싶어서쓰면 바로 망함 DBA한테 연락

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

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

    • 뭔말이냐
    List<Member>members = em.createQuery("select m from Member m",Member.class).getResultList();
    //SQL : select * from Member // 쿼리가 나갈껄 예상하고 JPQL 작성함

    그러나 Member Entity Team 즉시로딩이기 때문에

    해당 리스트에 해당하는 Team select해서 가져온다.

    //select * from Team where TEAM_ID = xxxx 이런식으로 쿼리가 나간다.

     

    N+1 문제란?

    • 처음 쿼리를 1 날렸는데 추가쿼리가 N 추가되서 나가는 문제

     

    해결방법

    • 일단 지연로딩으로 깔음
    • JPQL에서 배우는 패치 조인 (런타임에 동적으로 원하는애들을 선택해서 가져오는 )
    List<Member>members = em.createQuery("select m from Member m join fetch m.team",Member.class).getResultList();

    • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정

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

     

    지연 로딩 활용

    지연 로딩 활용

    • Member와 Team은 자주 함께 사용 -> 즉시 로딩

    • Member와 Order는 가끔 사용 -> 지연 로딩

    • Order와 Product는 자주 함께 사용 -> 즉시 로딩

    이론적인 실무에서는 지연로딩으로 발라야됨

     

    지연 로딩 활용 - 실무

     

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

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

    • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!

    (뒤에서 설명)

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

     

     

    영속성 전이: CASCADE

     

    영속성 전이: CASCADE

    • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속

    상태로 만들도 싶을 때

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

     

    예제

     Parent 클래스

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


        private String name;


        @OneToMany(mappedBy = "parent")
        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;
        }


        public List<Child> getChildList() {
            return childList;
        }


        public void setChildList(List<Child> childList) {
            this.childList = childList;
        }
    }

     

    child 클래스

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


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


        public Parent getParent() {
            return parent;
        }


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

    테스트 코드

     

                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);


                tx.commit();

    이런식으로 저장해야 DB 들어간다.

     

    나가는 쿼리문

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

    방식의 단점 : 귀찮다

    뭐가 귀찮냐 ?

    나는 지금 parent 중심으로 코드를 짜고있음

    parent persist 자동으로 child persist 했음 좋겠음

     

     

    그래서 그냥 em.persist(parent); 한다면

    그냥 parent insert 되고

    child 되지 않음

     

    이때 사용하는 CASCADE

    Parent 클래스

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

    이렇게하면 parent persist해도 child persist 된다.

     

    한마디로 연쇄

     

    영속성 전이: 저장

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

     

     

     

    영속성 전이: CASCADE - 주의!

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

    • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함

    을 제공할 뿐

     

    CASCADE의 종류

    • ALL: 모두 적용

    • PERSIST: 영속

    • REMOVE: 삭제

    • MERGE: 병합

    • REFRESH: REFRESH

    • DETACH: DETACH

     

    게시판과, 게시물의 첨부파일의 데이터(경로) 인경우 관리하는 경우

    게시물의 첨부파일을 게시물에서만 관리함

     

    쓰면 안되는 케이스

    파일을 여러군데에서 관리 하면 쓰면안됨

     

    소유자가 1개일때

    연관관계가 다른 테이블이 있다하면 쓰면 안됨

    그러면 운영이 너무 힘들어짐

     

    단일 Entity 종속적일때 라이프사이클이 똑같기때문에 쓰면 되고

    단일소유자일때 쓴다

     

    고아 객체

    고아 객체

    • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티

    를 자동으로 삭제

    • orphanRemoval = true

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

    parent1.getChildren().remove(0);

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

    • DELETE FROM CHILD WHERE ID=?

     

    예시

    Parent 클래스

    @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);


                tx.commit();

     

    orphanRemoval 사용하면

    Parent(부모 테이블) 에서 빠진 child 자식 테이블의 객체를 지워준다.

     

    고아 객체 - 주의

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

    보고 삭제하는 기능

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

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

    • @OneToOne, @OneToMany만 가능

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

    아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께

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

     

    조심히 써야함

     

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

     

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

    • CascadeType.ALL + orphanRemovel=true

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

    em.remove()로 제거

    • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명

    주기를 관리할 수 있음

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

    유용

     

     

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

    글로벌 페치 전략 설정

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

    • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연

    로딩으로 변경

    하는 방법

    • 일단 모든 Entity 뒤진다.
    • @ManyToOne , @OneToOne 발견하면 fatch= LAZY  변경한다.

     

    @Entity
    public class Category extends BaseEntity{


        @Id
        @GeneratedValue
        private Long id;
        private String name;


        //상위카테고리
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name="PARENT_ID")
        private Category parent;

     

    OneToMany 신경안 써도 기본전략이 LAZY

    @Entity
    public class Delivery extends BaseEntity{
        @Id @GeneratedValue
        private Long id;
        private String city;
        private String street;
        private String zipcode;
        private DeliveryStatus status;


        @OneToOne(mappedBy = "delivery" , fetch = FetchType.LAZY)
        private Order order;

     

    public class Order extends BaseEntity{


        @Id
        @GeneratedValue
        @Column(name="ORDER_ID")
        private Long id;


        @ManyToOne(fetch = LAZY)
        @JoinColumn(name="MEMBER_ID")
        private Member member;


        @OneToOne(fetch = LAZY)
        @JoinColumn(name="DELIVERY_ID")
        private Delivery delivery;

     

    @Entity
    public class OrderItem extends BaseEntity{
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ITEM_ID")
        private Long id;


        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "ORDER_ID")
        private Order order;


        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name ="ITEM_ID")
        private Item item;
        private int orderPric

     

     

    영속성 전이 설정

    • Order -> Delivery를 영속성 전이 ALL 설정

    • Order -> OrderItem을 영속성 전이 ALL 설정

     

    오더랑 주문은 같이 쓰겠다.

    public class Order extends BaseEntity{


        @Id
        @GeneratedValue
        @Column(name="ORDER_ID")
        private Long id;


        @ManyToOne(fetch = LAZY)
        @JoinColumn(name="MEMBER_ID")
        private Member member;


        @OneToOne(fetch = LAZY , cascade = CascadeType.ALL)
        @JoinColumn(name="DELIVERY_ID")
        private Delivery delivery;

     

    Order 생성 ,  Delivery 객체를 셋팅해서 넣은후

    order em.persist 하면  Delivery 객체도 다시 집어넣어진다.

        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
        private List<OrderItem> orderItems = new ArrayList<>();

    추가로 바뀌어준다.

     

    이제 Order 생성하고 Delivery 객체랑 OrderItem 객체를 생성해서 셋팅하고

    Order em.persist(order) 해서 집어넣으면

    Delivery, OrderItem 테이블에도 insert 된다.

    'JPA' 카테고리의 다른 글

    고급 매핑  (0) 2022.07.30
    다양한 연관관계 매핑  (0) 2022.07.29
    연관관계 매핑 기초  (0) 2022.07.27
    엔티티 매핑  (0) 2022.07.26
    영속성 관리 - 내부 동작 방식  (0) 2022.07.26
Designed by Tistory.