← 프로젝트 목록
PROJECT 01 / WORK · PURPLE ACADEMY

Purple주문 검색 SQL과 정기결제

교육 서비스의 결제 흐름과 관리자 백오피스를 개선했습니다.

ROLE
시스템개발부 · 백엔드 개발자
PERIOD
2026.04 — 현재
STACK
TypeScript · NestJS · MySQL · TypeORM · Redis · BullMQ

주문 검색 SQL 개선, 구독 단위 청구 경합 제어, 결제 응답 유실 복구, 셀프 재결제 약관 명시 동의를 맡았습니다.회사 코드는 공개할 수 없어 설계와 검증 결과만 정리했습니다.사례의 테스트 건수와 측정값은 당시 PR·개발 기록 기준입니다.

핵심 성과

  1. 01주문 검색 count SQL 4.73초 → 0.89초
  2. 02같은 구독에 30개 요청이 동시에 와도 청구는 1번
  3. 03PG 응답이 유실된 결제를 다시 조회해 실패 안내 없이 성공으로 복구
  4. 04서버가 대신하던 약관 동의를 리스크로 보고해 정식 과제로 맡고, 사용자 명시 동의로 전환
CASE 02

여러 청구 경로의 경합을 구독 단위로 제어하기

정기결제 배치, 관리자 재결제, 학부모 재결제가 같은 구독을 동시에 청구할 수 있었습니다.세 경로가 구독 단위로 같은 Redis 락을 쓰게 하고, 락을 잡은 뒤 결제 상태를 다시 확인하게 했습니다.같은 구독에 30개 요청을 동시에 보내도 청구는 한 번만 실행됐습니다.

내가 맡은 범위
관리자·배치 청구의 구독 락 적용과 TTL 조정, 학부모 재결제의 락 키 통일과 상태 재확인
선택과 이유
팀의 기존 공용 락을 구독 단위로 적용하고, 학부모 재결제는 락을 잡은 뒤 최신 결제 상태를 다시 확인하도록 했습니다.
트레이드오프
경합한 요청은 기다리지 않고 바로 거절됩니다.락 유효시간보다 오래 걸리는 처리는 막지 못합니다.
여러 청구 경로의 경합을 구독 단위로 제어하기 흐름도. 세 청구 경로를 구독 단위의 같은 락 키로 보호합니다. 경합한 요청은 기다리지 않고 바로 거절합니다.
세 청구 경로를 구독 단위의 같은 락 키로 보호합니다.경합한 요청은 기다리지 않고 바로 거절합니다.
여러 청구 경로의 경합을 구독 단위로 제어하기
세 청구 경로를 구독 단위의 같은 락 키로 보호합니다. 경합한 요청은 기다리지 않고 바로 거절합니다.

세 청구 경로를 구독 단위의 같은 락 키로 보호합니다.경합한 요청은 기다리지 않고 바로 거절합니다.

문제

정기결제 배치, 관리자 수동 청구, 학부모 재결제가 같은 구독을 동시에 청구할 수 있었습니다. 관리자 API와 웹 API는 별도 프로세스라서, 경로마다 따로 막아서는 서로의 청구를 막을 수 없었습니다.

내 기여와 해결

팀의 공용 Redis 락을 관리자·배치 청구에 구독 단위로 적용하고 TTL을 조정했습니다. 학부모 재결제에도 같은 락 키 규칙을 적용해, 세 경로가 같은 구독에 대해 하나의 락을 다투게 했습니다.

락을 잡은 뒤에는 구독과 결제 상태를 다시 조회하도록 했습니다. 락을 기다리는 사이 다른 경로가 먼저 청구를 끝냈을 수 있기 때문입니다.

검증 결과

두 앱의 실제 락 코드와 Redis를 쓰고 청구 함수만 테스트용으로 바꿔, 같은 구독에 30개 요청을 동시에 보내는 시험을 20번 반복했습니다. 매번 청구는 1번만 실행되고 나머지 29개는 거절됐습니다.

서로 다른 구독은 동시에 청구됐고, 처리 중 예외가 나거나 프로세스가 강제 종료돼도 락이 풀리는 것을 확인했습니다.

CASE 03

응답이 유실된 결제를 조회로 확인하고 복구하기

PG에서 결제가 끝났는데 응답만 유실되면, 서비스는 이를 실패로 보고 재청구 안내를 보낼 수 있었습니다.결과가 불확실한 오류에서는 PG에 결제를 다시 조회하고, 주문번호·금액·완료 상태가 모두 맞을 때만 성공으로 처리했습니다.

내가 맡은 범위
카드·브랜드페이 승인과 자동결제 오류 분류, 조회 기반 복구 조건 적용
선택과 이유
결과가 불확실한 오류만 조회하고, 같은 주문번호·같은 금액·완료(DONE)가 모두 맞을 때만 기존 성공 흐름으로 넘겼습니다.
트레이드오프
해당 오류에서는 PG 조회가 한 번 더 일어납니다.조회로도 확인되지 않으면 원래 오류를 그대로 둡니다.
응답이 유실된 결제를 조회로 확인하고 복구하기 흐름도. 결과가 불확실한 오류만 PG에 다시 조회합니다. 주문번호·금액·완료 상태가 모두 맞아야 성공으로 처리하고, 아니면 원래 오류를 유지합니다.
결과가 불확실한 오류만 PG에 다시 조회합니다.주문번호·금액·완료 상태가 모두 맞아야 성공으로 처리하고, 아니면 원래 오류를 유지합니다.
응답이 유실된 결제를 조회로 확인하고 복구하기
결과가 불확실한 오류만 PG에 다시 조회합니다. 주문번호·금액·완료 상태가 모두 맞아야 성공으로 처리하고, 아니면 원래 오류를 유지합니다.

결과가 불확실한 오류만 PG에 다시 조회합니다.주문번호·금액·완료 상태가 모두 맞아야 성공으로 처리하고, 아니면 원래 오류를 유지합니다.

문제

PG에서 결제가 완료됐어도 응답이 유실되면 서비스는 결과를 알 수 없습니다. 이를 바로 실패로 처리하면 실패 안내가 나가고, 같은 회차를 다시 청구할 수 있었습니다.

내 기여와 해결

카드·브랜드페이 승인과 자동결제에 조회 기반 복구를 적용했습니다. 상태 코드가 없는 오류, 5xx, “이미 처리된 결제” 오류가 나면 주문번호로 PG에 결제를 다시 조회합니다. 조회 결과가 같은 주문번호, 같은 금액, 완료(DONE) 상태일 때만 기존 성공 흐름으로 이어갑니다.

명확한 4xx 거절은 조회하지 않습니다. 404, 미완료, 주문·금액 불일치, 조회 실패는 성공으로 처리하지 않고 원래 오류를 그대로 둡니다. 성공으로 복구한 건은 로그를 남겨 나중에 추적할 수 있게 했습니다.

검증 결과

실제 관리자 주문·결제 코드와 Redis를 쓰고 PG와 DB만 테스트용으로 바꿔, PG가 결제를 완료한 뒤 응답만 유실되는 상황을 재현했습니다. 같은 결제에 10개 요청을 동시에 보내자 1개만 성공으로 복구되고 9개는 거절됐습니다.

청구, 복구 조회, 결제 저장은 각각 1번씩만 일어났고, 실패 안내는 한 번도 나가지 않았습니다.

다음 프로젝트GGUK