Post

Springboot Jpa - 프록시

JPA의 프록시 객체에 대해 알아보고, 이를 이용한 지연로딩에 대해서도 알아보자

Springboot Jpa - 프록시

프록시

find() 와 getReference()

find()
데이터베이스를 통해서 실제 엔티티 객체 조회
getReference()
데이터베이스 조회를 미루는 가짜(프록시)엔티티 객체 조회

프록시의 구조

프록시의 구조 프록시의 구조

프록시의 특징
실제 클래스를 상속 받아서 만들어졌기 때문에, 실제 클래스와 겉 모양이 같다.
이론상으론는 사용하는 입장에서는 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용해도된다.
프록시는 내부에 target이 따로 있는데, 이 target이 실제 레퍼런스를 참조하고 있다.

프록시 객체의 초기화

프록시의 초기화 프록시의 초기화

1
2
Member member = em.getReference(Member.class, id1);
member.getName();
  • 1 결과로 가져온 member는 실제 객체가 아닌, 프록시 객체다.
  • 2 getName()와 같이 실제 값을 조회하면, 실제 값이 없는 프록시 객체기 때문에 프록시의 초기화가 동작된다.
    이와 같은 로직은 여러번 반복하는 것이 아니라, 프록시와 실제 entity가 연결되어있지 않은 처음 1번만 동작한다.
    1. getName()호출
    2. 프록시 객체가 영속성 컨텍스트에게 초기화를 요청
    3. 영속성 컨텍스트에도 값이 없으면 DB에서 실제 데이터를 조회 = 라인 1에서 조회해야하는 내용을 실제 값이 필요할 때 조회
    4. DB에서 실제 데이터를 조회해 Entity 생성
    5. 프록시의 target은 실제 Entity를 참조하게 되며, 실제 Entity의 getName()을 실행한 결과를 반환
프록시의 특징
프록시 객체에 대한 매커니즘은 표준 스펙에는 없고, 구현하는 Hibernate와 같은 라이브러리 들이 구현하는 방식에 따라 결정되지만 이러한 흐름으로 동작한다.
프록시 객체는 처음 사용할 때 한 번만 초기화한다.
프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것이다.
프록시 객체는 원본 엔티티를 상속받은 것이라, 타입 체크시 주의해야한다. == 비교 실패, 대신 instance of 사용해야한다. -> 상속과 관련된 된다.
1
2
3
Member m1 = em.find(Member.class, 1L);
Member m2 = em.getReference(Member.class, 2L);
m1.getClass() == m2.getClass(); // false
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 getReference()를 호출시, 영속성 컨텍스트에 있는 엔티티 반환한다.
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생한다. 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.

프록시와 JPA의 동일성 보장 동일한 트랜잭션 내에서 find()getReference()중 어느것을 먼저 사용하는지에 따라 반환되는 객체가 다르다.

getReference()로 먼저 조회시
getReference()의 결과로 프록시를 반환하며, 이후 find()로 조회해도 프록시를 반환하게 된다.
find()로 먼저 조회시
find()의 결과로 Entity를 반환하며, 이후 getReference()를 호출해도 Entity를 반환하게 된다.

이렇게 동작하는 이유는 JPA에서의 동일성 보장을 하기 위해 참조값이 그대로 유지되어야해서(= ==비교가 가능해야해서) 프록시를 먼저 반환했다면 프록시로, Entity로 조회했다면 Entity로 반환하게 된다. 그런데 각 데이터별로 값을 처음 조회시 find()getReference()를 혼용해 사용하고, 이 데이터들의 타입을 비교한다면, ==이 아닌 instance of로 값을 조회해야 정확한 값의 비교가 될 수 있다.

프록시 확인

프록시 인스턴스의 초기화 여부 확인 PersistenceUnitUtil.isLoaded(Object entity)의 결과로 초기화 여부를 리턴해준다.

프록시 클래스인지 확인하는 방법 entity.getClass().getName()를 호출한 결과로 확인할 수 있다.

  • [패키지명][entity class name]$[proxy가 포함된 문자열]의 형식: 프록시
  • [패키지명][entity class name]의 형식: 엔티티

