본문 바로가기

백엔드/JPA

JPA 프록시와 영속성 전이

프록시에 대해

em.find()와 em.getReference() 의 차이는 프록시를 사용하고 안하고이다. find를 호출하면 select가 데이터베이스에 바로 날아가서 가져오는거고 getReference는 영속성 컨텍스트가 해당 객체에 대한 참조값만 들고있게 된다. 해당 객체에 접근하기 전까지는 select문이 나가지 않는다.

getId()를 하면 select문이 나가지 않는데 이유는 처음에 em.getReference()에서 인자로 id를 줬기때문에 영속성 컨텍스트가 알고있는 값이라 select문을 실행하지 않는다. getUsername() 할때에서야 비로소 select문이 나간다.

프록시의 특징을 정리하면 다음과 같다.

  • 프록시 객체는 처음 한번만 초기화 된다.
  • 객체에 접근해서 select문이 나갈때 프록시객체가 실제 엔티티로 바뀌는 것이 아니다. select를 통해 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것이다.
  • 프록시 객체는 원본 엔티티를 상속받는다.
  • 영속성 컨텍스트의 1차캐시에 getReference를 통해 찾는 엔티티가 이미 있다면 실제 엔티티가 호출된다. (프록시 객체가 아니고!)
  • 준영속 상태일때 프록시를 초기화하면 문제가 발생한다.

중요한 것은 getReference를 호출해도 이미 영속성 컨텍스트에 있다면 실제 엔티티 객체가 반환된다는 점이다. 이를 통해 JPA에서는 영속성을 제공한다. 한 트랙젝션 내에서 동일한 객체를 찾을 경우에는 모두 == 연산이 참이 됨을 보장하기 위해서이다. 

만약 getReference를 먼저 호출하고 find를 통해 찾으면 어떻게 될까?

Member m = new Member();
m.setName("name");
em.persist(m);
em.flush();
em.clear();
Member ref = em.getReference(Member.class, m.getId());
System.out.println("ref = " + ref.getClass());
Member find = em.find(Member.class , m.getId());
System.out.println("find = " + find.getClass());

결과는 위와 같이 나온다. find가 실행되면 select문이 나가면서 실제 엔티티 정보를 찾아오고 이를 그대로 1차 캐시에 저장하는게 아니고 1차캐시엔 기존의 getReference로 인해 생성된 프록시 객체가 있으므로 이 프록시 객체를 초기화시킨다.

 

즉시로딩 & 지연로딩

프록시 개념은 결국 즉시로딩과 지연로딩을 이해하기 위한 것이다. 즉시로딩과 지연로딩의 차이는 즉시로딩은 select문을 통해 해당 엔티티를 가져온 후 해당 엔티티가 참조하는 다른 엔티티 필드들도 즉시 쿼리를 날려서 가져오게 된다. 지
연로딩은 이후에 해당 객체를 참조할 때 select 쿼리가 나간다.

예제를 통해 확인해보자면.

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

    private String name;

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

이렇게 child가 참조하는 대상 객체인 parent를 즉시로딩으로 설정한 것부터 보자.

 Parent p = new Parent();
 p.setName("parent1");
 em.persist(p);
 Child child = new Child();
 child.setName("c1");
 child.setParent(p);

em.persist(child);

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

em.find(Child.class, child.getId());

즉시 join 쿼리가 나간다. 반면 지연로딩의 경우에는 아래와 같이 child 만을 select하며 이후에 child.getParent 를 통해 parent에 접근할 때에 select 쿼리가 나가게 된다.

 

즉시로딩 지연로딩 정리

  • 되도록 지연로딩만 사용하도록 하자.
  • 즉시로딩은 JPQL에서 1+N 문제가 발생한다.
  • x to One 시리즈는 default가 즉시로딩이므로 지연로딩으로 바꿔주자

JPQL에서 발생하는 1+N이란 무엇일까? 한번 JPQL을 써서 확인해보자. 먼저 child를 즉시로딩으로 다시 설정한 후

 List<Child> select_c_from_child_c = em.createQuery("select c from Child c", Child.class).getResultList();

실행결과를 확인하면 select문이 2개가 나가는 것을 볼 수 있다. em.find는 pk를 찍어서 가져오는 거기 때문에 join쿼리가 나가는데 JPQL은 그대로 sql로 번역되기 때문에 child만 가져오게 된다. 근데 가져오고 나니 즉시로딩으로 되어있으니 즉시 team에 대한 쿼리들을 날린다. 만약 select child의 결과가 5개면 5개마다 일일이 parent를 select하는 sql문이 나가야하는 것이다. 그래서 이를 1+N 문제라 한다.

이를 해결하는 방법

- 지연로딩
- fetch join

영속성전이

cascade 옵션을 주게되면 영속성 전이가 일어나서 연관된 엔티티들의 생명주기를 함께하게 된다. 부모 엔티티를 저장할때 자식엔티티도 함께 저장되거나 부모 엔티티가 삭제되면 함께 삭제 된다.

우선 Parent에다가 cascade ALL 옵션을 주고 연관관계 편의 메서드 addChild를 만들어둔다.

Parent p = new Parent();
p.setName("parent1");
Child child = new Child();
child.setName("c1");
p.addChild(child);

em.persist(p);

해당 코드를 실행하면 em.persist(child) 를 하지 않았음에도 불구하고 insert문이 child까지 다 나가게된다.

마찬가지로 em.remove(p)를 하게되면 child까지 delete문이 나갈것이다.

고아객체

먼저 고아객체에 대한 설정은 아래와 같이 할 수 있다.

만약 parent 객체가 삭제되면 parent에 속한 child 들은 전부 고아객체가 된다. (예제의 이름과 어울리는... ㅋㅋㅋ ㅠ)
컬렉션 프레임워크의 관점에서 보면 저 리스트에 속한 child 하나가 삭제되면 디비에는 대상 child에 대한 delete문만 나가지만 parent가 삭제되면 해당 parent의 child 리스트에 있는 모든 child들에 대해서도 delete문이 나가는 것이다.

고아객체 주의사항

  • 참조하는 곳이 하나일때만 사용해야한다. (Parent -> child)
  • 특정 엔티티가 개인소유일때 사용해야한다.
  • @OneToOne @OneToMany만 가능하다.

cascade를 ALL로 설정하고 orphanRemoval도 true로 설정하면 parent객체가 자신이 소유한 child 객체들을 관리할 수 있다. em.remove(child) 할 필요없이 parent.getChildList().remove( 대상 child ) 하면 삭제 쿼리가 나간다. 이 두 옵션을 통해서 parent가 child의 생명주기를 전부 관리하게 되는 것이다.

 

마무리

지연로딩을 쓰자! fetch join, 지연로딩, 즉시로딩의 차이를 잘 정리하자!

 

관련글

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard 수업을 들으며 정리한 글입니다.

 

'백엔드 > JPA' 카테고리의 다른 글

JPQL 기초  (0) 2021.08.08
JPA 값타입  (0) 2021.08.08
JPA 엔티티 매핑2  (0) 2021.08.05
JPA 엔티티 매핑1  (0) 2021.07.29
스프링부트 테스트 영속성과 JPQL  (0) 2021.07.27