파티션을 4개를 뒀을때는 TPS가 36.4/sec이 나온반면 파티션이 1개일경우엔 18.2/sec이 나왔다. 성능개선이 2배되었다.
분류 전체보기
-
파티션의 갯수별 메시지 소비 차이
2025.04.09
- kafka를 통한 대규모트래픽 및 동시성 제어 2025.04.06
- 락 순서 고정 2025.04.06
- 비관적 락(Pessimistic Lock) 도입 2025.04.05
- 포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈 2025.04.05
- RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례 2025.04.02
- 캐싱을 통한 성능 개선 2025.04.01
- QueryDSL을 활용한 동적 쿼리 최적화 2025.03.31
파티션의 갯수별 메시지 소비 차이
kafka를 통한 대규모트래픽 및 동시성 제어
✅ 1단계: Consumer 단일 스레드로 제한
기본 전제: 정합성 최우선이라면 동시에 여러 consumer thread가 처리하지 않도록 concurrency = 1
- 💡 이렇게 하면 Consumer는 Kafka Partition 단위로 순서대로 처리하므로 race condition 없이 안전.
- 단점: 처리 속도가 느릴 수 있음.
테스트
✅ 2단계: Partition 전략 활용한 멀티 Consumer 처리
처리량 늘리면서도 정합성 지키려면?
Kafka는 기본적으로 같은 key는 같은 Partition에 들어감
→ 그러니 senderUserId를 key로 설정해 메시지를 Kafka에 보낼 때 지정해줘:
이렇게 하면:
- 같은 유저 ID끼리는 순서대로 처리 (→ 락 충돌 없음)
- 다른 유저들끼리는 동시에 송금 처리 가능 (→ 성능 up)
테스트
'TIL' 카테고리의 다른 글
락 순서 고정 (0) | 2025.04.06 |
---|---|
비관적 락(Pessimistic Lock) 도입 (0) | 2025.04.05 |
포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈 (0) | 2025.04.05 |
RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례 (0) | 2025.04.02 |
캐싱을 통한 성능 개선 (0) | 2025.04.01 |
락 순서 고정
🛠 해결 방법: 락 순서 고정 (Lock Ordering)
🔍 문제 해결 방향
서로 다른 유저 간 교차 송금(A→B, B→A)이 동시에 발생할 경우,
각 쓰레드가 락을 거는 순서가 엇갈리면 데드락이 발생할 수 있어.
예시
- 쓰레드 1: A → B 송금 → A 계좌 락 → B 계좌 락
- 쓰레드 2: B → A 송금 → B 계좌 락 → A 계좌 락
→ 서로 락을 점유한 상태에서 상대 락을 기다리다가 영원히 대기 = 💥 데드락 발생
그래서 우리는 항상 락을 거는 순서를 고정했어.
userId가 낮은 계좌 → 높은 계좌 순으로 락을 획득하도록 해서
락 순서가 일관되도록 보장했어.
🔒 락 순서 고정 흐름
- 두 유저 ID를 비교해서 오름차순 정렬
- SELECT ... FOR UPDATE로 낮은 ID 계좌부터 락을 건다
- 나머지 계좌에 대해 동일하게 락을 건다
- 트랜잭션 내에서 안전하게 포인트 이체 수행

