일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- 알고리즘
- 구현
- 그래프 이론
- 재귀
- 깊이 우선 탐색
- 브루트포스 알고리즘
- springboot
- 프로그래머스
- 문자열
- dfs
- 소수 판정
- DB
- MYSQL
- 정수론
- 프로젝트
- JPA
- Spring Security
- 그래프 탐색
- 자료 구조
- 백트래킹
- Vue
- n과 m
- 수학
- 배포
- SWEA
- 너비 우선 탐색
- 백준
- 정보처리기사
- 다이나믹 프로그래밍
- 스택
- Today
- Total
영원히 남는 기록, 재밌게 쓰자
API 개발 공부 - 지연 로딩과 조회 성능 최적화에 대해서 본문
김영한 - jpa 활용 2편을 공부하고 정리했습니다.
예제 상황은 주문 + 배송정보 + 회원을 조회하는 API를 만드는 상황이다.
주문 정보에서 배송정보와 회원을 끌어와서 조회하는 상황이다. 그림에서 주문과 회원은 N:1, 주문과 배송의 관계는 1:1 관계이다. 모두 XToOne 관계로 각 엔티티가 주문과 직접적으로 연결되어 있는 상황이다.
주문 정보로 부터 배송정보와 회원을 조회하는 요청을 하면 다음과 같은 에러가 발생한다.
IllegalStateException, StackOverflowError 발생
왜 발생했을까?
컨트롤러 호출부터 엔티티로 접근하는 흐름을 살펴보자. 그림은 회원 엔티티에 대해서만 엔티티 접근 흐름을 나타냈다.
- API컨트롤러라서 jackson 객체로 엔티티를 변환하려고 할 때 주문 엔티티로 접근한다.
- 주문 엔티티의 정보인 회원 정보로 접근한다. 회원 엔티티로 접근하여 회원의 정보를 뿌리려 했는데 회원과 주문은 양방향 관계인 경우이다.
- 회원 엔티티의 orders (주문 정보)를 가져오기 위해 또 주문 엔티티에 접근한다. 또 해당 주문 엔티티에 접근하고 2번 상황이 반복된다.
이렇게 양방향 관계로 설정한 상황에서 데이터를 순환 참조하며 무한 루프가 발생하여 stackoverflow가 발생한다.
엔티티를 직접 반환하지 말자 DTO를 활용하자
주문 엔티티에서 정보를 조회할 때 양방향 관계를 맺은 다른 엔티티와 순환 참조가 발생하는 것을 막기 위해서 양방향 관계일 때 양쪽 중 한 쪽에서 json 객체가 순환 참조를 하지 못하도록 @JsonIgnore 어노테이션을 적용해주어 무한 루프를 막을 수 있다.
하지만 이렇게 해주면 엔티티에 화면 종속적인 어노테이션들이 남게 되는데 해당 엔티티를 특정 화면에서 값을 반환할 때만 사용하는 것이 아니라 여러 API에서 사용하는데 이런 어노테이션들의 의존성이 들어가버리면 엔티티 설정 정보가 지저분해진다.
엔티티를 위와 같이 @JsonIgnore를 사용해서 반환하는 방법이 있지만 자세한 방법은 강의 자료를 참고하고 강사님이 전달하고자 한 것은 엔티티를 그대로 반환하지 말라! 라는 것이었다.
엔티티 대신 DTO 반환하기
엔티티를 조회한 다음 이를 dto로 변환하여 반환하는 방법이다. 이 경우는 단점이 있다.
N + 1문제 발생
orders를 가져오는 쿼리 1번이 발생 (N개의 결과를 가져옴)
(가져온 N개의 결과에 대해서) DTO로 변환하는 과정에서 지연로딩으로 인해 회원에 대해서 N번의 쿼리가 발생
(가져온 N개의 결과에 대해서) DTO로 변환하는 과정에서 지연로딩으로 인해 배송에 대해서 N번의 쿼리가 발생
1번의 쿼리로 여러 번의 쿼리(N번)이 생성된다고 해서 N + 1 문제라고 하는데 이런 문제가 발생할 수 있다.
결과 2개를 가져오는데 쿼리가 5번이 나간 것을 확인할 수 있다.
[
{
"orderId": 1,
"name": "userA",
"orderDate": "2024-05-26T10:40:15.130548",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
}
},
{
"orderId": 2,
"name": "userB",
"orderDate": "2024-05-26T10:40:15.142101",
"orderStatus": "ORDER",
"address": {
"city": "창원",
"street": "2",
"zipcode": "2222"
}
}
]
2024-05-26T14:35:35.202+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
o1_0.order_id,
o1_0.delivery_id,
o1_0.member_id,
o1_0.order_date,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
fetch
first ? rows only
2024-05-26T14:35:35.203+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:INTEGER) <- [1000]
2024-05-26T14:35:35.203+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735203 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id fetch first ? rows only
select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id fetch first 1000 rows only;
2024-05-26T14:35:35.204+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name
from
member m1_0
where
m1_0.member_id=?
2024-05-26T14:35:35.204+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [1]
2024-05-26T14:35:35.204+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735204 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name from member m1_0 where m1_0.member_id=?
select m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name from member m1_0 where m1_0.member_id=1;
2024-05-26T14:35:35.205+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status
from
delivery d1_0
where
d1_0.delivery_id=?
2024-05-26T14:35:35.205+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [1]
2024-05-26T14:35:35.205+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735205 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status from delivery d1_0 where d1_0.delivery_id=?
select d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status from delivery d1_0 where d1_0.delivery_id=1;
2024-05-26T14:35:35.205+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
o1_0.order_id,
o1_0.delivery_id,
o1_0.member_id,
o1_0.order_date,
o1_0.status
from
orders o1_0
where
o1_0.delivery_id=?
2024-05-26T14:35:35.214+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [1]
2024-05-26T14:35:35.214+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735214 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 where o1_0.delivery_id=?
select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 where o1_0.delivery_id=1;
2024-05-26T14:35:35.215+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name
from
member m1_0
where
m1_0.member_id=?
2024-05-26T14:35:35.215+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [2]
2024-05-26T14:35:35.215+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735215 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name from member m1_0 where m1_0.member_id=?
select m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name from member m1_0 where m1_0.member_id=2;
2024-05-26T14:35:35.215+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status
from
delivery d1_0
where
d1_0.delivery_id=?
2024-05-26T14:35:35.216+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [2]
2024-05-26T14:35:35.216+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735216 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status from delivery d1_0 where d1_0.delivery_id=?
select d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status from delivery d1_0 where d1_0.delivery_id=2;
2024-05-26T14:35:35.216+09:00 DEBUG 45924 --- [nio-8080-exec-8] org.hibernate.SQL :
select
o1_0.order_id,
o1_0.delivery_id,
o1_0.member_id,
o1_0.order_date,
o1_0.status
from
orders o1_0
where
o1_0.delivery_id=?
2024-05-26T14:35:35.216+09:00 TRACE 45924 --- [nio-8080-exec-8] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [2]
2024-05-26T14:35:35.216+09:00 INFO 45924 --- [nio-8080-exec-8] p6spy : #1716701735216 | took 0ms | statement | connection 25| url jdbc:h2:tcp://localhost/~/jpashop
select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 where o1_0.delivery_id=?
select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 where o1_0.delivery_id=2;
fetch join으로 해결하기
지연 로딩으로 인해 쿼리를 N번 날려야 하는 문제를 fetch join을 통해서 개선 시킬 수 있다. fetch join은 데이터 베이스에서 join을 통해 지연 로딩 설정을 무시하고 한번에 데이터를 가져온다.
주문 repo
그래서 같은 데이터 결과를 가져오지만 실행되는 쿼리는 하나밖에 없다!!
[
{
"orderId": 1,
"name": "userA",
"orderDate": "2024-05-26T10:40:15.130548",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
}
},
{
"orderId": 2,
"name": "userB",
"orderDate": "2024-05-26T10:40:15.142101",
"orderStatus": "ORDER",
"address": {
"city": "창원",
"street": "2",
"zipcode": "2222"
}
}
]
2024-05-26T14:50:29.521+09:00 DEBUG 45924 --- [nio-8080-exec-1] org.hibernate.SQL :
select
o1_0.order_id,
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status,
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name,
o1_0.order_date,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
join
delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
2024-05-26T14:50:29.522+09:00 INFO 45924 --- [nio-8080-exec-1] p6spy : #1716702629522 | took 0ms | statement | connection 27| url jdbc:h2:tcp://localhost/~/jpashop
select o1_0.order_id,d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status,m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id
select o1_0.order_id,d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status,m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id;
쿼리를 실행할 때에 데이터 베이스 연결하는 네트워크 트래픽을 생각했을 때 만약 서비스가 db 요청을 많이하는 서비스라면 성능 차이가 더 많이 발생할 것이다.
여기서 더 개선하는 방법이 jpa 자체에서 dto를 조회하는 방법이 있는데 같은 fetch join을 사용하지만 select에서 반환할 dto에 맞는 데이터를 조회하는 방법이 있다.
하지만 이 방법은 화면(특정 조회 기능)에 종속적인 조회 전용 repo를 따로 두어야 한다면 좋은 방법이라고 한다.
정리
- 엔티티를 직접 반환하면 필요하지 않은 데이터까지 반환해야 한다.
- 엔티티를 직접 반환하면 엔티티에 화면 의존적인 어노테이션들이 추가될 수 있어서 엔티티가 지저분해진다.
- 엔티티 보다는 DTO로 변환하여 원하는 필드만 반환하도록하자.
- 양방향 관계라면 1 + N 문제를 조심하자.
- DTO로 변환하는 것을 JPA에서 한번에 하는 것도 좋지만 특정 상황(조회전용)이 아니라면 repo에서는 엔티티를 반환하고 따로 화면에 맞는 DTO로 변환하여 반환하자
'springboot > JPA' 카테고리의 다른 글
엔티티 매핑에 대해서 (0) | 2024.06.11 |
---|---|
queryDSL을 사용해서 페이징 처리 해보기 (0) | 2024.04.20 |
프록시 (0) | 2024.02.27 |
다대일(N:1) 연관 관계에서 양방향 단방향 매핑과 연관 관계의 주인에 대해서 알아보자. (2) | 2024.02.24 |
영속성 컨텍스트의 플러시(flush)에 대해서 (0) | 2024.02.21 |