Начав реализацию модуля оплаты, первой проблемой, с которой я столкнулся, было не просто "как вызвать API для одобрения платежа". Ключевым моментом было "сможем ли мы обеспечить согласованный опыт оплаты, не нарушая поток платежей сервиса, независимо от того, какие платежные агентства (PG) будут добавлены в будущем".
Начальная цель MVP заключалась в интеграции T***Payments, однако в дальнейшем могут возникнуть ситуации, когда необходимо поддерживать несколько PG одновременно в зависимости от способов оплаты, условий расчета, поддержки международных переводов (Stripe и др.) и требований клиентов. Поэтому мы установили цели, выходящие за рамки простой интеграции с API.
-
Собирает различные коды подключения PG в одном месте, предотвращая фрагментацию.
-
Обеспечивается структурная масштабируемость, позволяющая легко добавлять новые PG.
-
Сохраняет данные о платежах во внутреннюю БД для упрощения отслеживания и отладки.
1. Ограничения обработки ветвлений if-else и внедрение паттерна стратегии
Различные интеграции PG можно напрямую реализовать в логике платежного сервиса. Однако каждый раз, когда добавляются Stripe, K****Pay, зарубежные PG и т.д., поток платежей будет заполняться условиями, как показано ниже.
if (pgProvider.equals("T***Payments")) {
// T*** 승인 로직
} else if (pgProvider.equals("Stripe")) {
// Stripe 승인 로직
} else if (pgProvider.equals("SomeOtherPg")) {
// 다른 PG 승인 로직}
Этот подход имеет критический недостаток: с увеличением количества PG сильно связываются основные бизнес-процессы платежей и детали внешней интеграции. Платежный поток должен сосредоточиться только на главном процессе "Проверка суммы → Одобрение PG → Обработка успеха/неудачи", и нет необходимости знать, каковы заголовки аутентификации или методы парсинга ответов для каждого PG. Для разделения этой логики был применен паттерн стратегии (Strategy Pattern).
2. Дизайн общего интерфейса (PgProxy) и его реализации
Сначала все PG компании должны совместно реализоватьPgProxy Мы определили интерфейс. С точки зрения платежного потока, независимо от типа PG-компании, необходимо просто отправить одно и то же сообщение: "Подтвердите платеж."
public interface PgProxy {
String getPgProviderType();
WebClient getWebClient();
PgPaymentResponse approvePayment(String paymentKey, String orderId, Money amount);
PgPaymentResponse getPgPaymentResponse(String orderId);
}
Тo* платежный интерфейс наследуется и реализуется следующим образом.
@Configuration
@ConditionalOnProperty(value = "pg.t***.enabled", havingValue = "true")
public class T***PgProxy implements PgProxy {
@Value("${pg.t***.secret-key}")
private String secretKey;
@Override
public PgPaymentResponse approvePayment(String paymentKey, String orderId, Money amount) {
T***Payment response = confirmPayment(paymentKey, orderId, amount);
return new PgPaymentResponse(response);
}
@Override
public String getPgProviderType() {
return "T***Payments";
}
}
здесь @ConditionalOnPropertyС использованием аннотаций, было осуществлено управление тем, чтобы только необходимые реализации PG динамически регистрировались как весенние бины в зависимости от параметра (pg.t***.enabled=true). В будущем также будет добавлен StripePgProxy, который может быть подготовлен для реализации того же интерфейса.
3. Фабрика маршрутизации (PgProxyFactory), находящая подходящую стратегию
Теперь мы настраиваем PgProxyFactory для поиска реализации, соответствующей запрошенному типу PG.
@Component
public class PgProxyFactory {
private final Map<String, PgProxy> pgProxies;
public PgProxyFactory(List<PgProxy> pgProxies) {
this.pgProxies = pgProxies.stream()
.collect(Collectors.toMap(PgProxy::getPgProviderType, Function.identity()));
}
public PgProxy getPgProxy(String pgProvider) {
PgProxy pgProxy = pgProxies.get(pgProvider);
if (pgProxy == null) {
throw new NoSuchElementException("지원하지 않는 결제 대행사입니다: " + pgProvider); }
return pgProxy;
}
}
Когда контейнер Spring предоставит все бины типа PgProxy в виде списка, фабрика преобразует их в формат Map и хранит в памяти. Затем, когда бизнес-логика передает идентификатор PG (например, Enum), возвращается соответствующая реализация. Хотя имя класса Factorykaka, здесь он выполняет роль стратегического менеджера, который выбирает 'стратегический объект', соответствующий среде выполнения, а не 'создание' объекта.
4. Упрощение ключевой бизнес-логики
Фактическая логика службы платежей, применяющая стратегический паттерн, полностью освобождается от подробностей внешней связи.
@Service
@Transactional
@RequiredArgsConstructor
public class PaymentNeutFlow {
private final PaymentTask paymentTask;
private final PgProxyFactory pgProxyFactory;
private final PaymentLogic paymentLogic;
public String requestPayment(String paymentId, BigDecimal amount, String pgPaymentKey) {
paymentTask.updatePgPaymentKey(paymentId, pgPaymentKey);
try {
Payment payment = paymentLogic.findPayment(paymentId);
paymentTask.validateAmount(paymentId, amount);
// 외부 PG 통신: 팩토리에서 구현체를 가져와 승인 요청만 위임
PgPaymentResponse pgPaymentResponse = pgProxyFactory
.getPgProxy(payment.getPgProvider())
.approvePayment(pgPaymentKey, payment.getId(), payment.getAmount());
paymentTask.markSuccess(pgPaymentResponse.getOrderId());
return pgPaymentResponse.getOrderId();
} catch (Exception e) {
paymentTask.markFailed(paymentId);
throw e;
}
}
}
Теперь PaymentNeutFlowне знает URL API или формата объекта ответа конкретного PG. Он сосредотачивается только на своих основных бизнес-обязанностях, таких как хранение ключа оплаты, проверка суммы, делегирование одобрения через фабрику, обработка состояния успеха/ошибки.
5. Структура, открытая для расширения (достижение принципа OCP)
Эта структура проявляет свою истинную ценность, когда добавляется новый PG (например, N***Pay). Необходимо всего лишь выполнить две задачи.
-
PgProvider Enum в N***PayДобавляем константу.
-
PgProxyнаследует N***PgProxyСоздайте новый класс.
существующий платежный поток(PaymentNeutFlow)на фабрике(PgProxyFactory) кодом Не нужно ничего изменять.Потому что Spring автоматически обнаруживает и внедряет новые реализации. Поскольку область изменений минимально ограничена, вероятность возникновения ошибок в существующем функционале (регрессионных ошибок) также значительно снижается.
6. Эффект реализации и завершение
Преимущества, полученные путем объединения паттерна стратегии и концепции адаптера в модуле оплаты, следующие.
-
Упрощение платежного процесса:Сервисный уровень не требует знания деталей по каждой платёжной системе и сосредоточен только на самом бизнес-процессе платежей.
-
Высокая когезия:Код, зависящий от конкретного PG, такой как тайм-ауты связи, настройки заголовков и парсинг ответов, безопасно изолирован внутри каждого объекта Proxy.
-
Гибкие настройки управления: @ConditionalOnPropertyС помощью этого можно свободно включать и отключать определенные реализации PG в зависимости от локальной, разработки, эксплуатации и других условий.
-
Обеспечение удобства тестирования:Мокирование внешнего коммуникационного блока облегчает написание модульных тестов для самого процесса оплаты.
Платежная система неизбежно сталкивается с внешним миром, где часто происходят сбои и изменения. Надежный платежный модуль должен иметь барьер, который не позволит внешним изменениям затронуть основную логику системы.
chnsik