전체 보기
🟢

QueryDsl로 쿼리 최적화 (6초 → 0.5초 / 성능 12배 향상)

작성일자
2023/12/22
태그
SPRING
프로젝트
FIS
책 종류
1 more property

기존 로직

레거시 코드에선 user 리스트를 찾아오고, 리스트를 순회하며 각 user의 call 기록을 찾아온다.
Service 레이어 - 레거시
public List<CallHistoryResponse> findUserAndCallByDate(String date) { List<CallHistoryResponse> response = new ArrayList<>(); List<FisUser> users = customizedFisUserRepository.findAll(); for (FisUser user : users) { addCallHistory(date, response, user); } return response; } private void addCallHistory(String date, List<CallHistoryResponse> response, FisUser user) { List<Call> calls = fisUserRepository.findUserAndCallByDate(date, user.getId()); int total = calls.size(); int p = 0; int r = 0; int h = 0; int n = 0; int a = 0; for (Call call : calls) { switch (call.getParticipation()) { case PARTICIPATION: p++; break; case REJECT: r++; break; case HOLD: h++; break; case NONE: n++; break; case ABSENCE: a++; break; case COMPLETED, EMPTY: break; // TODO: 구현 필요 default: throw new IllegalStateException("Unexpected value: " + call.getParticipation()); } } response.add(new CallHistoryResponse(user.getId(), user.getLoginId(), user.getName(), user.getAuth(), total, p, r, h, n, a)); }
Java
복사
Repository 레이어 - 레거시
public interface CallRepository extends JpaRepository<Call, Long>, CustomizedCallRepository { List<Call> findAllByDateAndUserId(String date, Long userId); }
Java
복사
레포지토리 레이어(다른 예시)
레거시 코드에선 데이터 200개 찾아오는 데, 5~6초 가량 소요된다.
리스트 순회를 통해 사용자마다 데이터베이스 쿼리를 각각 날리기 때문이다.

변경한 로직 (6초 → 0.5초 / 성능 12배 향상)

쿼리를 한 번에 날리게 해줬다.
일단 sql문부터 짜보았다.
SELECT u.user_id, u.u_nickname, u.u_name, u.u_auth, COUNT(*) AS total_calls, SUM(CASE WHEN c.participation = 'PARTICIPATION' THEN 1 ELSE 0 END) AS p, SUM(CASE WHEN c.participation = 'REJECT' THEN 1 ELSE 0 END) AS r, SUM(CASE WHEN c.participation = 'HOLD' THEN 1 ELSE 0 END) AS h, SUM(CASE WHEN c.participation = 'NONE' THEN 1 ELSE 0 END) AS n, SUM(CASE WHEN c.participation = 'ABSENCE' THEN 1 ELSE 0 END) AS a FROM fis_user u LEFT JOIN calls c ON c.user_id = u.user_id AND c.date = "2023-10-23" GROUP BY u.user_id;
SQL
복사
서비스 레이어와 레포지토리 레이어를 리팩토링했다.
Service 레이어 - 리팩토링
public List<CallHistoryResponse> findUserAndCallByDate(String date) { return fisUserRepository.findCallHistoryByDate(date); }
Java
복사
Repository 레이어 - 리팩토링
@RequiredArgsConstructor public class CustomizedFisUserRepositoryImpl implements CustomizedFisUserRepository { private final JPAQueryFactory queryFactory; @Override public List<CallHistoryResponse> findCallHistoryByDate(String date) { return queryFactory .select(Projections.constructor(CallHistoryResponse.class, fisUser.id, fisUser.loginId, fisUser.name, fisUser.auth, call.count().intValue(), new CaseBuilder() .when(call.participation.eq(Participation.PARTICIPATION)).then(1) .otherwise(0).sum(), new CaseBuilder() .when(call.participation.eq(Participation.REJECT)).then(1) .otherwise(0).sum(), new CaseBuilder() .when(call.participation.eq(Participation.HOLD)).then(1) .otherwise(0).sum(), new CaseBuilder() .when(call.participation.eq(Participation.NONE)).then(1) .otherwise(0).sum(), new CaseBuilder() .when(call.participation.eq(Participation.ABSENCE)).then(1) .otherwise(0).sum() )) .from(fisUser) .leftJoin(call).on(call.user.id.eq(fisUser.id).and(call.date.eq(date))) .groupBy(fisUser.id) .fetch(); } }
Java
복사
고작 200개 데이터로 테스트 했는데도 무려 10~12배 정도나 빨라졌다. 물론, 로컬에서 돌린 거라 실행 환경이나 데이터 양에 따라 차이는 있겠지만 말이다.

변경하다가 발견한 재밌는 쿼리

아래 두 쿼리는 결과가 다를까? 같을까? 정답을 말하자면, 다르다.
(1)
SELECT ... FROM fis_user u LEFT JOIN call c ON c.user_id = u.id WHERE c.date = :date
SQL
복사
(2)
SELECT ... FROM fis_user u LEFT JOIN call c ON c.user_id = u.id AND c.date = :date
SQL
복사
(1)번 코드는 call이 존재하지 않는 fisUser들은 결과에 뜨지 않고,
(2)번 코드는 call이 존재하지 않는 fisUser들도 결과에 뜬다.
(참고로 나는 2번 결과가 필요했다)
왜 그런걸까?
(1)번 코드는 left join절에서 모든 fisUser를 가진 결과를 만든다. 해당하는 call이 있으면 이를 포함하고 말이다. 하지만, where절에 의해서 date가 일치하는 call이 없는 fisUser들은 모두 결과에서 제외된다.
(2)번 코드는 조건이 조인을 수행할 때 적용되어 다른 결과를 만든다. 즉, fisUser와 call이 조인되고 date가 일치하는지 확인하는데 이때, date가 일치하는 call이 없는 fisUser들은 call 관련 열들이 null로 채워지게 된다. 그 결과 call이 없는 fisUser들도 결과에 포함된다.
On 절의 조건은 데이터 결합을 위한 조건이고, Where 절의 조건은 데이터 필터링을 위한 조건이기 때문인데,
원리를 더 깊게 알고 싶다면, On 절과 Where 절의 차이에 대해 검색해보자.
QueryDsl 문법 참고