프록시를 강제 초기화 하는 방법 org.hibernate.Hibernate.initialize(entity)를 호출해 프록시를 강제 초기화 할 수 있다. 다만 JPA 표준에 있는 내용은 아니라서, Hibernate가 아닌 다른 구현 라이브러리를 사용한다면 entity.getName()과 같이 실제 값을 조회하는 로직으로 강제 초기화를 할 수 있다.

즉시 로딩과 지연 로딩

Member와 Team이 있다고 할 때, Member를 조회할 때 항상 Team의 정보도 필요한지 아니면 가끔 Team의 정보가 필요한지에 따라 로딩 방식을 변경해 성능을 향상시킬수 있다.

먼저 즉시로딩과 지연로딩에 대해 알아보자.

지연 로딩(LAZY Loading)

지연 로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Member {
  @Id @GeneratedValue
  private Long id;
  
  @Column(name = "USERNAME")
  private String name;
  
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  //생략
}
  • 9 Team에 대한 조회를 Lazy Loading으로 설정

지연 로딩을 이용한 프록시 조회1

1
2
Member m = em.find(Member.class, 1L);
Team team = m.getTeam();

지연 로딩을 이용한 프록시 조회2

1
team.getName();

Member만 조회하거나, m.getTeam()과 같이 단순 조회했을 때에는 사용되지 않다가 team.getName() 호출과 같이 실제로 team을 사용하는 시점에 프록시가 초기화 된다.

즉시 로딩(EAGER Loading)

즉시 로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Member {
  @Id @GeneratedValue
  private Long id;
  
  @Column(name = "USERNAME")
  private String name;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  //생략
}
  • 9 Team에 대한 조회를 Eager Loading으로 설정

그러면 한번에 데이터를 Join해서 같이 가져오게 되며, Team도 프록시가 아닌 Entity로 조회된다.

지연로딩과 즉시로딩의 주의

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생할 수 있기 떄문에 가급적 지연 로딩만 사용(특히 실무에서)
    연관관계가 N개일때, 모든 테이블이 즉시 로딩 설정이 되어있다고 하면, 1개의 테이블에서만 값을 조회하고 싶어도 모든 연관관계의 Join을 걸어 조회가 되기 때문에 속도가 느려짐 = 나의 의도와 맞지 않은 쿼리가 발생될 수 있다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
    JPQL로 데이터를 조회시, 조회한 이후에 연관관계를 살피게 된다. 그러면서 즉시로딩으로 설정되어있으면 결과의 수만큼 다시 연관관계 데이터를 조회하는 로직으로 동작한다. = 즉시 로딩일 때 JPQL에서는 한번에 데이터를 JOIN해서 가져오는 것이 아니라, 따로 조회하기 때문에 성능이 느려진다.
  • 연관 관계의 기본 설정이 다름
    • @ManyToOne, @OneToOne은 기본이 즉시 로딩
    • @OneToMany, @ManyToMany는 기본이 지연 로딩

영속성 전이(CASCADE)와 고아객체

영속성 전이(CASCADE)

특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 사용한다.

예를 들어서 Parent를 중심으로 코드를 작성하고 있는데, Parent를 영속할때 같이 연쇄적으로 자식까지 같이 영속되었으면 좋겠다라고 생각들 때 사용한다. @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)로 설정한다.

주의
엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐, 연관관계를 매핑하는 것과 아무 관련이 없다.

종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH
사용 예시 상황
게시판의 게시글에서 첨부파일을 올리는 경우 - 첨부파일이 게시글에서만 관리되는 경우 -> 사용하면 편리 O
게시판의 게시글에서 첨부파일을 올리는 경우 - 첨부파일이 다른 곳에서도 관리되거나 연관되어있는 경우 -> 사용하면 안됨 X

고아 객체

고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티
고아 객체 제거
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.
orphanRemoval = true으로 설정할 수 있다.
주의
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
참조하는 곳이 하나일 때 사용해야함!
특정 엔티티가 개인 소유할 때 사용 - @OneToOne, @OneToMany만 가능
개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께제거된다. = CascadeType.REMOVE처럼 동작한다.

영속성 전이 + 고아 객체, 생명주기

  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
This post is licensed under CC BY 4.0 by the author.