토스페이먼츠를 활용한 포인트 충전 시스템 구축기 - fitpassTeam/fitpass GitHub Wiki
토스페이먼츠를 활용한 포인트 충전 시스템 구축기
문제 : 포인트 충전을 관리자가 수동으로 충전해주는 시스템을 토스페이먼츠로 자동화하기
토스페이먼츠 도입 배경
- 포인트를 관리자가 직접 충전해주는 수동 시스템에서 자동으로 해주는 토스페이먼츠를 연동함.
- 사용자가 포인트를 충전하려면 실제 돈으로 결제해야 하기에, 이를 처리하기 위해서 신뢰된 PG사 연동을 해야하기에 도입을 결정
- Toss Payments는 금융 규제를 준수하는 합법적인 결제 처리 시스템을 제공하고, 테스트 환경을 구축해주기에 결정.
- 개발자 친화적 API : 직관적인 RESTful API와 상세한 문서 제공
- 간편한 연동 : Payment Widget을 통한 빠른 프론트엔드 구현
아키텍처 설계
계층형 아키텍처
Controller → Service → Repository → Entity
↓
Payment Client (토스페이먼츠 API)
도메인 분리
- payment : 결제 관련 로직 (토스페이먼츠 연동)
- point : 포인트 관리 로직 (기존 시스템)분리를 통해서 결제 로직과 포인트 로직의 책임을 명확히 구분하고, 향후 다른 결제 수단 추가시 확장성을 확보했다.
의사결정 배경 및 요구사항
비즈니스 요구사항
-
결제의 신뢰성 확보
실제 결제가 이루어진 경우에만 포인트가 정확히 적립되어야 하며, 이중 적립은 절대 불가
-
사용자 경험 향상
결제 실패, 취소, 지연 등 모든 상황에 대해 친절하고 명확한 피드백 제공
-
결제 흐름의 간결성
사용자 관점에서 복잡하지 않은 결제 절차를 제공하여 전환율(결제 성공률)을 높임
기술적 요구사항
-
확장성 (멀티 서버 환경 대응)
여러 서버에서 동시에 결제 callback을 수신하더라도
orderId
기준으로 중복 처리 방지(idempotency) -
결제-포인트 트랜잭션 일관성 확보
Toss 결제 상태와 우리 시스템의 포인트 적립이 반드시 동일한 시점에서 처리되어야 함
(예: DB 트랜잭션 + Toss confirm API 응답 처리 순서 중요)
-
장애 대비 안전성 확보
Toss 결제는 완료됐지만 서버 처리 실패한 경우를 고려해 후속 보정처리 가능하도록 설계
-
보안 및 위변조 방지
사용자가 조작한 금액을 그대로 포인트로 적립하는 일이 없도록 Toss의 결제 결과(
amount
)를 서버에서 항상 검증 -
유지보수성과 추적성
각 결제 요청 및 응답, 포인트 적립/환불 기록은 로그로 남기고, 관리자가 추적 가능해야 함
결제 플로우
토스페이먼츠 API 클라이언트 구현
@Component
@RequiredArgsConstructor
@Slf4j
public class TossPaymentClient {
private final TossPaymentConfig config;
private final WebClient webClient = WebClient.builder().build();
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
public PaymentResponseDto confirmPayment(PaymentConfirmRequestDto request) {
try {
String auth = encodeSecretKey(config.getSecretKey());
String response = webClient.post()
.uri(TossPaymentConfig.TOSS_CONFIRM_URL)
.header(HttpHeaders.AUTHORIZATION, "Basic " + auth)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(request)
.retrieve()
.bodyToMono(String.class)
.block();
return objectMapper.readValue(response, PaymentResponseDto.class);
} catch (Exception e) {
log.error("토스페이먼츠 결제 승인 실패", e);
throw new RuntimeException("결제 승인 처리 중 오류가 발생했습니다", e);
}
}
private String encodeSecretKey(String secretKey) {
return Base64.getEncoder()
.encodeToString((secretKey + ":").getBytes(StandardCharsets.UTF_8));
}
}
장점:
- 멀티 서버 대응:
orderId
기준 중복 결제 방지 - 세밀한 트랜잭션 처리: DB 트랜잭션과 Toss 응답을 묶어 일관성 확보
- 친절한 사용자 메시지 제공 가능
- 보안과 신뢰성 강화: 금액 위변조 방지, 응답 검증