[JPA] 프록시와 연관관계 관리

즉시로딩과 지연로딩 / 영속성 전이(CASCADE)와 고아 객체

Posted by Wonyong Jang on August 25, 2020 · 12 mins read

이전글에서 여러가지 연관관계 매핑에 대해서 살펴봤다.

이번 글에서는 프록시와 연관관계 관리에 대해서 살펴볼 예정이다.


1. 프록시

이전글에서 실습했던 것처럼 JPA에서는 em.find()를 통해서 데이터베이스의 실제 엔티티 객체를 조회한다.

JPA에서는 find() 메소드 외에도 em.getReference()도 존재하는데, 이는 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체가 조회된다.

프록시는 실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같다.

이론상으로 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

아래 코드를 보자.

Member member = new Member();
member.setName("hello");

em.persist(member);

em.flush(); // 영속성 컨텍스트에 있는 내용 DB에 반영   
em.clear(); // 영속성 컨텍스트 비우기 

Member findMember = em.getRererence(Member.class, member.getId()); // 실제 쿼리가 나가진 않는다.  
System.out.println("output : " + findMember.getName()); // 실제 사용할때 쿼리가 나가서 값을 가져온다.   

System.out.println("class : " + findMember.getClass()); // class hellojpa.Member$HibernateProxy$odcVhpjy   

위 코드에서 em.getRerence()의 반환값은 프록시 클래스이다.
프록시 객체는 실제 객체의 참조(target)를 보관한다.
즉 member.getName()을 요청했을 때, 프록시 객체에 값이 없으면 영속성 컨텍스트에 값을 요청(초기화)해서 값을 매핑 시킨다.

참고로, spring data jpa에서는 getOne(ID) 메소드를 제공하며, 해당 메소드를 사용하면 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.

스크린샷 2022-03-01 오후 11 28 37

프록시의 특징을 잘 알고 있는 것이 중요하며, 내용은 아래와 같다.

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

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

  • 프록시는 원본 객체를 상속받는다. 따라서 타입 체크시 주의가 필요하다 (비교 연산을 == 대신, instanceof 사용 해야한다)

      // m1, m2가 각각 실제 엔티티로 들어올지, 프록시로 들어올지 
      // 모르기 때문에 아래 코드는 같은 클래스임에도 false로 나오는 문제가 발생할 수 있다.   
      void logic(Member m1, Member m2) {
          if(m1.getClass() == m2.getClass()){
              //...
          }
      }
    
      // instanceof 사용
      void logic(Member m1, Member m2) {
          if(m1 instanceof Member) { 
          }
      }
    
  • 영속성 컨텍스트에 찾는 엔티티가 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.

      Member member1 = new Member();
      member1.setUsername("member1");
      em.persist(member1);
    
      em.flush();
      em.clear();
    
      Member m1 = em.find(Member.class, member1.getId());
    
      Member reference = em.getReference(Member.class, member1.getId());
    
      // 둘다 실제 엔티티 반환   
      System.out.println(m1 == reference) // true 
    
      // m1, refernce는 둘다 실제 엔티티를 반환한다.   
      // JPA에서는 같은 트랜잭션 내에서 같은 인스턴스를 조회하면 항상 같아야 한다.   
      // 한 트랜잭션 내에서 같은 인스턴스가 같다는 것을 보장해줘야 하기 때문에 이를 보장해준다.   
        
      // 그럼 반대로, 아래와 같은 상황을 보면,
      // 둘다 프록시를 반환한다.   
      Member refMember = em.getReference(Member.class, member1.getId());
    
      Member findMember = em.find(Member.class, member1.getId());
      // 한 트랜잭션 내에 같은 인스턴스는 같음을 보장해줘야 하기 때문에 
      // JPA는 이를 맞춰준다.   
    
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화시 문제가 발생한다.

      Member member1 = new Member();
      member1.setUsername("member1");
      em.persist(member1);
    
      em.flush();
      em.clear();
    
      Member reference = em.getReference(Member.class, member1.getId());  
    
      // 아래와 같이 영속성 컨텍스트 관리를 중지해버리면 refMember를 사용할 때, 에러 발생 
      em.detach(refMember); // 준영속으로 변경 
      // em.clear();  
      // em.close();
    
      refMember.getUsername(); // 에러가 발생한다.   
    
      tx.commit();
    

2. 즉시로딩과 지연로딩

이제 위에서 배운 프록시를 이용하여 즉시로딩과 지연로딩에 대해서 살펴보자.

2-1) 즉시로딩

하나의 Member에 Team 객체를 @ManyToOne 연관관계로 가지고 있다고 했을 때, 비지니스에 따라 Member만을 조회한다면 매번 Team 테이블에 조인해서 같이 가져오는 것이 비효율적일 것이다.

이때 JPA는 아래와 같이 지연로딩을 지원한다.

스크린샷 2022-03-03 오전 12 06 39

Team을 지연로딩으로 셋팅했기 때문에, Member를 find했을 때, Team에 대한 조회 쿼리가 나가지 않는다.

Team 객체를 실제 사용할 때, 프록시가 초기화되어 값을 가져온다.

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

    @Column(name = "USERNAME")
    private String username;

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

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    //...
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
em.persist(member);

member.setTeam(team);

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

Member m = em.find(Member.class, member.getId());

// 지연로딩으로 셋팅했기 때문에 프록시가 출력된다.
System.out.println("m : "+m.getTeam().getClass()); // class helloJpa.Team$HibernateProxy$VxocnX4r   

// 실제 사용하는 시점에 프록시를 통해서 값을 가져온다.
// getTeam() 할때는 프록시 객체를 가져오기 때문에 실제 쿼리가 나가지 않고, 
// 실제 값을 사용할 때 쿼리가 나간다.
System.out.println(m.getTeam().getName());

tx.commit();

2-2) 즉시로딩

지연로딩과는 반대로 즉시로딩에 대해서 살펴보자.

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

    @Column(name = "USERNAME")
    private String username;

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

위처럼 변경해서 실행을 해보면, Member와 Team을 조인하여 한번에 가져온다.
이때는, 프록시를 사용하지 않고 실제 엔티티를 가져온다.

즉시로딩을 사용하게 되면 연관관계가 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다.

또한, 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워지게 때문에, 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치조인을 사용하자.


3. Cascade(영속성 전이)

아래 예제를 먼저 살펴보자.

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

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> childes = new ArrayList<>();

    public void addchild(Child child) {
        childes.add(child);
        child.setParent(this);
    }
}
@Setter
@Getter
@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;

}

현재 parent와 child는 양방향 연관관계로 맺어진 상황이다.

영속성 전이(Cascade)의 경우 아래와 같이 6가지 종류가 있으며 주로 사용되는 것은 ALL 또는 PERSIST 이다.

  • ALL
  • PERSIST
  • REMOVE
  • MERGE
  • REFRESH
  • DETACH

ALL의 경우는 모든 영속성이 전이 되는 경우이고, PERSIST의 경우 엔티티가 저장될때만 연쇄적으로 저장되게 하는 옵션이다.


Referrence

https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21670?tab=curriculum&volume=1.00