1. 배경
일간 활성 사용자(DAU)와 월간 활성 사용자(MAU)를 수집해야 하는 요구사항이 있었다.
문제는 시스템이 오래되었고, 로그인 기록 테이블과 사용자 테이블 간의 키가 정규화되어 있지 않았다.
로그인 기록 테이블(login_activity)의 member_id와 사용자 테이블(member)의 email을 매칭해야 했다.
즉, 인덱스로 보장되지 않는 조건으로 조인을 수행해야 했다.
2. 기존 쿼리의 문제
활성 사용자 통계는 단일 테이블로는 수치가 정확하지 않았다.
탈퇴 사용자나 휴면 계정까지 포함될 수 있기 때문이다.
-- 부정확한 DAU 예시
SELECT COUNT(DISTINCT member_id)
FROM login_activity
WHERE login_date = '2025-06-17';
따라서 사용자 상태(is_active, is_deleted)를 반영하려면 반드시 사용자 테이블과 조인이 필요했다.
그러나 member_id와 email은 FK로 관리되고 있지 않아 조인 성능은 매우 떨어졌다.
3. 해결 방향
기존 아키텍처를 변경하는 것은 불가능했기에, 다음 세 가지를 최우선 과제로 삼았다.
- 정확한 통계 수치를 확보하는 것
- 외래키(FK) 구조 개선 계획은 별도로 상사와 논의하여 추진할 것
- 서비스에 미치는 영향을 최소화하는 것
추가로, 정상적인 통계 조회에 실패할 경우를 대비해 Fallback 처리를 적용했다.
조회 실패 시 통계 수치는 0으로 대체하여, 서비스 장애가 발생하지 않도록 설계했다.
4. 최종 쿼리와 로직 (예시)
조인 쿼리 (예시)
-- DAU: 조인 기반 정확 통계
SELECT COUNT(DISTINCT L.member_id)
FROM login_activity L
JOIN member M ON L.member_id = M.email
WHERE DATE(L.login_date) = #{targetDate}
AND M.is_deleted = 0
AND M.is_active = 1;
서비스 로직 (예시)
private Integer getDailyActiveUsers(LocalDate targetDate) {
try {
Integer count = memberStatisticsMapper.countDailyActiveUsers(targetDate);
return count != null ? count : 0;
} catch (Exception e) {
log.warn("DAU fallback: {}", e.getMessage());
return 0;
}
}
5. 결과
- 데이터 정확성 확보
탈퇴 및 비활성 계정을 제외한 정확한 DAU/MAU 집계가 가능해졌다. - 성능 이슈 감내
인덱스를 타지 않는 문제는 구조적 한계로 남았다.
트래픽 규모를 고려할 때 허용 가능한 수준으로 판단했다. - Fallback 안전성
쿼리가 실패하더라도 서비스 오류가 발생하지 않도록 보호 장치를 마련했다.
6. 회고
레거시 시스템에서 데이터 설계가 완벽하지 않은 경우가 꽤 많다.
이번 사례를 겪으면서 조인을 완전히 피할 수 없을 때 어떻게 절충할지 배울 수 있었다.
앞으로는 이런 부분들을 개선해야 할 필요가 있다.
- login_activity.member_id를 FK로 정비하는 작업
- DATE() 함수 대신 범위 조건을 활용해 인덱스 효과 극대화
- Redis 같은 캐시 도입으로 쿼리 부하 분산
이런 작업들이 진행되면 훨씬 더 안정적이고 효율적인 통계 서비스가 가능해질 것이다.
'SPRING BOOT' 카테고리의 다른 글
[개발 기록] MyBatis에서 복호화 컬럼이 VO에 매핑되지 않는 원인과 해결 (0) | 2025.06.20 |
---|---|
SpringBoot 프로젝트에서 JPA 어노테이션 에러 해결방법(jakarta.persistence.*;) (0) | 2025.02.22 |
Spring Boot와 React를 함께 사용하는 방법 (JSP와 React 병행) (0) | 2025.01.23 |
SpringBoot에서 API KEY 설정하기 (0) | 2024.11.02 |