본문 바로가기
BackEnd/java spring

[JPA] 프록시

by kkkdh 2023. 2. 15.
728x90

프록시

기능을 구현하는 과정에서 DB에서 가져오고 싶은 정보의 범위가 비지니스 로직에 따라 다르기 마련이다.

 

예를 들어 Member와 Team이라는 두 개의 entity가 연관관계 상에 있을 때, 구현 목적에 따라 두 개의 데이터가 한번에 조회되는 것이 좋을수도 있고, 그렇지 않을수도 있다.

 

이러한 상황을 JPA는 프록시와 지연로딩이라는 개념으로 기가막히게(?) 해결해준다고 한다.

 

먼저 프록시의 개념은 다음과 같다.

 

프록시 기초

  • em.find(): 앞서 살펴본 데이터베이스를 통해 실제 엔티티 객체를 조회하는 메서드
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회하는 메서드

기존에 알던 EntityManager의 find method 말고, getReference라는 신기한 메서드는 가짜 엔티티 객체를 조회한다고 하며, 이 가짜 객체를 프록시 객체라고 부른다.

 

가짜 객체를 조회하는 것의 의미는 다음과 같다.

  • 조회하는 시점에서 JPA가 만들어낸 프록시 객체를 만들어준다.
  • JPA가 영속성 컨텍스트에 없는 데이터에 대해 조회하는 시점에 query를 날린다. (필요할때 호출하는 개념)

이런 예제 코드를 돌려봤다.
실행 결과 필요한 정보가 없을때, query를 날린다.

id는 영속성 컨텍스트에 있는 정보이기 때문에, 조회 없이 출력하지만 name 정보는 DB에서 조회한 이후에 출력함을 확인할 수 있었다.

조회한 가짜 객체의 getClass() 호출 결과

위와 같이 hibernate에 의해 만들어진 가짜(proxy) 객체임을 출력해서 확인해볼 수도 있었다.

 

프록시 특징

  • 실제 클래스를 상속 받아서 만들어진다.
  • 실제 클래스와 겉모양이 같음
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상은 그렇다)

이렇게 생성된 프록시 객체는

  • 실제 객체의 참조(target)을 보관한다고 한다.
  • 프록시 객체를 호출하면, 프록시 객체가 실제 객체의 method를 호출하는 구조

출처: 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의

proxy 객체는 원래 객체에 대한 참조(target)를 갖고 있으며, 위 다이어그램 예시처럼 프록시 객체를 이용해 getName()과 같은 메서드를 호출하면, 프록시 객체가 영속성 컨텍스트에 초기화 요청을 보낸다.

 

초기화 과정을 통해 영속성 컨텍스트에 의해 실제 entity가 생성되고, 생성된 entity를 proxy 객체에서 traget을 통해 접근해 client가 원하는 정보를 반환하는 구조로 동작한다.

 

물론 한번 실제 엔티티 정보로 초기화하면, 이후에는 위 과정을 반복할 필요가 없다.

 

이런 구조로 동작하기 때문에, 앞선 예제 실습에서 getName method 호출시에 select query를 전달한 것이었다!!

 

사실 프록시 객체로 인한 동작 메커니즘은 hibernate와 같은 JPA 구현체에서 구현하기 나름이라고 한다.

 

프록시 특징 정리

  • 프록시 객체는 처음 사용할 때, 한번만 초기화한다.
  • 프록시 객체를 초기화할 때, 프록시 객체 → 엔티티 객체 ❌. 초기화시 프록시 객체를 통해 엔티티 객체에 접근 가능해질 뿐이다.
  • 프록시 객체는 엔티티 객체를 상속 받는다. 따라서 타입 체크시 (== 비교 말고, instance of 사용해야), 이건 같은 객체가 아니라는 위의 설명과도 같은 결의 개념이다.
    • 이 부분은 매번 프록시를 사용할지 안할지 모르기 때문에, JPA 사용시에 객체의 타입을 비교하는 상황에서는 instance of를 사용하는 것을 권장한다고 하신다.
  • 영속성 컨텍스트에 이미 엔티티 객체가 있는 경우 em.getReference() 호출해도 실제 엔티티 객체를 반환한다고 한다.
    • 이래서 객체 타입 비교 상황에는 instance of를 더더욱 사용해야 한다.
    • 이는 JPA의 트랜잭션 단위 작업에서 영속성 컨텍스트를 공유함으로써 repeatable read를 보장하는 특성과도 연관되어 있다.
    • 그래서 proxy로 가져온다 하더라도 같은 영속성 컨텍스트에서 조회한 엔티티 객체가 같아야 한다는 것을 보장하기 위해 이런식으로 동작함. ( == 연산에서 true를 반환해주기 위해서)
    • 이상하지만, 반대도 마찬가지이다. 먼저 프록시 객체를 호출한 상황이면, '==' 연산 결과를 true로 맞추기 위해서 이후에 find로 조회해도 프록시 객체를 반환한다.
  • 준영속 상태(영속성 컨텍스트에서 관리하지 않는)의 객체인 경우, 프록시를 초기화하면 문제가 발생한다.
    • 실무에서 정말 많이 만나는 오류라고 한다.
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다.

마지막 특징은 예제를 보자.

프록시 객체를 준영속 상태로 만든 이후(detach method를 이용했다.) 초기화를 시도하는 예제
LazyInitializationException 오류 발생

즉, 준영속 상태가된 프록시 객체를 초기화하려 시도하는 경우 (= 메서드를 호출해 영속성 컨텍스트에서 정보를 조회하려 하는 경우) 오류가 발생함을 확인할 수 있었다.

 

프록시를 확인할 수 있는 유틸리티 메서드

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity);

  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);

참고: JPA 표준에는 강제 초기화 없다. 예제에서는 강제 호출(member.getName())을 이용해 초기화 했었다.

즉시 로딩과 지연 로딩

즉시 로딩과 지연 로딩이라는 개념은 다음과 같은 상황에서 등장한다.

 

앞서 살펴본 예시에서 우리는 Member, Team entity가 서로 연관관계 상에 있음을 확인한 바가 있다. 그렇기 때문에 구현하는 비지니스 로직에 따라 다음과 같은 상황이 발생한다.

  • Member를 조회할때, Member와 연관된 Team 객체를 같이 조회하는 것이 유리한 경우
  • Member를 조회할때, 굳이 연관된 Team 객체 정보가 필요없는 경우

첫번째 경우에는 Member entity를 조회할때 마다 매번 Team entity를 join해서 데이터를 조회하는 것이 유리하다고 할 수 있겠지만, 두번째 경우에는 이런 구현 방식이 비효율적이다.

 

그렇기 때문에, 이와 같이 필요할때에만 조회하도록 구현하고 싶은 경우 지연로딩이라는 옵션을 적용할 수 있다.

 

지연 로딩은 다음과 같은 코드로 적용할 수 있다.

연관관계 차수 매핑할때, FetchType을 지정하면 된다.

위와 같이 LAZY로 fetch 옵션의 값을 설정해서 지연 로딩 방식을 적용할 수 있다.

 

이렇게 한 다음 아래의 예제 코드를 실행해보면, 

member, team entity의 연관관계를 매핑한 후 조회하는 예제

query가 다음과 같이 호출되는 결과를 확인할 수 있었다.

구분선 안의 query에서는 team을 제외한 member 객체만의 정보를 조회한다.

먼저 Member entity를 조회할 때에는 join 없이 member 객체의 정보만을 조회한 이후, Member entity 객체를 이용해 team에 접근할때, 두번째 query를 날려서 연관된 team 객체의 data를 조회하는 실행 결과를 위 예제를 통해 확인 가능했다.

 

여기서 Member entity 객체만 조회할때, 지연 로딩이 적용되기 위해서 연관된 Team entity 객체가 프록시 객체(가짜 객체)로 생성되는 것을 확인할 수 있었다.

 

요약하면, 우선 프록시 객체를 생성해서 조회한 다음 필요할때 프록시 객체가 영속성 컨텍스트를 통해 초기화 과정이 진행되고, 이후에 진짜 entity 객체와 프록시 객체가 연결되어 필요한 정보가 연결되는 구조라고 할 수 있다. (프록시 객체를 조회하는 것이 아니라 프록시 객체를 통해 entity 객체의 필드 값을 조회할때 호출되는 점에 유의하자!!)

 

이걸 "지연 로딩"이라고 부른다. 이와 반대로 즉시 로딩은 한번에 조회하고 싶을때 사용한다!

 

즉시 로딩으로 설정하기 위해서는 Fetch type을 EAGER로 설정하면 된다.

설정은 이렇게 변경하면 되고
query는 이렇게 한번에 조회하도록 join query가 날라간다.

대부분의 JPA 구현체는 가능하면, join query를 이용해 한번에 연관된 entity 객체들을 조회하려 한다고 한다.

 

프록시와 즉시 로딩 주의!!

  • 실무에서는 가급적으로 지연 로딩만을 사용!!!!
  • 즉시 로딩을 적용하면, 예상치 못한 SQL이 발생한다!!
    • join 한 두개는 별로 안느리지만, 여러 개인 경우..??
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다!!!
    • JPQL은 find와 같은 메서드와 달리 JPA를 통한 최적화 적용이 안된다.
    • createQuery method를 이용해 JPQL로 데이터를 조회하는 상황에서 즉시 로딩 방식으로 설정한 경우 연관된 데이터를 한번에 조회하기 위해서 조회한 데이터 하나마다 연관 객체 정보를 조회하기 위한 select query가 하나씩 추가적으로 실행된다. (N개의 객체 조회됬다? → N번의 select query 추가적으로 발생!!)
  • @ManyToOne, @OneToOne기본이 즉시 로딩으로 설정되어있다.
    • 따라서 Lazy로 설정을 바꿔줘야
  • 나머지 @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
  • 그러니깐 웬만하면, 직접 지정해주자!!

 

