기존 로직
•
레거시 코드에선 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 문법 참고