Skip to content

refactor: 지출 도메인 리팩토링#53

Merged
pbk2312 merged 16 commits into
devfrom
feat/50-expense-refactor-3
May 21, 2025
Merged

refactor: 지출 도메인 리팩토링#53
pbk2312 merged 16 commits into
devfrom
feat/50-expense-refactor-3

Conversation

@pbk2312

@pbk2312 pbk2312 commented May 19, 2025

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈 번호

📝작업 내용

1. 지출 내역 리스트 조회 시 문제점 파악

Virtual Users (VU): 최대 50명  
RPS(Requests/sec): 평균 732, 최대 1,360  
응답 시간 (http_req_duration):
  - 평균: 55.9 ms  
  - p90: 168.7 ms  
  - p95: 214.2 ms  
  - 최대: 630.8 ms  
  • DB 직접 조회로 인한 응답 지연
  • 모든 요청마다 RDB를 조회하면서 평균 응답시간 55ms, p95 기준 최대 214ms까지 증가

캐시처리 X

  • DB 커넥션 부족 현상 발생
    -> HikariCP 커넥션 풀의 기본 설정(10개)을 초과해 최대 37개 요청이 대기
    -> 캐시가 있었다면 대기 없이 응답 가능했을 부분에서 병목 발생
  • 비효율적인 쿼리 구성
    -> LEFT JOIN, 페이징 등으로 쿼리 자체의 비용도 높음

캐시 처리 X 디비 부하

2. 개선

  • 불필요한 JOIN 제거
    • LEFT JOIN team 삭제 → expense.team_id 로 바로 필터링
  • DTO 프로젝션
    • 필요한 컬럼만 조회
  • 페이징용 카운트 쿼리 분리
  • 캐시 적용
// 조회: 캐시 적용
@Cacheable(
  value = "recentExpenses",
  key = "'team:' + #teamId + ':page:' + #pageable.pageNumber + ':size:' + #pageable.pageSize"
)
public PageResponse<ExpenseResponse> getListExpense(Long teamId, Pageable pageable) { … }

// 생성·수정·삭제: 캐시 무효화
public CreateExpenseResponse saveExpense(...) {evictRecentExpensesForTeam(teamId)}
public CreateExpenseResponse updateExpense(...) {evictRecentExpensesForTeam(teamId)}
public ExpenseBalanceResponse deleteExpense(...) {evictRecentExpensesForTeam(teamId)}
 public void evictByPrefix(String cacheName, String prefix) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      log.warn("캐시 '{}'를 찾을 수 없습니다. 무효화 작업을 건너뜁니다.", cacheName);
      return;
    }

    if (cache instanceof CaffeineCache caffeineCache) {
      com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
          caffeineCache.getNativeCache();

      List<String> keysToRemove = nativeCache.asMap().keySet().stream()
          .map(Object::toString)
          .filter(key -> key.startsWith(prefix))
          .toList();
      keysToRemove.forEach(nativeCache::invalidate);

      log.info("Caffeine 캐시 '{}'에서 '{}'로 시작하는 {}건 무효화 완료.",
          cacheName, prefix, keysToRemove.size());
      return;
    }

    cache.clear();
    log.info("Caffeine 외 캐시 구현체라 전체 clear() 호출 – 네임스페이스='{}'", cacheName);
}
  • @CacheEvict로는 특정 키 하나만 지우거나, allEntries=true로 전체를 날리는 것만 가능

    • Caffeine 네이티브 API 활용: 내부 캐시 맵의 키들을 순회하며, 문자열이 지정한 접두사(prefix) 로 시작하는 항목만 골라 invalidate()
    • Caffeine 네이티브 API로 키 목록을 순회하면서 "team:{teamId}:" 로 시작하는 항목만 골라 한 번에 invalidate하기 때문에 해당 팀의 모든 페이지 캐시만 삭제, 다른 팀 캐시는 그대로 유지
  • 캐시 적용 후 성능 (k6 부하 테스트)

스크린샷 2025-05-19 오후 4 50 02

Virtual Users (VU): 최대 50명  
RPS(Requests/sec): 평균 2,000, 최대 2,840  
응답 시간 (http_req_duration):
  - 평균: 20.0 ms  
  - p90: 60.0 ms  
  - p95: 89.8 ms  
  - 최대: 565.5 ms  

결과

  • 응답 시간 대폭 단축 (평균 55.9→20.0 ms, p95 214.2→89.8 ms)

  • RPS 증가 (732→2,000)

  • 커넥션 풀 대기 해소 (Pending Threads 감소)

🧪 테스트 여부

  • 테스트 코드를 작성함
  • 테스트를 수행함

💬리뷰 요구사항

  • 지출 내역 리스트는 조회가 빈번히 일어나므로 캐시 처리를 해보았는데 어떻게 생각하시나요? 괜찮게 적용된건가요?
  • 예산 수정을 할 때 낙관적 락을 걸려고 했는데 우선 동시성 문제가 발생하는지 테스트 후 -> 개선을 해볼려했는데 아직 개념이 부족해서 시간이 여유가 되면 시도 해봐야할 것 같습니다. -> Budget에 락을 걸면 Team도 락이 걸려서 골치아파서 조금 봐야할거 같습니다.
  • 개선할게 너무 많네요...ㅠㅠ

@johnhuh619 johnhuh619 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다. 이번 리팩토링도 많이 배워갑니다.

@mipangg mipangg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다

@pbk2312 pbk2312 merged commit 27f98f4 into dev May 21, 2025
@mipangg mipangg deleted the feat/50-expense-refactor-3 branch May 21, 2025 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants