파티션을 4개를 뒀을때는 TPS가 36.4/sec이 나온반면 파티션이 1개일경우엔 18.2/sec이 나왔다. 성능개선이 2배되었다.

✅ 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 기반 구조에서는 이 셋의 조합이 현실적인 베스트 프랙티스라고 보면 돼.

+ Recent posts