이렇게 락 순서를 고정하면,
교차 송금이 발생하더라도 쓰레드들이 항상 동일한 순서로 락을 잡기 때문에
데드락 없이 안전하게 처리할 수 있어.
💡 도입 후 개선점
항목 | Before | After |
락 획득 순서 | 쓰레드마다 달라 데드락 가능성 있음 | 항상 고정된 순서로 락 획득 |
데드락 발생 가능성 | 높음 | 제로 |
교차 송금 안정성 | 불안정 | 안전하게 처리됨 |
👀 어디에 쓰이냐?
- A ↔ B 교차 송금
- 사용자 간 시간 기반 포인트 교환 (타임뱅크 스타일)
- 재고 차감 시 주문자가 다를 때
- 동일 리소스를 다수의 요청이 동시에 업데이트할 때
'TIL' 카테고리의 다른 글
kafka를 통한 대규모트래픽 및 동시성 제어 (0) | 2025.04.06 |
---|---|
비관적 락(Pessimistic Lock) 도입 (0) | 2025.04.05 |
포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈 (0) | 2025.04.05 |
RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례 (0) | 2025.04.02 |
캐싱을 통한 성능 개선 (0) | 2025.04.01 |
비관적 락(Pessimistic Lock) 도입
🛠 해결 방법: 비관적 락(Pessimistic Lock) 도입
비관적 락 도입
🔍 문제 해결 방향
- 트랜잭션 (@Transactional) 으로 송금 단위 작업을 하나의 논리적 처리 단위로 묶었고
- SELECT ... FOR UPDATE (비관적 락) 을 통해
같은 송신자 계좌에 여러 쓰레드가 동시에 접근할 때 락을 걸어서 충돌을 방지했어. - 테스트에서도 10개의 송금 요청을 동시에 날렸을 때도
- 데이터 정합성이 깨지지 않았고
- 데드락도 안 생기고
- 송신자 잔고: 500 / 수신자 잔고: 500 으로 정확하게 처리됐어.
🔒 비관적 락 적용 흐름
- @Lock(LockModeType.PESSIMISTIC_WRITE)를 통해 송신자, 수신자 계좌 row에 락을 건다.
- 트랜잭션 내에서 포인트 차감 및 적립을 수행한다.
- 트랜잭션이 끝나면 락이 해제된다.
💡 도입 후 개선점
항목 | Before | After |
데이터 정합성 | 충돌 및 데드락 발생 | 안정적 처리 |
에러 발생률 | Deadlock, NoSuchElementException | 없음 |
동시성 처리 전략 | 미비 | 명시적 비관적 락 도입 |
👀 그래서 이건 어디에 쓰이냐?
- 포인트 송금 / 은행 계좌 잔고 / 재고 차감 / 쿠폰 사용 등
- “하나만 있어야 하는 자원”을 실시간으로 정확하게 처리해야 할 때 매우 중요해
📌 추후 계획
- 트래픽 증가 시 낙관적 락 + 재시도로 전환 고려
- 이벤트 기반 처리 도입: Kafka + Redis Lock 조합 설계
- 조회 성능 향상을 위한 CQRS 구조 분리
'TIL' 카테고리의 다른 글
kafka를 통한 대규모트래픽 및 동시성 제어 (0) | 2025.04.06 |
---|---|
락 순서 고정 (0) | 2025.04.06 |
포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈 (0) | 2025.04.05 |
RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례 (0) | 2025.04.02 |
캐싱을 통한 성능 개선 (0) | 2025.04.01 |
포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈
💥 마주친 이슈
- 목표: 여러 사용자가 동시에 포인트를 송금할 때 데이터 정합성을 유지하는 로직을 구현하고, 데드락 발생 여부를 확인
🧪 수행한 테스트
- 테스트 시나리오
- 하나의 송신 계좌(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
- 읽기 전용 DB나 Read Replication 사용하면 확장성도 👍
💡 결론: 이 구조는 대규모 서비스에서 실제로 많이 쓰이는 패턴
- 카카오, 배민, 토스, 쿠팡 등도 이런 방식으로 내부 정합성 + 외부 이벤트 처리 + 조회 최적화를 조합해서 사용해.
- 지금처럼 MSA + Kafka + DB 기반 구조에서는 이 셋의 조합이 현실적인 베스트 프랙티스라고 보면 돼.
'TIL' 카테고리의 다른 글
락 순서 고정 (0) | 2025.04.06 |
---|---|
비관적 락(Pessimistic Lock) 도입 (0) | 2025.04.05 |
RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례 (0) | 2025.04.02 |
캐싱을 통한 성능 개선 (0) | 2025.04.01 |
QueryDSL을 활용한 동적 쿼리 최적화 (0) | 2025.03.31 |
RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례
✅ 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 적용 후 처리량
- 메시징 아키텍쳐
'TIL' 카테고리의 다른 글
비관적 락(Pessimistic Lock) 도입 (0) | 2025.04.05 |
---|---|
포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈 (0) | 2025.04.05 |
캐싱을 통한 성능 개선 (0) | 2025.04.01 |
QueryDSL을 활용한 동적 쿼리 최적화 (0) | 2025.03.31 |
RabbitMQ 와 Kafka (0) | 2025.03.07 |
캐싱을 통한 성능 개선
캐싱을 활용한 최적 경로 조회 성능 개선
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
);
}
'TIL' 카테고리의 다른 글
포인트 송금 시스템에서의 동시성 처리 및 데드락 이슈 (0) | 2025.04.05 |
---|---|
RabbitMQ 기반 비동기 아키텍처 도입 성능 개선 사례 (0) | 2025.04.02 |
QueryDSL을 활용한 동적 쿼리 최적화 (0) | 2025.03.31 |
RabbitMQ 와 Kafka (0) | 2025.03.07 |
DB인덱싱 (0) | 2025.03.06 |
QueryDSL을 활용한 동적 쿼리 최적화
QueryDSL을 활용한 동적 쿼리 최적화
1. 기술 도입 배경
기존 프로젝트에서는 JPA의 @Query 또는 Criteria API를 사용하여 복잡한 쿼리를 작성했으나,
- 가독성이 떨어지고 유지보수가 어려웠음.
- 동적 쿼리 작성 시 코드가 길어지고 복잡해짐.
- 여러 필터 조건을 유연하게 적용하기 어려웠음.
이러한 문제를 해결하기 위해 QueryDSL을 도입하여 가독성과 유지보수성을 향상하고, 동적 쿼리를 효율적으로 작성하는 방식을 적용함.
2. QueryDSL 적용 과정
- QueryDSL 환경 설정
- build.gradle에 QueryDSL 관련 의존성을 추가하고, JPAQueryFactory 빈을 설정하여 사용함.
- 동적 쿼리 구현
- 검색 필터(예: 사용자 이름, 카테고리, 작성일 등)를 조합하여 동적으로 데이터를 조회하는 기능을 구현.
- where() 절을 활용하여 조건이 있을 때만 쿼리에 추가하는 방식 적용.
- 페이징 및 성능 최적화
- 페이징을 위한 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 |