개요
이번 게시글에서는 아래의 주제에 대해 알아보겠습니다.
- 프록시
- 즉시 로딩/지연 로딩
- 영속성 전이: CASCADE
- 고아 객체
1. 프록시
- EntityManager의 find() 메서드는 DB를 통해서 실제 엔티티 객체를 조회하는 메서드
- EntityManager의 getReference() 메서드는 DB 조회를 필요한 시점에 할 프록시 엔티티 객체를 조회하는 메서드 즉, getReference() 메서드가 호출되는 시점에는 쿼리가 실행되지 않음
- 프록시 객체는 실제 객체의 참조를 보관
- 프록시 객체를 호출 시 프록시 객체는 실제 객체의 메서드 호출
예제: 직원을 나타내는 클래스가 있고 해당 직원은 어떤 기업의 소속
설명을 위한 예제
- printEmployeeAndCompany() 메서드를 호출할 경우 Employee 엔티티와 함께 Company 엔티티도 한 번에 조회해야 함
- 반면, printEmployee() 메서드를 호출할 경우 굳이 Employee 엔티티 내 Company 엔티티까지 조회할 필요 없음
- 어찌 되었든 위 예제처럼 EntityManager의 find() 메서드를 통해 엔티티를 조회하는 시점에서 쿼리가 실행돼 Employee와 Company를 모두 조회
- printEmployee() 메서드만 호출할 경우 불필요하게 Company 엔티티도 조회하기 때문에 성능적으로 손해
- 이를 위해 프록시와 지연 로딩이라는 개념이 나왔고 JPA에서는 getReference() 메서드를 제공함
find() vs getReference()
* 위 예제에서 getReference() 메서드 사용 시 바로 쿼리가 실행되는 것이 아니라 필요한 시점에 쿼리가 실행되는 것을 확인할 수 있음 (반면, find() 메서드 사용 시 바로 쿼리가 실행됨)
* 지연 로딩 관련해서는 추후 설명
1.1 프록시 객체 초기화 과정
앞서 프록시가 DB 조회가 필요한 시점에 쿼리를 실행하여 실제 객체의 참조를 보관한다는 것을 알 수 있었습니다.
이를 프록시 객체의 초기화라고 부르고 전체적인 과정은 아래와 같습니다.
Employee foundEmployee = entityManager.getReference(Employee.class, employee.getId());
foundEmployee.getName(); // 실제 DB 조회가 필요한 시점
- getName() 메서드가 호출된 시점이 실제 DB 조회가 필요한 시점
- 현재는 EmployeeProxy가 비어있으므로 PersistenceContext에 초기화 요청
- PersistenceContext가 DB를 조회하여 실제 Employee 엔티티 생성
- 실제 Employee 엔티티를 EmployeeProxy에 연결시켜줌
- EmployeeProxy의 getName() 호출
1.2 프록시의 특징
- 프록시 초기화 과정을 보면 알 수 있다시피 프록시 객체는 처음 사용할 때 한 번만 초기화 (단, PersistenceContext flush, clear 할 경우 다시 초기화해야 함)
- 프록시 객체를 초기화 시 프록시 객체를 통해서 실제 엔티티에 접근 가능 (프록시 객체가 엔티티로 바뀌는 것이 아님)
- 프록시 객체는 원본 엔티티를 상속을 받음
- 따라서, 타입 체크를 할 경우 == 비교 대신 instanceof 키워드를 통해 타입 비교를 해야 함
- 아래 예제를 통해 설명할 예정
- PersistenceContext에 이미 찾는 엔티티가 올라와 있을 경우 getReference() 메서드를 호출하더라도 프록시가 반환되는 것이 아니라 실제 엔티티 반환
- 반면, 프록시로 먼저 조회할 경우 그 이후 find() 메서드를 통해 조회를 하더라도 프록시를 반환함
- 영속 상태일 때는 프록시와 엔티티 == 비교해도 동일
- JPA에서는 같은 트랜잭션 내 즉, 같은 영속성 컨텍스트 내에서는 ==이 동일함을 보장
- PersistenceContext를 flush, clear 한 상태에서 프록시 초기화 시도할 경우 LazyInitializationException 예외가 발생
- 예제를 통해 설명할 예정
타입 체크를 instanceof로 해야 하는 이유를 설명하는 예제
영속 상태일 때, 실제 엔티티와 proxy 비교하는 예제
* 정리를 하자면 프록시와 엔티티를 먼저 조회한 쪽이 그 이후에도 조회가 되므로 개발을 진행할 때 반환되는 객체가 프록시든 엔티티든 상관없도록 개발하는 것이 중요 (예외처리를 잘하자)
LazyInitializationException 예외 발생하는 예제
1.3 프록시 관련 유용한 유틸 메서드
- PersistenceUnitUtil.isLoaded(Object entity): 프록시 인스턴스의 초기화 여부 확인하는 메서드
- org.hibernate.Hibernate.initialize(entity): 프록시 강제로 초기화하는 메서드
* JPA 공식 표준에는 강제 초기화가 없으므로 hibernate를 사용하지 않는 환경에서는 프록시의 메서드 아무거나 실행하는 방식으로 강제 초기화 진행 가능
2. 즉시 로딩/지연 로딩
2.1 지연 로딩 (Lazy Loading)
- 앞선 예제에서 printEmployee() 메서드를 호출할 때는 Employee를 조회할 때 굳이 Employee 엔티티 내 Company 엔티티까지 조회할 필요 없음
- 따라서, 성능을 극대화하기 위해서는 company 엔티티가 사용되는 시점에 쿼리를 실행하여 DB를 조회하는 것이 좋음
- 지연 로딩은 프록시로 조회할 때, 위의 내용을 적용한 내용
엔티티 정의
LazyLoading 예제
2.2 즉시 로딩 (Eager Loading)
- 앞선 예제에서 pringEmployeeAndCompany() 메서드를 호출할 때처럼 Employee를 조회할 때 Company도 같이 조회하는 빈도수가 높을 경우 즉시 로딩 사용
- 즉시 로딩을 사용할 경우 JPA가 조인을 통해 SQL 쿼리를 실행해 한번에 조회
- 하지만, 실무에서는 즉시 로딩은 가급적 사용하지 않아야 함
엔티티 정의
EagerLoading 예제
2.3 실무에서 지연 로딩만 사용해야 하는 이유
- 실무에서 즉시 로딩 즉, Eager Loading 가급적 사용 X
- Eager Loading은 JPQL에서 N+1 문제를 야기
- 간단히 요약하자면 추가 쿼리가 나가는 문제
- 실무에서는 하나의 엔티티에 다양한 연관관계가 존재할 텐데 이를 모두 JOIN문으로 조회 시 성능도 안 나올뿐더러 쿼리가 너무 많이 나감
- Lazy Loading으로 방지 가능
- @ManyToOne, @OneToOne 연관관계는 디폴트가 즉시 로딩이기 때문에 LAZY로 바꿔줘야 함
- 정리를 하자면, 모든 연관관계를 LAZY로 깔고 가고, 같이 조회하는 케이스가 더 많은 관계에 대해서만 JPQL의 fetchJoin을 사용해서 조회 시 같이 조회하도록 설정
- 혹은, JPQL의 엔티티 그래프 기능도 사용 가능
3. 영속성 전이: CASCADE
- DDL 내 CASCADE 키워드와 동일한 개념
- 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 적용하는 키워드
- Employee 엔티티 저장할 때 Company 엔티티도 함께 저장
- 이럴 경우 EntityManager의 persist 호출 횟수를 줄일 수 있음
- 영속성 전이는 편리함을 제공할 뿐 연관관계 매핑과 전혀 무관한 개념
- 연관관계 내 하나의 부모 엔티티가 있을 경우에만 사용해야 함
- 부모 엔티티가 여러 개 있는 관계에서 사용하면 안 됨
3.1 Cascade 종류
- ALL: 모두 적용
- PERSIST: 영속 (저장할 때만)
- REMOVE: 삭제 (매우 위험)
- MERGE: 병합
- REFRESH
- DETACH
* Cascade를 적용할 경우 PERSIST에만 적용할 것을 추천
Parent, Child 객체
예제
4. 고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라 부름
- orphanRemoval = true로 설정 시 고아 객체를 자동으로 제거
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 추정하고 삭제하는 기능이기 때문에 참조하는 곳이 하나일 때만 사용
- 즉, 특정 엔티티가 개인 소유할 때만 고아 객체 자동 제거 기능 적용
- @OneToOne, @OneToMany 연관관계에서 사용 가능
- 개념적으로 봤을 때, orphanRemoval = true를 활성화했을 때 부모 객체가 삭제될 경우 자식 객체도 삭제되므로 CacadeType.REMOVE처럼 동작
- 위험한 기능이므로 잘 사용해야 한다는 뜻
- CascadeType.ALL 혹은 CascadeType.REMOVE 적용 시 orphanRemoval = true로 활성화시키지 않더라도 동일하게 작동
Parent, Child 객체
예제
* 위 예시처럼 CascadeType.PERSIST와 orphanRemoval = true 기능 둘 다 활성화할 경우 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
* 이는 DDD의 Aggregate Root 개념을 구현할 때 사용됨
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
'DB > JPA' 카테고리의 다른 글
[JPA] JPQL 간단 정리 (0) | 2021.10.16 |
---|---|
[JPA] 값 타입 정리 (0) | 2021.09.29 |
[JPA] @MappedSuperclass (0) | 2021.09.07 |
[JPA] 상속관계 매핑 (2) | 2021.09.07 |
[JPA] 다양한 연관관계 매핑 (0) | 2021.08.31 |