지연 로딩의 활용!

실무에서는 전부 지연 로딩으로 적용해야 하지만!!

 

이론적으로 잠깐 보도록 하자.

  • 자주 함께 사용되는 entity 객체 → 즉시 로딩!
  • 가끔 함께 사용되는 entity 객체 → 지연 로딩!

 

요약!

  • 모든 연관관계에는 지연 로딩을 사용하자!!
  • 실무에서 즉시 로딩 사용 금지
  • JPQL fetch join이나, entity graph 기능을 활용하자! (나중에 더 자세히)
  • 즉시 로딩 사용하는 경우 상상치도 못한 쿼리 나가는 경우 발생..

영속성 전이: CASCADE

영속성 전이가 뭘까??

  • 연관관계나 앞서 살펴본 즉시/지연 로딩과 전혀 관련 없는 개념임에 주의하자.
  • 영속성 전이는 특정 Entity를 영속 상태로 만들고 싶을때, 연관된 Entity도 함께 영속 상태로 만들고 싶을때 사용한다.
  • 예를 들어 부모 entity 저장할때, 자식 entity도 같이 저장하고 싶은 경우에 사용할 수 있겠다.

이렇게 서로 연관관계가 있는 Parent, Child entity가 있다고 해보자.

다음과 같이 세개의 객체에 대해서 양방향 연관관계를 설정하기 위해서는 persist method를 세번이나 호출해야 된다.

이건 너무 귀찮다.

Parent entity 중심으로 코드를 짜고, parent가 알아서 child를 관리했으면 좋겠다.

 

이럴때 사용하는 것이 CASCADE이다. Parent entity class의 childList 필드를 다음과 같이 수정하자

이렇게 옵션을 변경
코드를 이렇게 바꿔도
함께 영속화 됨.

CASCADE 종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH

위 옵션 중에서 ALL, PERSIST 정도만 사용한다고 한다. (모두 적용 or 저장할때만 같이 관리되도록 하고픈 경우)

 

실무에서도 많이 사용한다고 한다.

 

그런데 중요한 점은 언제 사용해야 하느냐이다.

  • 일대다 관계에서 항상?? → X
  • 하나의 부모다 모든 자식을 관리하는 경우
    • 게시물 하나가 여러 개의 첨부파일을 관리하는 경우
  • 그러나, 여러 곳에서 파일을 관리한다면?
    • 이때는 사용하면 안되다.
  • 정리하자면, 소유자가 하나인데 하나의 소유자가 여러 데이터를 관리하는 경우 유용하게 사용할 수 있다.
    • 라이프 사이클이 같을때
    • 단일 소유자인 경우

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것을 고아 객체 제거라고 부른다.

 

그러니깐, 고아 객체부모 엔티티와 연관관계가 끊어진 자식 엔티티를 의미한다고 볼 수 있다.

 

orphanRemoval = true

orphanRemoval 옵션을 통해 고아 객체 제거 옵션을 설정할 수 있다.

이렇게 추가하고
연관된 child entity 객체를 제거했다.
이렇게 delete query가 나간다.

고아 객체 - 주의할 점!!

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
  • 따라서 참조하는 곳이 하나일때, 사용해야 한다!!
  • 특정 엔티티가 개인 소유하는 child 객체에만 사용!
  • @OneToOne, @OneToMany만 사용 가능하다
  • 개념적으로 부모를 제거하면, 자식은 고아 객체가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모 객체를 제거하는 경우 자식 객체까지 함께 삭제된다. (CascadeType.REMOVE와 같이 동작)

 

영속성 전이 + 고아 객체, 생명 주기 (전부 킨다면)

  • casecade = CascadeType.ALL + orphanRemoval = true
  • 스스로 생명주기를 관리하는 entity는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화한 경우, 부모 entity를 이용한 자식 entity 생명주기 관리가 가능해짐
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념(repository는 aggregate root만 접근한다?)을 구현할때 유용하다고 한다. (나중에 알게 되겠지)
728x90

'BackEnd > java spring' 카테고리의 다른 글

[JPA] JPQL 1 - 기본 문법 정리  (0) 2023.02.22
[JPA] 값 타입  (1) 2023.02.18
[JPA] 고급 매핑  (0) 2023.02.06
[JPA] 다양한 연관관계 매핑  (0) 2023.02.01
[JPA] 연관관계 매핑 기초  (0) 2023.01.30

댓글