팀원 : PM(1) / Design(1) / Frontend(2) / Backend(3)
기간 : 2024.03 ~ 2025.03
링크 : https://github.com/M-ung/MoodBuddy_Server
서비스 내용 : 사용자가 작성한 일기를 바탕으로 감정 분석하는 웹 서비스
소통 : GitHub, Slack, Notion, Discord
개선 전 -> Spring Batch를 활용해 QuddyTI 분석 (매달 말일 + 매달 첫날)
하지만 사용자가 한 달간 작성한 일기의 감정과 주제를 분석하는 과정에서 유저 한 명 당 11번의 쿼리가 발생한다는 걸 알았다.
그 이유는 기존에 사용자마다 각 감정, 각 주제 별로 한 번씩 조회해 N+1 문제가 발생했다.
📍 DiaryCountServiceImpl.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class DiaryCountServiceImpl implements DiaryCountService {
private final QuddyTIBatchJDBCRepository quddyTIBatchJDBCRepository;
@Override
public Map<DiaryEmotion, Long> getEmotionCountsByDate(final Long userId, LocalDate[] dates) {
EnumMap<DiaryEmotion, Long> emotionCounts = new EnumMap<>(DiaryEmotion.class);
for (DiaryEmotion emotion : DiaryEmotion.values()) {
long count = quddyTIBatchJDBCRepository.findEmotionCountsByUserIdAndDate(userId, emotion, dates[0], dates[1]);
emotionCounts.put(emotion, count);
}
return emotionCounts;
}
@Override
public Map<DiarySubject, Long> getSubjectCountsByDate(final Long userId, LocalDate[] dates) {
EnumMap<DiarySubject, Long> subjectCounts = new EnumMap<>(DiarySubject.class);
for (DiarySubject subject : DiarySubject.values()) {
long count = quddyTIBatchJDBCRepository.findSubjectCountsByUserIdAndDate(userId, subject, dates[0], dates[1]);
subjectCounts.put(subject, count);
}
return subjectCounts;
}
}
📍 QuddyTIBatchJDBCRepository.java
@Repository
@RequiredArgsConstructor
public class QuddyTIBatchJDBCRepository {
private final JdbcTemplate jdbcTemplate;
public long findEmotionCountsByUserIdAndDate(Long userId, DiaryEmotion emotion, LocalDate start, LocalDate end) {
String sql = "SELECT COUNT(*) FROM diary WHERE user_id = ? AND date BETWEEN ? AND ? AND emotion = ?";
return jdbcTemplate.queryForObject(sql, Long.class, userId, start, end, emotion.name());
}
public long findSubjectCountsByUserIdAndDate(Long userId, DiarySubject subject, LocalDate start, LocalDate end) {
String sql = "SELECT COUNT(*) FROM diary WHERE user_id = ? AND date BETWEEN ? AND ? AND subject = ?";
return jdbcTemplate.queryForObject(sql, Long.class, userId, start, end, subject.name());
}
}
만약 10,000명의 사용자라면, 10,000 * 11 = 110,000번 쿼리가 발생한다.
그리고 사용자 10,000명 기준, 쿼디티아이 생성 평균 약 919ms, 수정 평균 약 14,528ms 발생했다.
📍 DiaryCountServiceImpl.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class DiaryCountServiceImpl implements DiaryCountService {
private final QuddyTIBatchJDBCRepository quddyTIBatchJDBCRepository;
@Override
public Map<DiaryEmotion, Long> getEmotionCountsByDate(Long userId, LocalDate[] dates) {
return quddyTIBatchJDBCRepository.findEmotionGroupCountsByUserIdAndDate(userId, dates[0], dates[1]);
}
@Override
public Map<DiarySubject, Long> getSubjectCountsByDate(Long userId, LocalDate[] dates) {
return quddyTIBatchJDBCRepository.findSubjectGroupCountsByUserIdAndDate(userId, dates[0], dates[1]);
}
}
📍 QuddyTIBatchJDBCRepository.java
@Repository
@RequiredArgsConstructor
public class QuddyTIBatchJDBCRepository {
private final JdbcTemplate jdbcTemplate;
public Map<DiaryEmotion, Long> findEmotionGroupCountsByUserIdAndDate(Long userId, LocalDate start, LocalDate end) {
String sql = "SELECT emotion, COUNT(*) FROM diary WHERE user_id = ? AND date BETWEEN ? AND ? GROUP BY emotion";
return jdbcTemplate.query(sql, rs -> {
EnumMap<DiaryEmotion, Long> map = new EnumMap<>(DiaryEmotion.class);
while (rs.next()) {
String emotionStr = rs.getString("emotion");
long count = rs.getLong("COUNT(*)");
map.put(DiaryEmotion.valueOf(emotionStr), count);
}
for (DiaryEmotion e : DiaryEmotion.values()) {
map.putIfAbsent(e, 0L);
}
return map;
}, userId, start, end);
}
public Map<DiarySubject, Long> findSubjectGroupCountsByUserIdAndDate(Long userId, LocalDate start, LocalDate end) {
String sql = "SELECT subject, COUNT(*) FROM diary WHERE user_id = ? AND date BETWEEN ? AND ? GROUP BY subject";
return jdbcTemplate.query(sql, rs -> {
EnumMap<DiarySubject, Long> map = new EnumMap<>(DiarySubject.class);
while (rs.next()) {
String subjectStr = rs.getString("subject");
long count = rs.getLong("COUNT(*)");
map.put(DiarySubject.valueOf(subjectStr), count);
}
for (DiarySubject s : DiarySubject.values()) {
map.putIfAbsent(s, 0L);
}
return map;
}, userId, start, end);
}
}
사용자의 한 달간의 일기 감정+주제를 모두 가져온 후 Map을 활용해 분리하는 방법으로 변경했다.
개선 결과는 아래와 같다.
1. 사용자 10,000명 기준, 감정+주제 통계 조회 쿼리를 110,000 → 20,000건으로 최적화.
2. 감정+주제 통계를 루프 기반 N+1 쿼리에서 GROUP BY 단일 쿼리로 개선, 쿼리 수 90,000건 절감
3. 사용자 10,000명 기준, 쿼디티아이 수정 작업을 평균 약 14,528ms → 평균 약 4,003ms로 최적화하여 약 72% 성능 개선.
[[🔥TroubleShooting - MoodBuddy🔥] 너의 쿼디티아이는 뭐니?!]
https://velog.io/@_mung/TroubleShooting-MoodBuddy-%EB%84%88%EC%9D%98-%EC%BF%BC%EB%94%94%ED%8B%B0%EC%95%84%EC%9D%B4%EB%8A%94-%EB%AD%90%EB%8B%88