Now Loading ...
-
0. Intro
우리는 PG사 API와 연동하여 결제 서비스를 클라이언트에게 제공해주고 있다.
결제 서비스 내에서도 결제 수단 등록/삭제, 결제 수단 조회와 등록된 결제 수단을 통한 책정 금액 결제 또는 책정 금액의 청구서를 카카오톡으로 발송
우리는 가맹점과 소비자의 중간 매개체 역할이며, 소비자가 이용한 서비스에 대한 책정 금액을 가맹점이 결정하여 우리에게 전달한다.
우리는 책정된 금액과 소비자 정보를 전달받아 결제 방식을 결정한다.
만약 소비자가 우리의 플랫폼 APP의 회원일 경우, PG사에서 제공해주는 자동 결제 시스템을 통해 소비자가 미리 등록한 결제 수단으로 즉시 결제 요청한다.
소비자가 미리 등록한 결제 수단이 없거나 결제 수단에 문제가 있다고 판단될 경우, 소비자의 연락처를 통해 카카오톡 청구서를 발송한다.
소비자가 우리의 플랫폼 APP의 회원이 아닐 경우, 즉시 카카오톡 청구서 방식으로 요청한다.
결제 요청 후, PG사로부터 Callback API를 통해 결제 요청 결과를 수신 받는다.
환불이 필요할 경우, 결제 요청 당시 생성한 청구서 ID와 요청 금액을 전달하여 환불 요청한다.
재결제가 필요할 경우, 환불 요청 후, 새로운 청구서 ID를 생성하여 결제 요청한다.
기존 결제 서비스는 모든 외부 모듈들이 필수로 주입 받고 있는 common 모듈 안에 구현되어있어서 결제 서비스가 필요하지 않더라도 인스턴스를 생성하고 있었다.
우리는 기존 서비스 기능에 대한 고도화 작업 및 신규 기능 추가 작업으로 인해 점차 리소스 비용이 증가하고 있었다.
따라서 결제 서비스 전용 모듈을 분리하여 MSA 아키텍처로 전환하기로 결정하였다.
이를 통해 결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄이며, 향후 기능 확장 및 유지보수를 용이하게 할 수 있을 것으로 기대하고 있다.
또한, MSA 아키텍처로의 전환은 각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련해줄 것이다.
목표
결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄인다.
향후 기능 확장 및 유지보수를 용이하게 한다.
각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련한다.
추상화 구조로 설계해본다. 어댑터 패턴 (Adapter Pattern) 이란?
기존 복잡한 VIEW TABLE을 사용하지 않는 방향으로, 제거하는 방향으로 설계해보자.
결제 모듈만이 관리하는 테이블과 각 클라이언트가 관리하는 테이블을 분리하고 각자 관리한다.
-
-
-
-
3. PG사 외부 API 통신을 위한 RestClient 설계
Spring Framework 6.1에서 도입된 RestClient와 @HttpExchange 어노테이션을 활용한 선언적 HTTP 클라이언트 구현에 대해 설명합니다.
인터페이스 기반 RestClient 설계
Spring의 @HttpExchange 어노테이션을 사용하여 선언적으로 HTTP 클라이언트를 정의할 수 있습니다.
@Component
@HttpExchange
public interface PgPaymentApiRestClient {
@PostExchange("...")
바로결제ResponseBody 바로결제(@RequestBody 바로결제RequestBody request);
@PostExchange("...")
청구서카카오톡발송ResponseBody 청구서카카오톡발송(@RequestBody 청구서카카오톡발송RequestBody request);
@PostExchange("...")
청구서URL조회ResponseBody 청구서URL조회(@RequestBody 청구서URL조회RequestBody request);
@PostExchange("...")
결제정보조회ResponseBody 결제정보조회(@RequestBody 결제정보조회RequestBody request);
@PostExchange("...")
환불요청ResponseBody 환불요청(@RequestBody 환불요청RequestBody request);
@PostExchange("...")
청구서파기ResponseBody 청구서파기(@RequestBody 청구서파기RequestBody request);
}
인터페이스 설명
PgPaymentApiRestClient 인터페이스는 다음과 같은 특징을 가집니다:
선언적 HTTP 통신
@HttpExchange 어노테이션으로 HTTP 클라이언트임을 선언
각 메서드에 @PostExchange 어노테이션으로 POST 요청 정의
요청/응답 DTO를 통한 타입 안전성 보장
메서드 구성
바로결제: 즉시 결제 요청 처리
청구서카카오톡발송: 결제 청구서 카카오톡 발송
청구서URL조회: 결제 청구서 URL 정보 조회
결제정보조회: 기존 결제 내역 조회
환불요청: 결제 취소 및 환불 처리
청구서파기: 발급된 청구서 무효화
요청/응답 매핑
@RequestBody를 통한 요청 본문 자동 매핑
응답 데이터의 자동 역직렬화
RestClient 설정
아래는 RestClient의 구성과 설정을 담당하는 설정 클래스입니다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RestClientConfig {
@Value("${payment.pg.api.url}")
private String pgApiUri;
@Bean
public PgPaymentApiRestClient pgPaymentApiRestClient() {
return HttpServiceProxyFactory.builderFor(
RestClientAdapter.create(paymentRestClientBuilder().baseUrl(pgApiUri).build()))
.build()
.createClient(PgPaymentApiRestClient.class);
}
@Bean
public RestClient.Builder paymentRestClientBuilder() {
return RestClient.builder()
.defaultHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.defaultStatusHandler(
HttpStatusCode::isError,
(request, response) -> {
log.error("httpRequest: {}, {}", request.getHeaders(), request.getURI());
log.error("httpResponse: {}, {}", response.getStatusCode(), response.getStatusText());
throw new PaymentException(response.toString());
});
}
}
설정 클래스 주요 특징
환경 설정
@Value 어노테이션으로 PG사 API URL 주입
외부 설정을 통한 유연한 환경 관리
RestClient 빌더 설정
Content-Type과 Accept 헤더 기본값 설정
에러 상태 코드에 대한 전역 핸들러 구성
상세한 에러 로깅 구현
프록시 팩토리 설정
HttpServiceProxyFactory를 통한 인터페이스 구현체 자동 생성
RestClient 어댑터 구성
동적 프록시를 통한 HTTP 클라이언트 생성
장점
코드 간결성
인터페이스 선언만으로 HTTP 클라이언트 구현
보일러플레이트 코드 최소화
타입 안전성
컴파일 시점에 타입 검증
DTO를 통한 요청/응답 데이터 구조화
유지보수성
선언적 프로그래밍으로 가독성 향상
중앙화된 설정으로 일관성 있는 구현
확장성
새로운 API 엔드포인트 추가가 용이
공통 설정의 유연한 확장
주의사항
에러 처리
적절한 예외 처리 로직 구현 필요
상세한 로깅으로 문제 추적 가능성 확보
타임아웃 설정
적절한 타임아웃 값 설정 필요
네트워크 지연 상황 고려
보안
민감한 정보의 로깅 제외
적절한 인증/인가 처리
이러한 설계를 통해 PG사와의 안정적이고 유지보수가 용이한 HTTP 통신을 구현할 수 있습니다.
-
4. 결제 서비스 Runnable 패턴 구현
결제 처리 과정에서 발생하는 다양한 이벤트를 처리하기 위한 PaymentAdapterRunnable 구현에 대해 설명합니다.
이 클래스는 결제 처리의 각 단계에서 발생하는 이벤트(UUID 생성, 성공, 실패)를 함수형 인터페이스를 통해 처리합니다.
클래스 구조
public record PaymentAdapterRunnable<T>(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<T> successRunnable,
ErrorRunnable errorRunnable) {
public static PaymentAdapterRunnable<BaseResponse> paymentRunnable(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<BaseResponse> successRunnable,
ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(createUuidRunnable, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<Void> callbackRunnable(
SuccessRunnable<Void> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<TalkResponse> sendBillKakaoTalkRunnable(
SuccessRunnable<KakaoTalkResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<UrlResponse> getBillUrlRunnable(
SuccessRunnable<UrlResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<RefundResponse> refundRunnable(
SuccessRunnable<RefundResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<DestroyResponse> destroyRunnable(
SuccessRunnable<DestroyResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
주요 컴포넌트
1. Record 클래스 정의
public record PaymentAdapterRunnable<T>(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<T> successRunnable,
ErrorRunnable errorRunnable) {
Java 21의 Record 기능을 활용한 불변 데이터 클래스
제네릭 타입 <T>를 통한 다양한 응답 타입 지원
세 가지 핵심 Runnable 컴포넌트 포함:
CreateUuidRunnable: UUID 생성 시점의 콜백
SuccessRunnable<T>: 성공 처리 콜백
ErrorRunnable: 오류 처리 콜백
2. 정적 팩토리 메서드
결제 요청 Runnable
public static PaymentAdapterRunnable<BaseResponse> paymentRunnable(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<BaseResponse> successRunnable,
ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(createUuidRunnable, successRunnable, errorRunnable);
}
모든 콜백을 포함한 전체 Runnable 생성
결제 요청 시 사용되는 기본 구현
콜백 Runnable
public static PaymentAdapterRunnable<Void> callbackRunnable(
SuccessRunnable<Void> successRunnable,
ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
UUID 생성이 필요 없는 콜백 처리용
빈 UUID 생성 콜백 포함
기타 특화된 Runnable
sendBillKakaoTalkRunnable: 카카오톡 청구서 발송
getBillUrlRunnable: 청구서 URL 조회
refundRunnable: 환불 처리
destroyRunnable: 청구서 파기
설계 특징
1. 함수형 프로그래밍 활용
람다식을 통한 간결한 콜백 구현
함수형 인터페이스를 통한 유연한 이벤트 처리
2. 제네릭을 통한 타입 안전성
응답 타입에 따른 타입 파라미터 지정
컴파일 시점의 타입 검증
3. 불변성 보장
Record 클래스를 통한 불변 객체 구현
스레드 안전성 확보
4. 메서드 특화
각 결제 프로세스에 특화된 팩토리 메서드
불필요한 콜백 제거로 효율성 향상
사용 예시
// 결제 요청 Runnable 생성
PaymentAdapterRunnable<BaseResponse> runnable = PaymentAdapterRunnable.paymentRunnable(
uuid -> log.info("UUID created: {}", uuid),
(uuid, response) -> log.info("Payment success: {}", response),
(uuid, error) -> log.error("Payment failed: {}", error)
);
// 환불 요청 Runnable 생성
PaymentAdapterRunnable<RefundResponse> refundRunnable = PaymentAdapterRunnable.refundRunnable(
(uuid, response) -> log.info("Refund success: {}", response),
(uuid, error) -> log.error("Refund failed: {}", error)
);
장점
코드 재사용성
공통 콜백 패턴 표준화
중복 코드 제거
유연성
다양한 결제 프로세스에 대응
콜백 조합의 자유로운 구성
유지보수성
명확한 책임 분리
테스트 용이성
타입 안전성
컴파일 타임 타입 체크
런타임 에러 방지
주의사항
콜백 체인 관리
과도한 콜백 중첩 방지
적절한 에러 처리 구현
메모리 관리
콜백 객체의 적절한 해제
순환 참조 방지
디버깅
콜백 실행 추적을 위한 로깅 구현
명확한 에러 메시지 제공
이러한 Runnable 패턴 구현을 통해 결제 처리의 각 단계에서 발생하는 이벤트를 효율적이고 유연하게 처리할 수 있습니다.
-
Intro
우리는 PG사 API와 연동하여 결제 서비스를 클라이언트에게 제공해주고 있다.
결제 서비스 내에서도 결제 수단 등록/삭제, 결제 수단 조회와 등록된 결제 수단을 통한 책정 금액 결제 또는 책정 금액의 청구서를 카카오톡으로 발송
우리는 가맹점과 소비자의 중간 매개체 역할이며, 소비자가 이용한 서비스에 대한 책정 금액을 가맹점이 결정하여 우리에게 전달한다.
우리는 책정된 금액과 소비자 정보를 전달받아 결제 방식을 결정한다.
만약 소비자가 우리의 플랫폼 APP의 회원일 경우, PG사에서 제공해주는 자동 결제 시스템을 통해 소비자가 미리 등록한 결제 수단으로 즉시 결제 요청한다.
소비자가 미리 등록한 결제 수단이 없거나 결제 수단에 문제가 있다고 판단될 경우, 소비자의 연락처를 통해 카카오톡 청구서를 발송한다.
소비자가 우리의 플랫폼 APP의 회원이 아닐 경우, 즉시 카카오톡 청구서 방식으로 요청한다.
결제 요청 후, PG사로부터 Callback API를 통해 결제 요청 결과를 수신 받는다.
환불이 필요할 경우, 결제 요청 당시 생성한 청구서 ID와 요청 금액을 전달하여 환불 요청한다.
재결제가 필요할 경우, 환불 요청 후, 새로운 청구서 ID를 생성하여 결제 요청한다.
기존 결제 서비스는 모든 외부 모듈들이 필수로 주입 받고 있는 common 모듈 안에 구현되어있어서 결제 서비스가 필요하지 않더라도 인스턴스를 생성하고 있었다.
우리는 기존 서비스 기능에 대한 고도화 작업 및 신규 기능 추가 작업으로 인해 점차 리소스 비용이 증가하고 있었다.
따라서 결제 서비스 전용 모듈을 분리하여 MSA 아키텍처로 전환하기로 결정하였다.
이를 통해 결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄이며, 향후 기능 확장 및 유지보수를 용이하게 할 수 있을 것으로 기대하고 있다.
또한, MSA 아키텍처로의 전환은 각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련해줄 것이다.
목표
결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄인다.
향후 기능 확장 및 유지보수를 용이하게 한다.
각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련한다.
추상화 구조로 설계해본다. 어댑터 패턴 (Adapter Pattern) 이란?
기존 복잡한 VIEW TABLE을 사용하지 않는 방향으로, 제거하는 방향으로 설계해보자.
결제 모듈만이 관리하는 테이블과 각 클라이언트가 관리하는 테이블을 분리하고 각자 관리한다.
-
-
-
Touch background to close