티스토리 뷰
JPA를 이용해 구현을 하다보면 그래프 탐색을 통해 다양한 테이블을 탐색하게 된다.
하지만 객체를 기반으로 구현돼있는 JPA는 자칫 잘못하면 불필요한 집단 쿼리(n + 1)가 발생해 성능에 악영향을 줄 가능성이 매우 높다. 나 또한 이런 문제를 격었기 때문에 그 경험을 공유하려한다.
해당 포스트가 N+1문제 해결을 설명하려는 글은 아니며, 연관된 문제에서 조금 더 유연한 대처를 하는 방법을 소개하려한다.
내가 만난 문제는 고객, 판매자(Account) 테이블과 결제(Payment) 테이블 2가지가 존재했다.
이런 환경에서 판매자가 가지고있는 결제들을 불러오고 결제한 구매자(고객)의 데이터를 함께 List로 return해주는 문제를 해결해야 했다.
처음 작성한 코드는 다음과 같다.
//@Query("select a from Account a where join fetch a.paymentList a.idfAccount = :idfAccount")
Account seller = accountRepository.findById(idfAccount);
List<Payment> paymentList = seller.paymentList;
List<PaymentDto> paymentDtoList = paymentList.stream().map(payment -> {
AccountDto accountDto = new AccountDto();
BeanUtils.copyProperties(payment.getAccount(), accountDto);
return PaymentDto.builder()
.accountDto(accountDto)
...중략...
}).collect(Collectors.toList());
seller 고객 객체를 로딩할 때 fetch를 이용해 paymentList를 함께 DB에서 가져왔다. 그후 대한 List을 이용해 PaymentDto를 생성하여 최종적으로 출력해주는 코드이다.
하지만 위 코드처럼 작성하게 된다면 iterator내에서 account를 Proxy에서 가져올 수 없다며 에러를 뱉어낸다.
이는 payment의 account의 fetch 전략을 LAZY로 생성하기 때문인데 이를 EAGER로 변경해주면 정상 출력된다.
하지만 엄청난 성능 손해가 발생한다. 이유는 모든 iterator에서 고객 정보를 DB에서 SELECT하기 때문이다.
그럼 seller를 findById할때 payment의 account도 join하면 되는것 아닌가? 라고 생각할 수도 있다. 하지만 그 방법은 불가능한데 이유는 JPA에서 JPQL은 Collection탐색이 불가하다.
그럼 이 문제를 해결하려면 어떻게 해야할까? 방법은 간단하다.
seller에서 PaymentList를 참조하여 가져오는 것이 아닌 Payment에 직접 데이터를 요청하면 된다. (설명이 메롱한데 코드를 보자)
Account seller = accountRepository.findById(idfAccount);
//@Query("select p from Payment p join fetch p.account where p.seller = :seller")
List<Payment> paymentList = paymentRepository.findAllBySeller(seller);
List<PaymentDto> paymentDtoList = paymentList.stream().map(payment -> {
AccountDto accountDto = new AccountDto();
BeanUtils.copyProperties(payment.getAccount(), accountDto);
return PaymentDto.builder()
.accountDto(accountDto)
...중략...
}).collect(Collectors.toList());
이전 코드와 비교했을 때 1줄이 변경됐는데 기존 paymentList를 seller에서 가져오는게 아닌 paymentRepository에서 find하는 방법이다.
이렇게 구현하게 된다면 총 2번의 쿼리로 우리가 원하는 모든 데이터를 가져올 수 있다.
findBySeller를 호출할때 account(고객)을 fetch조인하기 때문에 반복문에서 payment마다 쿼리를 수행하는 것이 아닌 하나의 쿼리로 모든 payment + account(고객) 데이터를 join하여 불러오기 때문이다.
첫 번째 방법으로 구현했을 때 한번 요청에 2~300ms가 넘던 응답 시간도 100ms이하로 개선이 되었다.
JPA를 쓰다보면 객체지향적 패러다임에 빠져 시야가 가려질 수 있는데 상황에 따라 유연한 대처가 필요하다는 것을 확실히 느끼게 되는 계기였다.
'개발 > BACKEND' 카테고리의 다른 글
QueryDSL을 도입한 이유에 대해 (0) | 2022.05.20 |
---|---|
[JPA] Custom Repository 구현 (0) | 2022.05.08 |
[Spring Boot] Security ProviderNotFoundException에러 해결 (0) | 2022.03.19 |
자바 ORM 표준 JPA(Hibernate)에 대해 (0) | 2022.03.05 |
[간단] Lombok @SuperBuilder 에 대해서 (0) | 2022.03.05 |
- Total
- Today
- Yesterday
- 인디게임
- mobx
- Unity3D
- 유니티
- 턴드림
- JPQL
- 사이드프로젝트
- 신작
- 우주게임
- 인디
- 개발
- 이명규
- 게임 개발
- studio108
- Java
- 모험
- spring boot
- frontend
- 개발일지
- JIRA
- 게임
- 보따리장사
- 튜토리얼
- QueryDSL
- Lombok
- 용사
- spring
- 스크럼
- JPA
- 게임개발
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |