✅ 1단계: Consumer 단일 스레드로 제한

기본 전제: 정합성 최우선이라면 동시에 여러 consumer thread가 처리하지 않도록 concurrency = 1

 

  • 💡 이렇게 하면 Consumer는 Kafka Partition 단위로 순서대로 처리하므로 race condition 없이 안전.
  • 단점: 처리 속도가 느릴 수 있음.

테스트 

✅ 2단계: Partition 전략 활용한 멀티 Consumer 처리 

처리량 늘리면서도 정합성 지키려면?

Kafka는 기본적으로 같은 key는 같은 Partition에 들어감
→ 그러니 senderUserId를 key로 설정해 메시지를 Kafka에 보낼 때 지정해줘:

kafka 메시지 생성할 때

 

이렇게 하면:

  • 같은 유저 ID끼리는 순서대로 처리 (→ 락 충돌 없음)
  • 다른 유저들끼리는 동시에 송금 처리 가능 (→ 성능 up)

테스트

🛠 해결 방법: 락 순서 고정 (Lock Ordering)


🔍 문제 해결 방향

서로 다른 유저 간 교차 송금(A→B, B→A)이 동시에 발생할 경우,
각 쓰레드가 락을 거는 순서가 엇갈리면 데드락이 발생할 수 있어.

예시

  • 쓰레드 1: A → B 송금 → A 계좌 락 → B 계좌 락
  • 쓰레드 2: B → A 송금 → B 계좌 락 → A 계좌 락
    → 서로 락을 점유한 상태에서 상대 락을 기다리다가 영원히 대기 = 💥 데드락 발생

그래서 우리는 항상 락을 거는 순서를 고정했어.
userId가 낮은 계좌 → 높은 계좌 순으로 락을 획득하도록 해서
락 순서가 일관되도록 보장했어.


🔒 락 순서 고정 흐름

  1. 두 유저 ID를 비교해서 오름차순 정렬
  2. SELECT ... FOR UPDATE로 낮은 ID 계좌부터 락을 건다
  3. 나머지 계좌에 대해 동일하게 락을 건다
  4. 트랜잭션 내에서 안전하게 포인트 이체 수행
 

이렇게 락 순서를 고정하면,
교차 송금이 발생하더라도 쓰레드들이 항상 동일한 순서로 락을 잡기 때문에
데드락 없이 안전하게 처리할 수 있어.


💡 도입 후 개선점


 

항목 Before After
락 획득 순서 쓰레드마다 달라 데드락 가능성 있음 항상 고정된 순서로 락 획득
데드락 발생 가능성 높음 제로
교차 송금 안정성 불안정 안전하게 처리됨

👀 어디에 쓰이냐?

  • A ↔ B 교차 송금
  • 사용자 간 시간 기반 포인트 교환 (타임뱅크 스타일)
  • 재고 차감 시 주문자가 다를 때
  • 동일 리소스를 다수의 요청이 동시에 업데이트할 때

🛠 해결 방법: 비관적 락(Pessimistic Lock) 도입

 

비관적 락 도입

🔍 문제 해결 방향

  1. 트랜잭션 (@Transactional) 으로 송금 단위 작업을 하나의 논리적 처리 단위로 묶었고
  2. SELECT ... FOR UPDATE (비관적 락) 을 통해
    같은 송신자 계좌에 여러 쓰레드가 동시에 접근할 때 락을 걸어서 충돌을 방지했어.
  3. 테스트에서도 10개의 송금 요청을 동시에 날렸을 때도
    • 데이터 정합성이 깨지지 않았고
    • 데드락도 안 생기고
    • 송신자 잔고: 500 / 수신자 잔고: 500 으로 정확하게 처리됐어.

🔒 비관적 락 적용 흐름

    1. @Lock(LockModeType.PESSIMISTIC_WRITE)를 통해 송신자, 수신자 계좌 row에 락을 건다.
    2. 트랜잭션 내에서 포인트 차감 및 적립을 수행한다.
    3. 트랜잭션이 끝나면 락이 해제된다.

💡 도입 후 개선점

 

항목 Before After
데이터 정합성 충돌 및 데드락 발생 안정적 처리
에러 발생률 Deadlock, NoSuchElementException 없음
동시성 처리 전략 미비 명시적 비관적 락 도입

👀 그래서 이건 어디에 쓰이냐?

  • 포인트 송금 / 은행 계좌 잔고 / 재고 차감 / 쿠폰 사용 등
  • “하나만 있어야 하는 자원”을 실시간으로 정확하게 처리해야 할 때 매우 중요해

 

