실무에서 조회기능을 메인으로 개발하고 있다보니 JPA 데이터 조회 최적화에 항상 관심을 가지고 있다. 특히 엔티티 설계시 엔티티간의 연관관계에 대해 중점적으로 설계하였고 이를 실무 개발에 적용하고자 하였다.
이에 해당 내용을 명확히 이해하고 적용하고자 JPA 프록시와 엔티티 연관 관계에 대해서 정리해보고자 한다.
그에 앞서 Proxy라는 것을 알아보자.
백엔드 개발자 아니 개발자 라면 Proxy Sever의 개념을 알고 있을 것이다.
Proxy Server의 개념에 대해 깊이 있게 들어가게 되면 글의 길이가 길어지니 간단히간단히 이야기하자면 Proxy Server는 실 서버와 데이터의 도메인 및 IP를 외부에 오픈하기 전 중계 역할을 한다고 생각하면 된다.
예를 들자면 내가 B.com이라는 도메인으로 데이터를 처리하고 있다고 해보자. 하지만 외부 인터넷망에 B.com 이라는 도메인을 오픈하게 될 경우 보안상으로 이슈가 생길 수 있다.
이를 A.com이라는 도메인으로 서비스를 제공하고 Proxy 설정을 통해 A.com에 요청을 B.com으로 연결해준다면 외부 인터넷망에 B.com의 정보를 오픈할 필요가 없어 보안성이 향상 될 수 있다.
각설하고 이제 JPA 프록시에 대해 알아보자.
밑의 코드는 음식 FoodEntity (이하 Food) 와 음식점 StoreEntity (이하 Store) 의 클래스 이며 두 엔티티의 관계는 N : 1 즉 @ManyToOne 관계이다.
/*
Food Entity
*/
package com.jpastudy.ms.domain.Entity;
import lombok.Getter;
import javax.persistence.*;
@Getter
@Entity
@Table(name = "tb_test_food")
public class FoodEntity {
@Id
@Column(name="food_id")
private String foodId;
private String foodName;
private String foodCalorie;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "store_id")
public StoreEntity storeEntity;
}
/*
Store Entity
*/
package com.jpastudy.ms.domain.Entity;
import lombok.Getter;
import javax.persistence.*;
@Getter
@Entity
@Table(name = "tb_test_store")
public class StoreEntity {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private String storeId;
private String storeName;
private String storeNumber;
private String address;
}
Food 와 Store는 N : 1 의 연관관계가 맺어진 것은 이해 했다. 그렇다면 Food를 조회 할 때 항상 Store의 데이터를 조회 해야 할까?
난 Food의 데이터만을 활용하여 뒷단 로직을 개발하고자 하는데 Store 데이터까지 모두 조회하게 되면 이는 리소스 낭비 일 것 이다.
각설하고 코드로 살펴보자. 우선 Food의 값만 조회해보자.
@Test
public void proxyTest(){
FoodEntity foodEntity = em.find(FoodEntity.class,5L); // food_id 값이 5L인 데이터를 찾는다.
System.out.println("======= 쿼리 전송 =======");
System.out.println("Food ID : " +foodEntity.getFoodId());
System.out.println("Food Name : " + foodEntity.getFoodName());
System.out.println("Food Calorie : " + foodEntity.getFoodCalorie());
System.out.println("======= 쿼리 결과 =======");
em.close();
}
깔끔하다.
Food값을 조회하여 ID, Name, Calorie 값들을 가져왔다.
그렇다면 그렇다면 Food와 연관관계 매핑되어 있는 Store의 값을 조회해보자.
@Test
public void proxyTest(){
FoodEntity foodEntity = em.find(FoodEntity.class,5L); // food_id 값이 5L인 데이터를 찾는다.
System.out.println("======= 쿼리 전송 =======");
System.out.println("Food ID : " +foodEntity.getFoodId());
System.out.println("Food Name : " + foodEntity.getFoodName());
System.out.println("Food Calorie : " + foodEntity.getFoodCalorie());
System.out.println(foodEntity.getStoreEntity().getClass());
System.out.println("======= 쿼리 결과 =======");
System.out.println("///////////////////////////////");
System.out.println("///////////////////////////////");
System.out.println("======= Store 데이터 =======");
System.out.println("Store ID : " + foodEntity.getStoreEntity().getStoreId());
System.out.println("Store Name : " + foodEntity.getStoreEntity().getStoreName());
System.out.println("Store Address : " + foodEntity.getStoreEntity().getAddress());
System.out.println("Store Number : " + foodEntity.getStoreEntity().getStoreNumber());
em.close();
}
여기서 Store 데이터를 조회해오는 것을 보면
Food Name : 항정살
Food Calorie : 500
class com.jpastudy.ms.domain.Entity.StoreEntity$HibernateProxy$w7bUtIS5
======= 쿼리 결과 =======
///////////////////////////////
///////////////////////////////
======= Store 데이터 =======
Store ID : 1
2022-01-31 20:41:55.592 DEBUG 55151 --- [ main] org.hibernate.SQL :
select
storeentit0_.store_id as store_id1_1_0_,
storeentit0_.address as address2_1_0_,
storeentit0_.store_name as store_na3_1_0_,
storeentit0_.store_number as store_nu4_1_0_
from
tb_test_store storeentit0_
where
storeentit0_.store_id=?
2022-01-31 20:41:55.594 INFO 55151 --- [ main] p6spy : #1643629315594 | took 0ms | statement | connection 2| url jdbc:mysql://localhost/test?serverTimezone=UTC
select storeentit0_.store_id as store_id1_1_0_, storeentit0_.address as address2_1_0_, storeentit0_.store_name as store_na3_1_0_, storeentit0_.store_number as store_nu4_1_0_ from tb_test_store storeentit0_ where storeentit0_.store_id=?
select storeentit0_.store_id as store_id1_1_0_, storeentit0_.address as address2_1_0_, storeentit0_.store_name as store_na3_1_0_, storeentit0_.store_number as store_nu4_1_0_ from tb_test_store storeentit0_ where storeentit0_.store_id=1;
Store Name : 제주돌돗촌
Store Address : 경기도 고양시
Store Number : 0311231234
1. Store ID : 기존의 영속성 컨텍스트 값을 가져온다.
2. Store Name, Address, Number는 Store값을 조회시 Store ID값을 활용한 조회 쿼리가 나간다.
그리고 이 콘솔창에서 JPA Proxy를 확인 할 수 있다. (line 3)
class com.jpastudy.ms.domain.Entity.StoreEntity$HibernateProxy$w7bUtIS5
Food를 조회했을 때 Store 객체는 'HibernateProxy' 객체 형체로 조회 되며 Store Proxy 초기화 및 호출시 영속성 컨텍스트를 조회하고 값이 없으면 DB에 쿼리를 날려 실제 값을 얻게 된다.
또한 Proxy값이 변화하는 것이 아닌 Proxy 객체를 통해서 실제 Entity로 접근 하는 것이다.
그리고 Food 객체를 Detach, Clear의 메소드를 통해서 준영속성 컨텍스트 상태로 변경 될 경우 해당 프록시 값 초기화 및 호출시 다음과 같은 에러 ( could not initialize proxy - no Session) 가 발생한다.
그리고 엔티티간 연관관계에서 Annotation으로 fetch 타입의 지연 로딩과 지연 로딩을 설정할 수 있다.
1. 지연 로딩
지연 로딩은 다음과 같이 설정할 수 있으며 (fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩
@JoinColumn(name = "store_id")
public StoreEntity storeEntity;
위의 데이터 쿼리 예시 처럼 Store 데이터 조회 시 쿼리가 한번 더 나가게 된다.
2. 즉시 로딩
즉시 로딩은 다음과 같이 설정할 수 있다. (fetch = FetchType.EAGER)
@ManyToOne(fetch = FetchType.EAGER) // 즉시로딩
@JoinColumn(name = "store_id")
public StoreEntity storeEntity;
이 경우에는 Food 조회 시 연관관계에 있는 Store 데이터 값들을 프록시가 아닌 실제 객체 값들을 조회하는 쿼리가 나가게 된다.
Food Name : 항정살
Food Calorie : 500
class com.jpastudy.ms.domain.Entity.StoreEntity
======= 쿼리 결과 =======
///////////////////////////////
///////////////////////////////
======= Store 데이터 =======
Store ID : 1
Store Name : 제주돌돗촌
더 나아가 line3에서 Proxy가 아닌 실제 Entity값을 가져오는 것을 확인 할 수 있다.
JPA 김영한님의 강의에서는 무조건 실무에서 Entity간의 관계는 지연로딩 (fetch = FecthType.LAZY) 설정으로 적용 되어야 한다고 강조했다.
그 이유는 AEnttiy.class 에 BEntity, CEntity 여러개의 연관관계가 매핑되어 있을 경우 즉시 로딩 설정을 하게 되면 (fetch = FecthType.EAGER) 그 연관관계 전체에 대한 쿼리가 나가고 이는 성능 이슈가 발생할 수 있기 때문이다.
즉 어떤 쿼리가 나갈지 모르기 때문이다.
끝.
현재 실무에서 QueryDSL을 이용하여 전체 Entity값을 DTO로 조회하고 있어 Entity간 연관관계 속성을 쓰고 있지 않다.
이번 포스팅을 통해 JPA Proxy 개념을 다시 한번 체크하고 조회 로직의 성능 향상을 위해 해당 내용을 적용할 수 있는지 검토 및 적용해 볼 예정이다.
'개발 지식 > JPA' 카테고리의 다른 글
[JPA] Connection Reset by Peer (0) | 2024.07.14 |
---|---|
[JPA] Entity Class의 @NoargsConstructor (access = AccessLevel.PROTECTED) (12) | 2022.02.06 |
[ETC] javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: Unknown integral data type for ids : java.lang.String (0) | 2022.01.31 |
[JPA] JPA가 무엇일까 (0) | 2021.12.26 |
[JPA] 1. JPA 공부를 시작해보자 (0) | 2021.06.21 |
댓글