Springboot Jpa - 프록시
JPA의 프록시 객체에 대해 알아보고, 이를 이용한 지연로딩에 대해서도 알아보자
프록시
find() 와 getReference()
- find()
- 데이터베이스를 통해서 실제 엔티티 객체 조회
- getReference()
- 데이터베이스 조회를 미루는 가짜(프록시)엔티티 객체 조회
프록시의 구조
- 프록시의 특징
- 실제 클래스를 상속 받아서 만들어졌기 때문에, 실제 클래스와 겉 모양이 같다.
- 이론상으론는 사용하는 입장에서는 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용해도된다.
- 프록시는 내부에 target이 따로 있는데, 이 target이 실제 레퍼런스를 참조하고 있다.
- 이론상으론는 사용하는 입장에서는 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용해도된다.
프록시 객체의 초기화
1
2
Member member = em.getReference(Member.class, “id1”);
member.getName();
- 1 결과로 가져온
member
는 실제 객체가 아닌, 프록시 객체다. - 2
getName()
와 같이 실제 값을 조회하면, 실제 값이 없는 프록시 객체기 때문에 프록시의 초기화가 동작된다. - 이와 같은 로직은 여러번 반복하는 것이 아니라, 프록시와 실제 entity가 연결되어있지 않은 처음 1번만 동작한다.
getName()
호출- 프록시 객체가 영속성 컨텍스트에게 초기화를 요청
- 영속성 컨텍스트에도 값이 없으면 DB에서 실제 데이터를 조회 = 라인 1에서 조회해야하는 내용을 실제 값이 필요할 때 조회
- DB에서 실제 데이터를 조회해 Entity 생성
- 프록시의 target은 실제 Entity를 참조하게 되며, 실제 Entity의
getName()
을 실행한 결과를 반환
- 2
- 프록시의 특징
- 프록시 객체에 대한 매커니즘은 표준 스펙에는 없고, 구현하는 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
2
Member m = em.find(Member.class, 1L);
Team team = m.getTeam();
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개념을 구현할 때 유용