오봉이와 함께하는 개발 블로그
JPA 2 - 간단한 주문 조회 V1 : 엔티티 직접 노출 본문
간단한 주문 조회 V1: 엔티티를 직접 노출
주문 + 배송정보 + 회원을 조회하는 API를 만들자.
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.
OrderSimpleApiController
xToOne(ManyToOne, OneToOne) 관계를 어떻게 최적화 하는지에 대해 알아보자.
/** *
* xToOne(ManyToOne, OneToOne) 관계 최적화
* Order
* Order -> Member
* Order -> Delivery
*
*/
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
return all;
}
}
- 엔티티를 직접 노출하는 것은 좋지 않다.
- order -> member와 order -> address는 지연로딩이다.
- 따라서 실제 엔티티 대신 프록시 존재
- jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름
- 예외 발생
- Hibernate5Module을 스프링 빈으로 등록하면 해결(스프링 부트 사용중)
스택 오버 플로우가 발생한다.
Order 엔티티에는 Member가 있고, Member 엔티티에도 Order가 있다. 즉, 무한루프에 걸린 것이다.
객체를 JSON으로 만드는 jackson라이브러리는 Order에도 Member, Member에도 Order를 보고 무한으로 뽑아낸 것이다.
양방향 연관관계가 걸리는 부분마다 @JsonIgnore
를 걸어주면 문제는 해결된다.
-> 예 : Order와 Member가 있으면 Member에 있는 orders(Order)에 @JsonIgnore
적용
양방향 연관관계가 걸리는 부분마다 @JsonIgnore
를 걸고 실행해도 오류가 발생한다.
먼저 결론만 말하면 지연 로딩(fetch = LAZY
)으로 설정했기 때문이다.
지연 로딩으로 설정했기 때문에 조회하는 엔티티(Order
) 외에 양방향 연관관계가 걸려 있는 다른 엔티티(Member
)에 대해서 조회(삭제, 수정 포함)하지 않으면 쿼리가 날아가지 않고 프록시 객체
가 생성되어 Order
가 조회된다.
jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모르기 때문에 예외가 발생한다!
Hibernate5Module 등록
우선 build.gradle
에 다음 라이브러리를 추가하자.implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
// JpashopApplication에 추가.
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
여기까지 하면 아래와 같은 결과가 나온다.
(@JsonIgnore
, Hibernate5Module
)
[
{
"id": 4,
"member": null,
"orderItems": null,
"delivery": null,
"orderDate": "2022-09-04T21:28:46.842452",
"status": "ORDER",
"totalPrice": 50000
},
{
"id": 11,
"member": null,
"orderItems": null,
"delivery": null,
"orderDate": "2022-09-04T21:28:46.901549",
"status": "ORDER",
"totalPrice": 220000
}
]
다음과 같이 설정하면 강제로 지연 로딩 가능
@Bean
Hibernate5Module hibernate5Module() {
Hibernate5Module hibernate5Module = new Hibernate5Module();
hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
return hibernate5Module;
}
이 옵션을 키면 order
-> member
, member
-> orders
양방향 연관관계를 계속 로딩하게 된다. 따라서 @JsonIgnore
옵션을 한곳에 주어야 한다.
[
{
"id": 4,
"member": {
"id": 1,
"name": "userA",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
}
},
"orderItems": [
{
"id": 6,
"item": {
"id": 2,
"name": "JPA1 BOOK",
"price": 10000,
"stockQuantity": 99,
"categories": [],
"author": null,
"isbn": null
},
"orderPrice": 10000,
"count": 1,
"totalPrice": 10000
},
{
"id": 7,
"item": {
"id": 3,
"name": "JPA2 BOOK",
"price": 20000,
"stockQuantity": 98,
"categories": [],
"author": null,
"isbn": null
},
"orderPrice": 20000,
"count": 2,
"totalPrice": 40000
}
],
"delivery": {
"id": 5,
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"status": null
},
"orderDate": "2022-09-04T21:33:49.856508",
"status": "ORDER",
"totalPrice": 50000
},
{
"id": 11,
"member": {
"id": 8,
"name": "userB",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
}
},
"orderItems": [
{
"id": 13,
"item": {
"id": 9,
"name": "SPRING1 BOOK",
"price": 20000,
"stockQuantity": 197,
"categories": [],
"author": null,
"isbn": null
},
"orderPrice": 20000,
"count": 3,
"totalPrice": 60000
},
{
"id": 14,
"item": {
"id": 10,
"name": "SPRING2 BOOK",
"price": 40000,
"stockQuantity": 296,
"categories": [],
"author": null,
"isbn": null
},
"orderPrice": 40000,
"count": 4,
"totalPrice": 160000
}
],
"delivery": {
"id": 12,
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"status": null
},
"orderDate": "2022-09-04T21:33:49.901703",
"status": "ORDER",
"totalPrice": 220000
}
]
강제 지연 로딩 대신 다른 방법 사용
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // order.getMember()까지는 프록시지만, getName()을 하는 순간 Lazy를 강제 초기화 함.
order.getDelivery().getAddress(); // order.getDelivery()까지는 프록시지만, getAddress()을 하는 순간 Lazy를 강제 초기화 함.
}
return all;
}
[
{
"id": 4,
"member": {
"id": 1,
"name": "userA",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
}
},
"orderItems": null,
"delivery": {
"id": 5,
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"status": null
},
"orderDate": "2022-09-04T21:40:58.260084",
"status": "ORDER",
"totalPrice": 50000
},
{
"id": 11,
"member": {
"id": 8,
"name": "userB",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
}
},
"orderItems": null,
"delivery": {
"id": 12,
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"status": null
},
"orderDate": "2022-09-04T21:40:58.308573",
"status": "ORDER",
"totalPrice": 220000
}
]
엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을
@JsonIgnore
처리 해야 한다. 안그러면 양쪽을 서로 호출하면서 무한 루프가 걸린다.
정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다. 따라서
Hibernate5Module
를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법이다.
지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다. 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라!
인프런 김영한 지식공유자님 강의 - 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
'BE > JPA' 카테고리의 다른 글
JPA 2 - 간단한 주문 조회 V3 : 엔티티를 DTO로 변환 (페치 조인 최적화) (0) | 2022.09.04 |
---|---|
JPA 2 - 간단한 주문 조회 V2 : 엔티티를 DTO로 변환 (0) | 2022.09.04 |
JPA 2 - API 개발 고급 : 조회용 샘플 데이터 입력 (0) | 2022.09.04 |
JPA 2 - 회원 조회 API (0) | 2022.09.04 |
JPA 2 - 회원 수정 API (0) | 2022.09.04 |