WEB/JPA

김영한 (ORM 표준 JPA 프로그래밍 8) 프록시와 연관관계 관리

Tony Lim 2021. 3. 14. 23:32
728x90

 

//            Member findMember = em.find(Member.class, member.getId());
            Member findMember = em.getReference(Member.class, member.getId());
            System.out.println("findMember.getClass() = " + findMember.getClass());
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.username = " + findMember.getUsername());
findMember.getClass() = class hellojpa.Member$HibernateProxy$ZHmYRRE5
findMember.id = 1

getReference 시점에는 select 쿼리가 나가지 않고 밑에 print 문을 호출 하는 순간 select 쿼리가 나가게 된다. 그냥 프록시 객체를 가져온다.

 

프록시 특징

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

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

프록시 객체는 원 본 엔티티를 상속 받음, 따라서 타입 체크시 주의 해야함 ( 비교할떄는 == 대신 instance of 사용 ) 언제든지 프록시가 올 수 있다. 프록시는 상속관계임으로 instance of 비교를 하면 true가 뜬다.

영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
그래서 em find == em.getReference 를 하면 항상 true가 나온다.
같은 transaction 안에서 2개의 호출을 했을 경우 만 -> repeatable read를 보장하기 떄문에

한 tx내에서 em.getReference 를 한번이라도 호출 했으면 em.find에서도 proxy를 return 하게 된다 -> repeatable read를 보장하기 위해서

 

영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazinitializationException 예외를 터트림) , detach(), clear() , close() 도 마찬가지 exception 을 불러온다.

em.getReference 로 받아온 refMember를 detach 한후에 refMember.getUsername()을 할시에 위와 같은 Exception을 발생시킨다. -> 영속성 context에서 더 이상 관리하지 않기 때문이다.

하지만 Hibernate 5.4.1 Final 버전에서는 단순히 Session (EntityManager 가 종료) 이 종료되었다고 exception 을 띄우는게 아니라 transaction 도 종료가 되어있는지 확인을 하게 된다.

 

 

프록시 객체의 초기화

  1. 처음에 getName()을 통해서 Member target이 null 임을 확인한다. 
  2. 영속성 컨텍스트에게 초기화를 요청한다. 
  3. 영속성 컨텍스트가 DB에 쿼리를 날려 Member 엔티티를 조회해온다.
  4. target 이 Meber를 가르키게되고 메소드를 호출해준다. 

 

프록시 확인

프록시 인스턴스의 초기화 여부 확인 = PersistenceUnitUtil.isLoaded(Object entity)
entityManagerFactory 로붙어 getPersistenceUnitUtil 을 통해 얻을 수 있다.

프록시 클래스 확인 방법 = entity.getClass().getName() 

프록시 강제 초기화 = org.hibernate.Hibernate.initialize(entity), 강제호출도 마찬가지 (member.getName())

 

즉시 로딩과 지연 로딩

Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createdBy as createdb2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.LOCKER_ID as locker_i7_4_0_,
        member0_.TEAM_ID as team_id8_4_0_,
        member0_.USERNAME as username6_4_0_,
        locker1_.id as id1_3_1_,
        locker1_.name as name2_3_1_,
        team2_.TEAM_ID as team_id1_8_2_,
        team2_.createdBy as createdb2_8_2_,
        team2_.createdDate as createdd3_8_2_,
        team2_.lastModifiedBy as lastmodi4_8_2_,
        team2_.lastModifiedDate as lastmodi5_8_2_,
        team2_.name as name6_8_2_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.LOCKER_ID=locker1_.id 
    left outer join
        Team team2_ 
            on member0_.TEAM_ID=team2_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

지연로딩(Lazy)를 사용하지 않은 상태에서는 단순히 Member m = em.find() 만 해도 연관된 모든 테이블을 조인 하여 조회한다. 하지만

    @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");
            em.persist(member1);

            member1.setTeam(team);

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

            tx.commit();
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_4_0_,
        member0_.createdBy as createdb2_4_0_,
        member0_.createdDate as createdd3_4_0_,
        member0_.lastModifiedBy as lastmodi4_4_0_,
        member0_.lastModifiedDate as lastmodi5_4_0_,
        member0_.LOCKER_ID as locker_i7_4_0_,
        member0_.TEAM_ID as team_id8_4_0_,
        member0_.USERNAME as username6_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
m.getTeam().getClass() = class hellojpa.Team$HibernateProxy$lIwU9Cxb

LAZY 옵션을 적용해주면 이때는 테이블을 다 조인해서 불러오지않고 딱 Member만 select해오는 것을 볼수 있다.

또한 아직 사용하지않은 Team 객체는 프록시를 return 해주는 것도 확인가능하다. m.getTeam().getClass()를 하면 위와같이 프록시 객체를 가지고 있는것을 확인할 수 있다.

========================
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_8_0_,
        team0_.createdBy as createdb2_8_0_,
        team0_.createdDate as createdd3_8_0_,
        team0_.lastModifiedBy as lastmodi4_8_0_,
        team0_.lastModifiedDate as lastmodi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
========================

m.getTeam().getName() 팀 객체를 사용하는 순간 이때 team table을 호출하여서 필요한 값을 찾는다.

이와는 반대로 즉시로딩 Eager가 존재한다.

member를 쓸 때 비즈니스 로직에서 90%는 팀을 같이 쓴다면 굳이 지연로딩을 통해 쿼리를 2번 날리는 것보다 즉시로딩을 통해 join해서 한방 쿼리로 가져오는것이 유리하다.

 

프록시와 즉시로딩 주의

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

즉시 로딩을 적용하면 예상하지못한 Sql 이 발생한다. 

즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. 최초 쿼리가 1이고 그로인하여 발생하는 쿼리가 추가로 N개가 나간단 뜻이다.

@ManytoOne, @OneToOne은 기본이 즉시로딩임으로 LAZY 로 설정을 해주어야한다.

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

 

영속성 전이: CASCADE

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

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

@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);
    }
@Entity
public class Child
{
    @Id @GeneratedValue
    private Long id;

    private String name;

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

parent 만 영속성 켄텍스안에 넣어주었는데 저절로 child1, 2 도 자동으로 persist되었다. 

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

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

언제써야할것인가? 연관관계가 위에처럼 간단할때 써야한다 즉 소유자가 하나일때 써야한다. 다른곳에서도 Child에 연관관계가 있다하면 쓰지 않는것이 좋다.

 

 

고아객체

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

orphanRemoval=true

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

    private String name;

    @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.persist(child1);
//            em.persist(child2);

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

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

            tx.commit();
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?

childlist 에서 삭제 된 녀석은 Delete 쿼리가 날라가면서 삭제가 된다.

고아객체 주의사항

Cascade와 마찬가지로 참조하는 곳이 하나일 때 사용해야한다. 특정 엔티티가 개인 소유할때만 사용.

@OneToOne, @OneToMany 만 가능하다.

부모객체를 제거 하면 당연히 그안에있는 childlist들도 delete query를 통해서 다 날라간다.

 

CascadeType.ALL + orphanRemovel = true 이 두개의 옵션을 켜주면

부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음

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

 

 

728x90