JPA에서 또 다른 고비인, 프록시 에 대해서 알아보자.

먼저, 한 번 생각을 해보자.
과연 우리가 Member만 조회하고 싶을 때, 굳이 외래 키인 Team까지 다 끌고와야 할까?
아니다. 분명히 Member와 Team이 모두 필요한 경우가 있겠지만, Member만 필요한 경우도 있을 것이다.

그럴 때 JPA는 지연 로딩프록시 라는 기능을 제공한다.

getReference

원래 우리는 em.find()로 DB나 Context의 값을 조회했다.
하지만 위의 Method는 Find하는 시점에 Select Query를 바로 날려버린다.

하지만 getReference()로 조회를 하게 되면, Query를 날리는 시점이 바뀐다.
이후에 내가 직접 값을 조회하거나 사용할 때에만 Queyr가 날아가게 된다!

JPA에서는 getReference()에 의해 조회되는 객체를, 가짜 객체(Proxy) 라고 한다.
실제 Entity를 상속받아 겉모습만 동일하다.

Proxy

프록시 객체는 실제 객체의 참조(Target)를 보관하는 객체이다.
[사진 첨부]

getReference() 를 통해 프록시 객체를 가지고 온 뒤에, getName()을 호출하면,
당연히 프록시 객체에는 값이 없기 때문에, 영속성 컨텍스트에 값 초기화를 요청한다.
그렇게 되면, Context와 DB를 조회해 실제 Entity를 생성해서 실제 값과 매핑을 해준다.
[사진 첨부]

Proxy의 특징

  • 프록시 객체는 최초 사용 시 한번만 초기화 요청을 한다.
  • 프록시 객체를 초기화 할 때, 실제 Entity로 바뀌는 것은 아니다.
    • Proxy 객체가 초기화를 요청하면, 매핑을 받아서 본체에 접근이 가능한 것이다.
  • 영속성 컨텍스트에 찾는 Entity가 이미 있다면?
    • getReference()도 실제 Entity를 반환한다.
    • 반대의 경우도 마찬가지로, 프록시를 먼저 얻고 find를 하면 그 객체도 프록시다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태라면?
    • 프록시를 초기화할 때 문제가 발생한다.
    • LazyInitializationException

Proxy의 Method

  • 프록시 인스턴스의 초기화 여부
    • Entity Manager Factory에서 getPersistenceUnitUtil()을 얻는다.
    • 이후 isLoaded(Proxy)를 통해서 초기화 되었는지 아닌지 알 수 있다.
  • 프록시 클래스 확인 방법
    • Proxy.getClass() 를 통해서, 실제 Entity인지 Proxy인지 알 수 있었다.
  • 프록시 강제 초기화
    • Hibernate.initialize(Proxy) 로 강제 초기화가 가능하다.
    • 당연히 초기화이기 때문에, Query가 강제로 나간다.

JPA의 지연 로딩

JPA의 Annotation으로도, 프록시 객체를 구현할 수 있다.

@ManyToOne(fetch = LAZY)와 같이 연관관계에 속성을 부여하게 되면,
연관관계 요소를 지연 로딩으로 세팅을 하게 된다.

이렇게 되면, em.find(Entity)를 할 때는 Member만 가져올 수 있으며,
이후에 실제 getTeam().getName()로 Team을 사용 할 때에 초기화를 요청한다.

그럼, Member와 Team을 자주 사용한다면 어떻게 할 것인가?

###JPA의 즉시 로딩 위의 지연 로딩과 전혀 반대의 개념이다.

@ManyToOne(fetch = EAGER)와 같이 속성을 부여하게 되면,
연관관계 요소를 즉시 로딩으로 세팅을 한다.

이렇게 되면 em.find(Entity)를 할 때 모든 쿼리를 싹 보내버린다.
즉, 반환받는 객체가 Proxy가 아닌 실제 Entity가 되는 것이다.

하지만, 실무에서는 가급적이면 지연 로딩 만 사용하도록 한다!
즉시 로딩에서는, 예상치 못한 SQL문이 발생하게 된다. ==> 성능 저하 초래

이는 JPQL에서 N+1 문제를 발생시킨다.

  • ex) ```createQuery(“select m from member”, Member.class)
  • JPQL은 SQL이 그냥 내가 짠대로 들어가기 때문에, 문제가 발생한다.
  • 나는 Member만 조회했지만, 즉시 로딩이기 때문에 Team 또한 필요해진다.
  • 즉, Team에 대한 Query문이 한번 더 날아가게 되는 것이다.

@ManyToOne, @OneToOne은 기본이 즉시 로딩이다.
따라서, LAZY로 꼭 설정해주어야 한다.
@OneToMany는 기본이 지연 로딩이다.

Bonus : 영속성 전이(CASCADE)

특정 Entity를 영속 상태로 만들 때, 연관 Entity도 같이 영속시키고 싶다면?
예를 들어, 부모 Entity를 저장할 때 자식 Entity도 같이 영속시키는 것!

부모 Entity의 연관 관계에 Cascade 속성을 추가해줌으로써 사용할 수 있다.
OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) 와 같이 사용한다.
단순히, 영속성을 전이하는 것이지 연관 관계 매핑과는 전혀 관계가 없다.
단지 편의성을 제공할 뿐!

  • ALL : 모두 적용
  • PERSIST : 영속
    • 보통 ALL과 PERSIST까지만 사용한다.
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : 리프레시
  • DETACH : DETACH

사용 시, 주의할 점은 자식들을 관리하는 부모가 단 하나일 때만 사용하도록 한다!

Bonus : 고아 객체

고아 객체란, 부모 Entity와 연관 관계가 끊긴 자식 Entity를 말한다.

OneToMany(mappedBy = "parent", orphanRemoval = true)와 같이 부모에 사용한다.
부모의 자식 Collection에서 자식을 지우게 되면, 자동으로 DB에 Delete Query가 날아간다.
그리고 부모를 em.remove(parent) 하게 되면, 자식들은 자동으로 싹 다 지워지게 된다.

이것도 마찬가지로 자식을 소유한 부모가 하나일 때만 사용해야 한다!!
@OneToMany, @OneToOne`관계만이 사용 가능하다.

CASCADE와 OrphanRemoval의 장점은, 두 속성을 모두 사용하게 된다면
부모의 생명주기를 통해서 자식의 생명주기까지 동시에 관리가 가능하다는 점이다.
이는 DDD(도메인 주도 설계)의 Aggragate Root 개념을 구현할 때 유용하다.