📌 추후 계획

  • 트래픽 증가 시 낙관적 락 + 재시도로 전환 고려
  • 이벤트 기반 처리 도입: Kafka + Redis Lock 조합 설계
  • 조회 성능 향상을 위한 CQRS 구조 분리

💥 마주친 이슈

  • 목표: 여러 사용자가 동시에 포인트를 송금할 때 데이터 정합성을 유지하는 로직을 구현하고, 데드락 발생 여부를 확인

🧪 수행한 테스트

  • 테스트 시나리오
    • 하나의 송신 계좌(sender)에서 수신 계좌(receiver)로 50 포인트씩 송금
    • 총 10개의 쓰레드가 동시에 송금 요청을 실행
    • 각 쓰레드는 PointService.transferPoints() 메서드를 호출
  • 핵심 테스트 코드

  • 테스트 로그
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-9] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.166+09:00 ERROR 15836 --- [point-service] [pool-2-thread-9] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.166+09:00 ERROR 15836 --- [point-service] [pool-2-thread-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-3] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.167+09:00  WARN 15836 --- [point-service] [pool-2-thread-7] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.167+09:00 ERROR 15836 --- [point-service] [pool-2-thread-3] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [ool-2-thread-10] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.167+09:00 ERROR 15836 --- [point-service] [ool-2-thread-10] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.167+09:00 ERROR 15836 --- [point-service] [pool-2-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.166+09:00  WARN 15836 --- [point-service] [pool-2-thread-8] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2025-04-05T16:38:42.168+09:00 ERROR 15836 --- [point-service] [pool-2-thread-8] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.167+09:00 ERROR 15836 --- [point-service] [pool-2-thread-7] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.167+09:00 ERROR 15836 --- [point-service] [pool-2-thread-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2025-04-05T16:38:42.167+09:00 ERROR 15836 --- [point-service] [pool-2-thread-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
에러 발생: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update point_accounts set total_points=?,user_id=? where account_id=?]; SQL [update point_accounts set total_points=?,user_id=? where account_id=?]
Sender 잔액: 950
Receiver 잔액: 50
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-04-05T16:38:42.229+09:00  INFO 15836 --- [point-service] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-04-05T16:38:42.232+09:00  INFO 15836 --- [point-service] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2025-04-05T16:38:42.306+09:00  INFO 15836 --- [point-service] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
> Task :test
BUILD SUCCESSFUL in 6s
4 actionable tasks: 1 executed, 3 up-to-date
오후 4:38:42: Execution finished ':test --tests "com.timebank.pointservice.service.PointServiceTest.동시성_테스트_송금_충돌"'.

🚨 발생한 문제: 데드락(Deadlock)

  • 에러 로그 요약:
  • pgsql
    복사편집
    SQL Error: 1213, SQLState: 40001 Deadlock found when trying to get lock; try restarting transaction
  • 원인 분석:
    • 여러 트랜잭션이 동시에 동일한 송신 계좌 row에 대해 update를 시도하며 교착 상태 발생
    • JPA가 자동으로 처리하는 SQL update 쿼리에서 락 충돌 발생

🧩 해결 접근 방식

  • 문제 인식:
    • 단순한 동시 호출이 아니라 동일 자원에 대한 경쟁 접근이 원인임을 파악
  • 대응 방법:

 

✅ 1. 비관적 락 + 트랜잭션 처리 (서비스 내부)

💡 어떤 상황에 유리해?

  • 동일한 데이터를 동시에 수정하려는 경우 (ex. 포인트 차감, 잔고 감소 등)
  • 실시간 정합성이 아주 중요한 경우

💪 장점:

  • 충돌 즉시 차단 (락을 걸어서 다른 트랜잭션 접근 막음)
  • 데이터 정합성 매우 높음

⚠️ 주의점:

  • 동시에 들어오는 트래픽이 많으면 락 대기 시간이 늘어나 성능 저하 우려 → 그래서 CQRS + 메시지 큐 조합과 같이 써줘야 함.

✅ 2. Redis 분산 락 or 메시지 큐 (서비스 간 처리)

💡 어떤 상황에 유리해?

  • 여러 서비스가 동시에 동일 리소스를 처리할 수 있어서 순서 보장, 중복 방지가 필요할 때
  • MSA 환경에서 비동기 송수신할 때 (예: Kafka 이벤트 소비)

💪 장점:

  • 메시지 큐(Kafka 등)는 순서 보장, 재시도, 장애 대응 가능
  • Redis 분산 락은 간단한 순차 제어용으로 빠르고 유용

⚠️ 주의점:

  • Redis 락은 네트워크 장애/만료 등 엣지 케이스 주의
  • Kafka 등 메시지 큐는 적절한 consumer 처리 속도오프셋 관리 필요

✅ 3. 조회는 CQRS로 분리 (Command/Query 분리)

💡 어떤 상황에 유리해?

  • 트래픽의 대부분이 조회 중심일 때
  • 정합성보다 응답 속도가 중요한 화면 (예: 마이페이지, 대시보드 등)

💪 장점:

  • 조회 쿼리를 별도 DB/Redis 캐시로 분리하면 메인 트랜잭션에 부담 X
  • 읽기 전용 DBRead Replication 사용하면 확장성도 👍

💡 결론: 이 구조는 대규모 서비스에서 실제로 많이 쓰이는 패턴

  • 카카오, 배민, 토스, 쿠팡 등도 이런 방식으로 내부 정합성 + 외부 이벤트 처리 + 조회 최적화를 조합해서 사용해.
  • 지금처럼 MSA + Kafka + DB 기반 구조에서는 이 셋의 조합이 현실적인 베스트 프랙티스라고 보면 돼.

✅ RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례


1. 기술 도입 배경

기존 시스템은 주문 생성 API를 시작으로, 배달 생성 → 배달 경로 계산 → 슬랙 메시지 전송까지 총 4단계의 API 호출이 순차적으로 진행되는 구조였다.
JMeter를 활용한 성능 테스트 결과, 동기식 처리 흐름에서 평균 처리량은 약 26건/초(26 TPS) 수준으로 확인되었다.

그러나 트래픽이 증가하는 상황에서 각 API 호출이 순차적으로 진행됨에 따라 병목현상이 발생했고, 시스템 전체의 확장성에 한계가 있었다.
이에 따라 각 단계를 비동기화하여 병렬로 처리하고, **메시지 지향 미들웨어(RabbitMQ)**를 도입해 안정적이고 효율적인 비즈니스 플로우를 구성하고자 했다.


2. RabbitMQ 적용 과정

✅ 아키텍처 변경 사항

  • 기존:
    사용자 → 주문생성 API → (동기 호출) 배달생성 → 경로계산 → 슬랙알림
  • 변경 후:
    사용자 → 주문생성 API → RabbitMQ로 메시지 전송
    컨슈머 1: 배달생성 → 메시지 발행
    컨슈머 2: 경로계산 → 메시지 발행
    컨슈머 3: 슬랙알림 전송

✅ 기술 적용 흐름

  • 주문생성 API에서 메시지를 order.queue에 발행
  • 메시지를 받은 컨슈머가 각 단계의 처리를 담당하며, 완료 시 다음 큐로 메시지 전달
  • 전체 흐름은 메시지 기반의 엄격한 순차 실행을 유지하면서도, 비동기 처리를 통해 API 간 종속 시간을 제거

✅ 테스트 환경 구성

  • JMeter로 TPS 100 이상 유입 테스트 진행
  • 처리 완료 기준은 order의 status가 배송완료로 변경되기 까지
  • 처리 완료 로그 또는 DB 기록 기반으로 최종 처리량 산정

3. 해결한 문제 및 성과

🎯 주요 성과

항목적용 전적용 후개선폭
평균 처리량 (Throughput) 26 TPS 87 TPS ▲ 234% 증가
병목 발생률 높음 (API 순차 지연) 매우 낮음 (비동기 큐 병렬 소비) 해소
API 대기 시간 순차적 누적 발생 컨슈머 기반 비동기 처리 감소

🧠 해결한 문제

  • 병목 현상 해소:
    비동기 구조로 API 간 대기시간 제거 → 시스템 응답 시간 대폭 개선
  • 확장성 확보:
    컨슈머 수 증가를 통해 자연스러운 수평 확장이 가능해짐
  • 유지보수 유리:
    각 처리 단계가 독립적인 서비스 단위로 분리되어, 향후 개별 개선 및 로깅, 모니터링이 용이

💡 추가 성능 개선 및 RabbitMQ 사용 이유

  • 확장성:
    트래픽 증가 시 컨슈머 인스턴스 수만 늘리면 대응 가능
  • 유연한 장애 대응:
    특정 단계 장애 발생 시 해당 큐만 지연되고, 전체 서비스는 영향 없음
  • 비동기 기반 구조 확장성:
    이후에도 재고확인, 결제처리, 배송추적 등 다양한 플로우에 유연하게 연결 가능
  • 비즈니스 로직 분리:
    서비스 간 강결합을 줄이고, 각 단계를 이벤트 중심으로 전환함으로써 **도메인 중심 아키텍처(Domain-driven architecture)**로의 전환 기반 확보RabbitMQ 적용 전 평균 처리량

  • RabbitMQ 적용 전 평균 처리량

 

  • RabbitMQ 적용 후 처리량

 

  • 메시징 아키텍쳐

 

캐싱을 활용한 최적 경로 조회 성능 개선

1. 기술 도입 배경

허브 간 최적 경로를 계산하는 API는 외부 API(hub-service) 호출과 복잡한 경로 계산 로직이 포함되어 있어 응답 시간이 길고 서버 부하가 컸음.
동일한 출발지와 도착지에 대해 반복 요청이 많은 상황에서, 중복 연산을 줄이기 위한 캐시 기반 성능 개선이 필요했음.

  • 복잡한 계산 로직으로 인한 응답 지연
  • 동일한 요청이 빈번하게 발생
  • 불필요한 DB/네트워크 트래픽 증가
    → 성능 최적화를 위해 Spring Cache 기반 캐싱 도입

2. 캐싱 적용 과정

✅ Spring Cache 적용

  • @Cacheable(value = "optimalRoutes", key = "#originHubId + ':' + #destinationHubId") 적용
  • origin과 destination 허브 ID 조합을 캐시 key로 설정
  • 동일 요청 시, 계산 없이 캐시된 결과 반환

✅ 트랜잭션과의 조합

  • @Transactional과 함께 사용하여 DB 접근 일관성 유지
  • 캐시는 메서드 진입 시점에 적용됨 (side effect 없음)

✅ 예외 처리 및 안전성 확보

  • 허브 정보가 없을 경우 예외 처리하여 캐시 오염 방지
  • 외부 API 실패 시에도 안정적으로 작동하도록 구성

3. 해결한 문제 및 성과

✅ 중복 계산 제거로 성능 향상

  • 평균 응답 시간: 66ms → 13ms로 단축
  • 약 80% 성능 개선

✅ 가독성 높은 캐시 로직

  • 비즈니스 로직에 영향을 주지 않는 방식으로 캐시 구현
  • 키 조합 방식으로 유연한 캐싱 가능

✅ 서버 부하 감소 및 안정성 향상

  • 트래픽 급증 시에도 캐시 덕분에 처리량 유지
  • 외부 시스템 부하 분산

✅ 향후 확장 고려

  • Redis로 캐시 스토리지를 변경해 분산 환경 대응 가능
  • TTL(Time To Live), 캐시 무효화 전략 도입 여지 확보

✅ 예외 처리

  • Hub 정보가 존재하지 않을 경우 RuntimeException 처리
  • 실패한 요청이 캐시에 저장되지 않도록 설계

4. 핵심 코드 예시

@Cacheable(value = "optimalRoutes", key = "#originHubId + ':' + #destinationHubId")
@Transactional
public List<Map<String, UUID>> findOptimalRoute(UUID originHubId, UUID destinationHubId) {
    var origin = hubService.getHubById(originHubId).getBody().getData();
    var destination = hubService.getHubById(destinationHubId).getBody().getData();
    if (origin == null || destination == null) {
        throw new RuntimeException("허브 정보를 찾을 수 없습니다.");
    }
    return hubRouteDomainService.findOptimalRoute(
        hubRouteRepository.findAll(), originHubId, destinationHubId
    );
}

 

QueryDSL을 활용한 동적 쿼리 최적화

1. 기술 도입 배경

기존 프로젝트에서는 JPA의 @Query 또는 Criteria API를 사용하여 복잡한 쿼리를 작성했으나,

  • 가독성이 떨어지고 유지보수가 어려웠음.
  • 동적 쿼리 작성 시 코드가 길어지고 복잡해짐.
  • 여러 필터 조건을 유연하게 적용하기 어려웠음.

이러한 문제를 해결하기 위해 QueryDSL을 도입하여 가독성과 유지보수성을 향상하고, 동적 쿼리를 효율적으로 작성하는 방식을 적용함.


2. QueryDSL 적용 과정

  1. QueryDSL 환경 설정
    • build.gradle에 QueryDSL 관련 의존성을 추가하고, JPAQueryFactory 빈을 설정하여 사용함.
  2. 동적 쿼리 구현
    • 검색 필터(예: 사용자 이름, 카테고리, 작성일 등)를 조합하여 동적으로 데이터를 조회하는 기능을 구현.
    • where() 절을 활용하여 조건이 있을 때만 쿼리에 추가하는 방식 적용.
  3. 페이징 및 성능 최적화
    • 페이징을 위한 count 조회는 fetchOne()을 사용하여 불필요한 데이터 조회를 최소화함.

3. 해결한 문제 및 성과

가독성 및 유지보수성 향상

  • 기존 @Query 기반의 복잡한 SQL을 QueryDSL로 변환하여 가독성을 개선.
    동적 필터링 구현
  • 검색 조건(이름, 날짜, 카테고리 등)을 조합하여 유연하게 검색할 수 있도록 개선.
    쿼리 성능 최적화
  • QueryDSL의 fetchJoin()을 활용하여 N+1 문제를 해결하고, 필요한 컬럼만 Projections.constructor()로 가져와 성능 개선.

 페이징 처리

  • offset()과 limit()을 활용하여 데이터 페이징 처리.
@Override
public Page<ReviewDetailResponseDto> findAllReviews(ReviewSearchCondition condition, Pageable pageable) {
    int pageSize = pageable.getPageSize() > 0 ? pageable.getPageSize() : 10;

    // 리뷰 목록 조회 쿼리 (fetchJoin을 통한 N+1문제 해결)
    List<ReviewDetailResponseDto> content = queryFactory
            .select(Projections.constructor(ReviewDetailResponseDto.class,
                    review.id,
                    order.id,
                    menu.id,
                    restaurant.id,
                    review.title,
                    review.content,
                    review.rating,
                    menu.name,
                    restaurant.name,
                    review.createdBy,
                    review.createdAt
            ))
            .from(review)
            .join(review.order, order).fetchJoin()
            .join(order.menu, menu).fetchJoin()
            .join(menu.restaurant, restaurant).fetchJoin()
            .where(
                    review.isDeleted.isFalse(),
                    keywordContains(condition.keyword()),
                    createdAtBetween(condition.startDate(), condition.endDate())
            )
            .orderBy(
                    condition.isAsc() ? review.createdAt.asc() : review.createdAt.desc(),
                    condition.isAsc() ? review.updatedAt.asc() : review.updatedAt.desc()
            )
            .offset(pageable.getOffset())
            .limit(pageSize)
            .fetch();

    // 총 레코드 수 조회 (fetchJoin 없이 실행하여 성능 최적화)
    Long total = queryFactory
            .select(review.count())
            .from(review)
            .join(review.order, order)
            .join(order.menu, menu)
            .join(menu.restaurant, restaurant)
            .where(
                    review.isDeleted.isFalse(),
                    keywordContains(condition.keyword()),
                    createdAtBetween(condition.startDate(), condition.endDate())
            )
            .fetchOne();

    return new PageImpl<>(content, pageable, total != null ? total : 0);
}

 

각각의 코드들(JPAQueryFactory, fetchjoin, projections.constructor , offset, fetchone 등 )의 사용이유들은 좀더 조사해서 추가 작성 필요.

'TIL' 카테고리의 다른 글

RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례  (0) 2025.04.02
캐싱을 통한 성능 개선  (0) 2025.04.01
RabbitMQ 와 Kafka  (0) 2025.03.07
DB인덱싱  (0) 2025.03.06
Cache  (0) 2025.03.05

RabbitMQ와 Kafka 모두 메시지 브로커이다. 하지만 목적과 동작 방식이 꽤 다르다.

쉽게 말하자면

  • Kafka : 대규모 데이터를 실시간 스트리밍
    • 실시간 로그 수집, 모니터링, 이벤트 스트리밍
    • 대규모 트래픽 처리
    • 인스타그램 피드, 넷플릭스 추천 시스템 등에 적합
    • 장기 저장을 목표
  • RabbitMQ : 큐에 담아 하나씩 처리
    • 이메일 인증, 결제 처리, 알림 전송 같은 소규모 작업에 적합
    • 트랜잭션 보장(한 번만 전달)
    • MSA간 데이터 전달
    • 단기 저장을 목표

 

'TIL' 카테고리의 다른 글

캐싱을 통한 성능 개선  (0) 2025.04.01
QueryDSL을 활용한 동적 쿼리 최적화  (0) 2025.03.31
DB인덱싱  (0) 2025.03.06
Cache  (0) 2025.03.05
Redis  (0) 2025.03.04

+ Recent posts