인프런 커뮤니티 질문&답변
결제 관련 서킷 브레이커 전략, 데이터 정합성 및 타임아웃 설정 질문
작성
·
43
·
수정됨
2
안녕하세요, 제미니님!
강의 완독 후, 결제 시스템의 안정성을 높이기 위해 개인 프로젝트에 외부 PG사 연동 구간에 장애 격리 처리를 직접 구현하며 경험하고 있습니다. 구현 과정에서 고민했던 설계 내용과 제 나름의 가설이 맞는지, 그리고 기존 강의 코드의 설정 의도에 대해 여쭤보고 싶습니다. 🙇🏻♂️
1. 서킷 브레이커 도입 및 트랜잭션 분리 전략
@Component
class BeeceptorPaymentClient(
private val restClient: RestClient,
private val circuitBreaker: CircuitBreaker,
) : PaymentClient {
private val log = LoggerFactory.getLogger(javaClass)
override fun requestPayment(command: PaymentCommand): PaymentResult {
return circuitBreaker.run("beeceptor-payment") {
executePayment(command)
}
.fallbackIfOpen {
log.warn("[Circuit Open] Beeceptor 결제 서비스 차단됨. 잠시 후 재시도 필요.")
throw CoreException(ErrorType.PAYMENT_EXTERNAL_API_UNAVAILABLE)
}
.getOrElse { e ->
when (e) {
is HttpClientErrorException -> {
log.warn("[PAYMENT_REJECTED] 결제 거절: ${e.responseBodyAsString}")
throw CoreException(ErrorType.PAYMENT_REJECTED)
}
is CoreException -> throw e
else -> {
log.error("[PAYMENT_FAILED] Beeceptor 결제 호출 실패", e)
throw CoreException(ErrorType.PAYMENT_EXTERNAL_API_FAIL)
}
}
}
}
}외부 PG사 장애 전파를 막기 위해 Resilience4j를 도입했고, 테스트를 위해 Beeceptor를 활용했습니다.
설정 전략:
COUNT_BASED(최소 10회, 실패율 50% Open), 단순 비즈니스 에러(4xx)는ignoreExceptions로 제외하여 시스템 장애만 감지하도록 설정했습니다.트랜잭션 분리(Facade 패턴): 트랜잭션에 대한 DB 커넥션 점유 시간을 최소화하기 위해 아래와 같이 로직을 3단계로 분리했습니다.
1. 검증: DB 조회 (ReadOnly 트랜잭션)
2. 외부 요청: 외부 PG API 호출 (No 트랜잭션 + 서킷 브레이커 적용)
3. 상태 업데이트: 결제 결과 저장 (Write 트랜잭션)
@Component
class PaymentConfirmFacade(
private val paymentService: PaymentService,
private val paymentClient: PaymentClient,
) {
fun success(orderKey: String, externalPaymentKey: String, amount: BigDecimal): Long {
// 1. [DB Transaction] 결제 검증
val command = paymentService.validatePayment(orderKey, externalPaymentKey, amount)
// 2. [No Transaction] 외부 API 호출
val paymentResult = paymentClient.requestPayment(command)
// 3. [DB Transaction] 결제 완료 처리
// 외부 API에서 받은 결과(transactionId 등)를 넘겨줌
return paymentService.completePayment(orderKey, paymentResult)
}
}
[질문] Facade 패턴을 적용함에 따라 '외부 요청'(2번)은 성공했으나, '상태 업데이트 및 저장'(3번)에서 DB 장애 등으로 실패할 경우 데이터 불일치가 발생합니다. 현재 로직상 completePayment가 실패하면 트랜잭션이 롤백되어 TransactionHistory조차 남지 않고 Payment 상태는 READY로 유지됩니다.
따라서 저는 배치/스케줄러를 통해 일정 시간이 지나도 READY 상태로 남아있는 결제 건들을 조회한 뒤, PG사 결제 내역 조회 API와 크로스 체크하여 누락된 결제를 보정하는 로직이 필요하다고 판단됩니다. 이러한 접근 방식이 실무에서사용하는 일반적인 보정 패턴인지 궁금합니다.
2. DB Connection/Socket Timeout 설정 의도와 Facade 적용 시의 상관관계
외부 API 타임아웃(Connect 3s, Read 30s)을 학습하며 적용하던 중, 기존 강의 프로젝트의 db-core.yml 설정이 눈에 들어왔습니다.
connection-timeout: 1100(1.1초)socketTimeout: 3000(3초)
[질문 1] 보통 DB 타임아웃을 넉넉하게 잡는 경우도 있는데, 이렇게 타이트하게 설정하신 의도가 "트래픽 급증 시 DB 커넥션을 얻지 못하면 빠르게 실패 처리하여 스레드 풀 고갈을 막고 시스템 전체 장애를 방지하기 위함" 인지, 혹은 다른 운영 노하우가 담겨 있는 것인지 궁금합니다.
[질문 2] 저는 Facade 패턴을 적용하여 외부 API 호출 시점에는 트랜잭션(DB Connection)을 점유하지 않도록 분리했습니다. 따라서 기술적으로는 DB 타임아웃과 서킷 브레이커(외부) 타임아웃을 서로 독립적인 관점으로 설정해도 된다고 판단했습니다.
다만, 실무 운영 관점에서는 결국 '전체 사용자 대기 시간(User Latency)' 이라는 제약이 존재할 텐데, 이때 전체 응답 시간 제한 내에서 DB와 외부 API 타임아웃 비중을 어떻게 배분하시는지 강사님만의 기준이나 노하우가 궁금합니다 !
긴 글 읽어주셔서 감사합니다.
답변 2
1
안녕하세요 질문 감사드립니다!
[질문1-1]
해당 방식의 보정 배치는 규모에 따라다르지만 기본적으로 유효하다고 봅니다!
문제 상황을 예로 들면 READY 상태로 배치를 돌리기 때문에 만약 서비스 특정이나, 어떤 이벤트로 고객들이 결제창까지 갔다가 결제를 안하고 서비스를 꺼버리게 되면 데이터가 모두 READY 일텐데 그럼 이 결제건들에 대해서는 무한으로 PG사에 조회를 하는 방식일 것이고
데이터가 많기 때문에 배치가 점점 느려질 것 입니다 (조회 기간도 만들긴 하겠지만 미결제건이 폭등할때 기준으로 보면 많을 수 있겠죠)
이 부분은 내부 배치도 문제지만 PG사 쪽에서도 건별 요청이 계속 들어올 것이기 때문에 비정상적인 조회로 볼 수 있을 것 같습니다
그래서 이걸 대체하려면 다른 전략을 활용 할 수 있을 것 같습니다
Payment 의 결제 중 같은 상태를 추가하거나, 별도 테이블에 실패 이력을 쌓거나 (물론 이것도 우리 디비 장애면 안쌓일텐데, 이건 운영 이슈로 해결해야겠죠 ㅎㅎ)
또는 이벤트 적재 인프라가 있다면 결제 실패시 이벤트를 발행해서 맥락을 끊어서 볼수도 있을 것 같습니다
[질문2-1]
적어주신 내용도 타임아웃들을 짧게 잡는 것과 연관이 있습니다!
일반적으로 배치나 특이사항을 제외하고는 커넥션 타임아웃은 1초도 넉넉하다고 봅니다
정상적인 상황에서 서버와 디비가 같은 네트웍을 사용하게 구성하는게 일반적이고, 그럼 연결을 얻는데는 초 단위일 필요도 없기 때문에 최소한으로 잡아둔 것입니다!
다만 소켓타임아웃은 쿼리의 규모, 데이터의 양에 따라도 문제가 생길 수 있는데요, 일반적으로 배치를 제외하고는 그런 쿼리를 만들지 않는게 좋기 때문에 저는 처음부터 길게 주지 않는 편입니다! (추후 정말 필요하면 늘리는 방식)
[질문2-2]
음.. 이건 사실 매 상황마다 다르기 때문에 어떤 기준은 없는 것 같습니다!
대신 쓸대없이 시간을 낭비하지 않게 구성하는게 기본 자세인 것 같습니다
시간을 늘려야한다면 충분히 타당한 근거가 있어야한다고 봅니다
저도 어디서 들은건데 현대의 대한민국 유저들은 화면이 2.5초만 멈춰있어도 바로 인지를 하고 더 나아가서는 답답함을 느끼기 시작한다고 합니다
(대신 결제나 공공기관 사이트 등 몇몇 부분에선 느린 것에 학습이 아주 잘 되어있죠, 기대가 없달까요)
반대로 에러가 뜨고 “다시 시도해주세요” 는 적어도 한두번 정도는 짜증을 안 낸다고 하네요
(썻던 글이 다 날라가는게 아니라면..)
그래서 결국 가능한+현실적인 타협선을 잘 정해서 전체 대기 시간을 조절하는게 좋다고 생각합니다
또 우리 서버와 디비 등 내부 통제가능한 인프라에 대해서는 가능한 짧게 잡아주는게 좋다고 생각합니다, 일반적으로 같은 네트워크 망이면 느슨하게 설정할 필요가 없겠죠!
각각 답변이 잘 됬나 모르겠네요! 추가 질문은 답글이나 질문으로 올려주시길 바랍니다!
모쪼록 답이 됬길 바랍니다! 감사합니다!
0
자세하고 깊이 있는 답변 정말 감사드립니다!
덕분에 실무적인 관점에서 많은 인사이트를 얻을 수 있었습니다.
특히 결제 상태를 세분화(IN_PROGRESS, CANCELED 등) 하여 장애 발생 시 실패 이력을 명확히 관리하는 방법과, 내부/외부 리소스 및 서비스 성격(공공기관 vs 사기업)에 따라 타임아웃 전략을 유연하게 가져가야 한다는 점이 크게 와닿았습니다.
답변 주신 내용과 별개로 코드 설계와 관련해 한 가지 더 여쭤보고 싶은 점이 있습니다.
진행하신 프로젝트의 db-core 모듈을 보니 Entity 클래스는 순수하게 데이터만 담고 있고, validate 같은 유효성 검증 로직은 주로 비즈니스 계층(Service)에서 처리하시는 것을 확인했습니다.
DB와 매핑되는 클래스에는 비즈니스 로직을 최대한 배제하고, 서비스 레이어에서 검증을 처리하다가 로직이 복잡해지면 추후 별도의 Validator 클래스 등으로 분리하여 관리하는 방식을 지향하시는지 제미니님의 평소 개발 스타일이 궁금합니다. (해당 프로젝트 코드에서도 그러한 스타일이 느껴져서 질문드립니다!)




