Skip to content

Commit 68e3db9

Browse files
authored
Merge pull request #150 from sieun0322/main
[volume-6] 장애 대응 및 회복전략
2 parents 5795511 + 12d1676 commit 68e3db9

File tree

69 files changed

+102686
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+102686
-5
lines changed

apps/commerce-api/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ dependencies {
1010
implementation("org.springframework.boot:spring-boot-starter-web")
1111
implementation("org.springframework.boot:spring-boot-starter-actuator")
1212
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
13-
13+
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
14+
implementation("io.github.resilience4j:resilience4j-spring-boot3")
15+
implementation("org.springframework.boot:spring-boot-starter-aop")
1416
// querydsl
1517
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
1618
annotationProcessor("jakarta.persistence:jakarta.persistence-api")

apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
import org.springframework.boot.SpringApplication;
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
66
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
7+
import org.springframework.cloud.openfeign.EnableFeignClients;
8+
import org.springframework.scheduling.annotation.EnableScheduling;
79
import java.util.TimeZone;
810

911
@ConfigurationPropertiesScan
12+
@EnableFeignClients
13+
@EnableScheduling
1014
@SpringBootApplication
1115
public class CommerceApiApplication {
1216

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.payment.CardType;
4+
import com.loopers.interfaces.api.client.PaymentCreateV1Dto;
5+
6+
public record CreatePaymentCommand(Long userId, Long orderId, CardType cardType, String cardNo, Long amount, String callbackUrl) {
7+
8+
public static CreatePaymentCommand from(Long userId, PaymentCreateV1Dto.PaymentRequest request) {
9+
return new CreatePaymentCommand(
10+
userId,
11+
request.orderId(),
12+
request.cardType(),
13+
request.cardNo(),
14+
request.amount(),
15+
request.callbackUrl()
16+
);
17+
}
18+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.order.Money;
4+
import com.loopers.domain.payment.Payment;
5+
import com.loopers.domain.payment.PaymentService;
6+
import com.loopers.infrastructure.monitoring.PaymentMetricsService;
7+
import com.loopers.interfaces.api.client.PaymentCallbackV1Dto;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class PaymentFacade {
14+
15+
private final PaymentService paymentService;
16+
private final PgClient pgClient;
17+
private final PaymentMetricsService paymentMetricsService;
18+
19+
public PaymentInfo requestPayment(CreatePaymentCommand command) {
20+
21+
// 1) 도메인 초기 상태 저장
22+
Payment payment = Payment.create(command.orderId()
23+
, command.cardType(), command.cardNo()
24+
, Money.wons(command.amount()));
25+
Payment saved = paymentService.createPendingPayment(payment);
26+
27+
// 2) PG 요청 (외부 통신)
28+
PgPayRequest pgRequest = new PgPayRequest(
29+
command.orderId().toString(),
30+
command.cardType().toString(),
31+
command.cardNo(),
32+
command.amount(),
33+
command.callbackUrl()
34+
);
35+
36+
PgPayResponse pgResponse = pgClient.requestPayment(pgRequest);
37+
38+
// 3) PG 결과 반영하여 최종 상태 저장
39+
Payment finalPayment = paymentService.updateWithPgResult(saved, pgResponse);
40+
41+
return PaymentInfo.from(finalPayment);
42+
}
43+
44+
public PaymentInfo getPayment(String paymentId) {
45+
return PaymentInfo.from(paymentService.getPayment(paymentId));
46+
}
47+
48+
public void handlePaymentCallback(PaymentCallbackV1Dto.CallbackRequest dto) {
49+
try {
50+
// 콜백 로그 저장
51+
Payment payment = paymentService.getPaymentByOrderId(dto.orderId());
52+
boolean success = dto.reason() == null || !dto.reason().toLowerCase().contains("error");
53+
paymentMetricsService.recordPaymentCallback(success, payment.getCardType().name());
54+
55+
// 결제 상태
56+
payment.approved();
57+
paymentService.updatePaymentFromCallback(payment);
58+
} catch (Exception e) {
59+
// 콜백 처리 실패 시 에러 로그
60+
try {
61+
Payment payment = paymentService.getPaymentByOrderId(dto.orderId());
62+
paymentMetricsService.recordPaymentError("/api/v1/payments/callback", "general", payment.getCardType().name());
63+
} catch (Exception ignored) {
64+
// 로그 저장 실패는 무시
65+
}
66+
throw e;
67+
}
68+
}
69+
70+
public PgPaymentInfoResponse getPaymentInfoFromPg(String transactionKey) {
71+
return pgClient.getPaymentInfo(transactionKey);
72+
}
73+
74+
public PgPaymentListResponse getPaymentsByOrderFromPg(String orderId) {
75+
return pgClient.getPaymentsByOrder(orderId);
76+
}
77+
}
78+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.payment.CardType;
4+
import com.loopers.domain.payment.Payment;
5+
import com.loopers.domain.payment.PaymentStatus;
6+
7+
import java.math.BigDecimal;
8+
9+
public record PaymentInfo(
10+
String paymentId,
11+
Long orderId,
12+
CardType cardType,
13+
String cardNo,
14+
BigDecimal amount,
15+
PaymentStatus status,
16+
String transactionKey
17+
) {
18+
19+
public static PaymentInfo from(Payment payment) {
20+
return new PaymentInfo(
21+
payment.getId().toString(),
22+
payment.getOrderId(),
23+
payment.getCardType(),
24+
payment.getCardNo(),
25+
payment.getAmount().getAmount(),
26+
payment.getStatus(),
27+
payment.getTransactionKey()
28+
);
29+
}
30+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.payment.Payment;
4+
import com.loopers.domain.payment.PaymentService;
5+
import com.loopers.domain.payment.PaymentStatus;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.stereotype.Service;
9+
10+
import java.time.LocalDateTime;
11+
import java.util.List;
12+
13+
@Slf4j
14+
@Service
15+
@RequiredArgsConstructor
16+
public class PaymentSyncBatchService {
17+
18+
private final PaymentService paymentService;
19+
private final PgClient pgClient;
20+
21+
public void syncPendingPayments() {
22+
log.info("결제 상태 동기화 배치 시작");
23+
24+
List<Payment> pendingPayments = paymentService.getPendingPayments();
25+
log.info("동기화 대상 결제 건수: {}", pendingPayments.size());
26+
27+
int successCount = 0;
28+
int failCount = 0;
29+
30+
for (Payment payment : pendingPayments) {
31+
try {
32+
syncPaymentStatus(payment);
33+
successCount++;
34+
} catch (Exception e) {
35+
log.error("결제 동기화 실패 - paymentId: {}, orderId: {}, error: {}",
36+
payment.getId(), payment.getOrderId(), e.getMessage());
37+
failCount++;
38+
}
39+
}
40+
41+
log.info("결제 상태 동기화 배치 완료 - 성공: {}, 실패: {}", successCount, failCount);
42+
}
43+
44+
private void syncPaymentStatus(Payment payment) {
45+
if (payment.getTransactionKey() == null) {
46+
log.debug("transactionKey가 없는 결제는 스킵 - paymentId: {}", payment.getId());
47+
return;
48+
}
49+
50+
try {
51+
PgPaymentInfoResponse pgInfo = pgClient.getPaymentInfo(payment.getTransactionKey());
52+
53+
PaymentStatus pgStatus = PaymentStatus.valueOf(pgInfo.status());
54+
if (payment.getStatus() != pgStatus) {
55+
log.info("결제 상태 변경 감지 - paymentId: {}, {} -> {}",
56+
payment.getId(), payment.getStatus(), pgStatus);
57+
58+
paymentService.updatePaymentFromCallback(payment);
59+
}
60+
} catch (Exception e) {
61+
log.warn("PG 결제 정보 조회 실패 - paymentId: {}, transactionKey: {}",
62+
payment.getId(), payment.getTransactionKey(), e);
63+
}
64+
}
65+
66+
public void syncFailedPayments() {
67+
log.info("실패 결제 재확인 배치 시작");
68+
69+
List<Payment> failedPayments = paymentService.getRecentFailedPayments(24);
70+
log.info("재확인 대상 실패 결제 건수: {}", failedPayments.size());
71+
72+
for (Payment payment : failedPayments) {
73+
try {
74+
syncPaymentStatus(payment);
75+
} catch (Exception e) {
76+
log.error("실패 결제 재확인 실패 - paymentId: {}", payment.getId(), e);
77+
}
78+
}
79+
80+
log.info("실패 결제 재확인 배치 완료");
81+
}
82+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.loopers.application.payment;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.scheduling.annotation.Scheduled;
6+
import org.springframework.stereotype.Component;
7+
8+
@Slf4j
9+
@Component
10+
@RequiredArgsConstructor
11+
public class PaymentSyncScheduler {
12+
13+
private final PaymentSyncBatchService batchService;
14+
15+
@Scheduled(fixedDelay = 300000) // 5분마다 실행
16+
public void syncPendingPayments() {
17+
try {
18+
batchService.syncPendingPayments();
19+
} catch (Exception e) {
20+
log.error("결제 상태 동기화 배치 실행 중 오류 발생", e);
21+
}
22+
}
23+
24+
@Scheduled(cron = "0 */30 * * * *") // 30분마다 실행
25+
public void syncFailedPayments() {
26+
try {
27+
batchService.syncFailedPayments();
28+
} catch (Exception e) {
29+
log.error("실패 결제 재확인 배치 실행 중 오류 발생", e);
30+
}
31+
}
32+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.loopers.application.payment;
2+
3+
public interface PgClient {
4+
PgPayResponse requestPayment(PgPayRequest request);
5+
6+
PgPaymentInfoResponse getPaymentInfo(String transactionKey);
7+
8+
PgPaymentListResponse getPaymentsByOrder(String orderId);
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.loopers.application.payment;
2+
3+
public record PgPayRequest(
4+
String orderId,
5+
String cardType,
6+
String cardNo,
7+
Long amount,
8+
String callbackUrl
9+
) {
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.loopers.application.payment;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
public record PgPayResponse(
6+
@JsonProperty("transactionKey")
7+
String transactionKey,
8+
@JsonProperty("status")
9+
String status,
10+
@JsonProperty("reason")
11+
String reason
12+
) {
13+
public boolean isSuccess() {
14+
return "PENDING".equals(status);
15+
}
16+
}

0 commit comments

Comments
 (0)