Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added payment mvc (토스) #114

Merged
merged 19 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ public class KafkaConfig {
@Value("${kafka.topic.partitions:3}")
private int TOPIC_PARTITIONS;

@Value("${payments.events.topic.name}")
@Value("${payment.events.topics.process}")
private String PAYMENT_PROCESSED_EVENT;

@Value("${payments.events.topic.fail}")
@Value("${payment.events.topics.process-fail}")
private String PAYMENT_PROCESS_FAILED_EVENT;

@Bean
Expand Down Expand Up @@ -107,7 +107,7 @@ static class CustomKafkaConsumerConfig extends KafkaConsumerConfig {

private final KafkaTemplate<String, Object> kafkaTemplate;

@Value("${payments.dead-letter-topic.name}")
@Value("${payment.dead-letter-topic}")
private String DEAD_LETTER_TOPIC;

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.f_lab.joyeuse_planete.payment.controller;

import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode;
import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException;
import com.f_lab.joyeuse_planete.core.util.log.LogUtil;
import com.f_lab.joyeuse_planete.payment.service.PaymentService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/payment")
public class PaymentController {

private static final String TOSS = "토스";

private final PaymentService paymentService;

@GetMapping("/toss-success")
public void tossSuccess(@RequestParam("paymentKey") String paymentKey,
@RequestParam("orderId") Long orderId,
@RequestParam("amount") BigDecimal amount) {

paymentService.processPaymentSuccess(paymentKey, orderId, amount, TOSS);
}

@GetMapping("/toss-fail")
public void tossFail(@RequestParam(value = "orderId", required = false) Long orderId,
@RequestParam("code") String code,
@RequestParam("message") String message) {

if (orderId == null) {
LogUtil.exception("PaymentController.tossFail", new JoyeusePlaneteApplicationException(ErrorCode.ORDER_NOT_PROCESSED_EXCEPTION_CUSTOMER));
return;
}

paymentService.processPaymentFailure(orderId, code, message, TOSS);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package com.f_lab.joyeuse_planete.payment.repository;

import com.f_lab.joyeuse_planete.core.domain.Payment;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.math.BigDecimal;


public interface PaymentRepository extends JpaRepository<Payment, Long> {

@Query(value = "INSERT INTO Payment p (payment_key, processor, order_id, total_cost, status) VALUES (:paymentKey, :processor, :orderId, totalCost, status)", nativeQuery = true)
Payment save(@Param("paymentKey") String paymentKey, @Param("processor") String processor, @Param("orderId") Long orderId, @Param("totalCost") BigDecimal totalCost, @Param("status") String status);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@
package com.f_lab.joyeuse_planete.payment.service;

import com.f_lab.joyeuse_planete.core.domain.Payment;
import com.f_lab.joyeuse_planete.core.domain.PaymentStatus;
import com.f_lab.joyeuse_planete.core.events.PaymentProcessedEvent;
import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode;
import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException;
import com.f_lab.joyeuse_planete.core.kafka.service.KafkaService;
import com.f_lab.joyeuse_planete.core.util.log.LogUtil;
import com.f_lab.joyeuse_planete.payment.repository.PaymentRepository;
import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Timed("payment")
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PaymentService {

public void process() {
private final PaymentRepository paymentRepository;
private final KafkaService kafkaService;

@Value("${payment.events.topics.process}")
private String PAYMENT_PROCESS_EVENT;

@Transactional
public void processPaymentSuccess(String paymentKey, Long orderId, BigDecimal amount, String processor) {
try {
paymentRepository.save(paymentKey, processor, orderId, amount, PaymentStatus.DONE.toString());

// "https://api.tosspayments.com/v1/payments/confirm" 에 결제성공 로직 구현
} catch (Exception e) {
LogUtil.exception("PaymentService.processPaymentSuccess", e);
throw new RuntimeException(e);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 더 명확한 예외로 처리하시면 됩니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 현재는 비동기를 적용하여 exception을 적용하도록 해두었습니다.

}

sendKafkaPaymentProcessedEvent(null);
}

@Transactional
public void processPaymentFailure(Long orderId, String code, String message, String processor) {
try {
paymentRepository.save("null", processor, orderId, BigDecimal.ZERO, PaymentStatus.ABORTED.toString());

} catch(Exception e) {
LogUtil.exception("PaymentService.processPaymentFailure", e);
throw new RuntimeException(e);
}
}

public void sendKafkaPaymentProcessedEvent(PaymentProcessedEvent event) {
try {
kafkaService.sendKafkaEvent(PAYMENT_PROCESS_EVENT, event);
} catch(Exception e) {
LogUtil.exception("PaymentService.sendKafkaEvent", e);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로깅 이외 다른 처리는 없어도 되는걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 오래 생각을 해보았는데 여기서 예외를 던져버리면 모든 transaction이 rollback 이 되어버리기 때문에 로깅 처리만 하고 재시도 로직은 카프카 dead letter topic queue 에서 처리되게 하는 것이 맞다고 생각하여 이렇게 처리하였습니다.

}
}

private Payment findPaymentById(Long paymentId) {
return paymentRepository.findById(paymentId).orElseThrow(
() -> new JoyeusePlaneteApplicationException(ErrorCode.PAYMENT_NOT_EXIST_EXCEPTION)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,66 @@
package com.f_lab.joyeuse_planete.payment.service.handler;

import com.f_lab.joyeuse_planete.core.events.OrderCreatedEvent;

import com.f_lab.joyeuse_planete.core.events.PaymentProcessedEvent;
import com.f_lab.joyeuse_planete.core.events.PaymentProcessingFailedEvent;
import com.f_lab.joyeuse_planete.core.kafka.exceptions.RetryableException;
import com.f_lab.joyeuse_planete.core.kafka.service.KafkaService;

import com.f_lab.joyeuse_planete.core.kafka.util.ExceptionUtil;
import com.f_lab.joyeuse_planete.core.util.log.LogUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaHandler;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

import java.util.Objects;

import static com.f_lab.joyeuse_planete.core.util.time.TimeConstantsString.FIVE_SECONDS;


@Slf4j
@Component
@RequiredArgsConstructor
@KafkaListener(topics = "${payments.dead-letter-topic.name}", groupId = "${spring.kafka.consumer.group-id}")
@KafkaListener(topics = "${payment.dead-letter-topic}", groupId = "${spring.kafka.consumer.group-id}")
public class PaymentDeadLetterTopicHandler {

private final KafkaService kafkaService;

@KafkaHandler
public void processDeadOrderCreatedEvent(@Payload OrderCreatedEvent orderCreatedEvent,
@Header(value = KafkaHeaders.EXCEPTION_FQCN, required = false) String exceptionName,
@Header(value = KafkaHeaders.EXCEPTION_MESSAGE, required = false) String exceptionMessage,
@Header(value = KafkaHeaders.ORIGINAL_TOPIC, required = false) String originalTopic) {
public void processDeadPaymentProcessedEvent(@Payload PaymentProcessedEvent paymentProcessedEvent,
@Header(value = KafkaHeaders.EXCEPTION_FQCN, required = false) String exceptionName,
@Header(value = KafkaHeaders.EXCEPTION_MESSAGE, required = false) String exceptionMessage,
@Header(value = KafkaHeaders.ORIGINAL_TOPIC, required = false) String originalTopic) {

handleDeadEventsForRetries(paymentProcessedEvent, exceptionName, exceptionMessage, originalTopic);
}

@KafkaHandler
public void processDeadPaymentProcessingFailedEvent(@Payload PaymentProcessingFailedEvent paymentProcessingFailedEvent,
@Header(value = KafkaHeaders.EXCEPTION_FQCN, required = false) String exceptionName,
@Header(value = KafkaHeaders.EXCEPTION_MESSAGE, required = false) String exceptionMessage,
@Header(value = KafkaHeaders.ORIGINAL_TOPIC, required = false) String originalTopic) {

handleDeadEventsForRetries(paymentProcessingFailedEvent, exceptionName, exceptionMessage, originalTopic);
}

private void handleDeadEventsForRetries(Object event, String exceptionName, String exceptionMessage, String originalTopic) {
if (Objects.isNull(exceptionMessage) ||
Objects.isNull(originalTopic) ||
ExceptionUtil.noRequeue(exceptionMessage)
) {
LogUtil.deadLetterMissingFormats(exceptionName, exceptionMessage, originalTopic);
return;
}

try {
Thread.sleep(Integer.parseInt(FIVE_SECONDS));
} catch (InterruptedException e) {
throw new RetryableException();
}

// TODO: THINK ABOUT THE LOGICS;
kafkaService.sendKafkaEvent(originalTopic, event);
}
}
46 changes: 46 additions & 0 deletions payment/src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
server:
port: 8080
tomcat:
mbeanregistry:
enabled: true

management:
endpoints:
web:
exposure:
include: "*"
exclude: "env, beans"
info:
java:
enabled: true
os:
enabled: true

prometheus:
metrics:
export:
pushgateway:
enabled: true
base-url: ${MONITORING_SERVER_IP}
push-rate: 30s
job: orders-service
enabled: true

spring:
kafka:
bootstrap-servers: ${KAFKA_SERVER_IP}

datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: validate

logging:
level:
org.hibernate.sql: ERROR
89 changes: 29 additions & 60 deletions payment/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
management:
endpoints:
web:
exposure:
include: "*"
exclude: "env, beans"

info:
java:
enabled: true

os:
enabled: true

server:
port: 8083
tomcat:
mbeanregistry:
enabled: true
port: 8082

spring:
application:
name: payments

kafka:
bootstrap-servers: localhost:9092
producer:
transaction-id-prefix: payments-tx
transaction-id-prefix: payment-tx

consumer:
group-id: payments
group-id: payment

jpa:
hibernate:
ddl-auto: create-drop

payments:
orders:
events:
topic:
name: payments.order-creation-event
fail: payments.order-creation-failed-event
topics:
create: orders.order-created-event
cancel: orders.order-cancellation-event

dead-letter-topic:
name: payments.dead-letter-topic
dead-letter-topic: orders.dead-letter-topic

foods:
events:
topic:
name: foods.order-created-event
topics:
reserve: foods.food-reserved-event
reserve-fail: foods.food-reservation-failed-event

dead-letter-topic: foods.dead-letter-topic


payment:
events:
topics:
process: payment.payment-processed-event
process-fail: payment.payment-processing-fail-event

dead-letter-topic: payment.dead-letter-topic

kafka:
topic:
Expand All @@ -51,32 +46,6 @@ kafka:
container:
concurrency: 3

---

spring:
config:
activate:
on-profile: prod

kafka:
bootstrap-servers: ${KAFKA_SERVER_IP}

management:
endpoints:
web:
exposure:
include: "*"
exclude: "env, beans"

info:
java:
enabled: true

os:
enabled: true

server:
port: 8080
tomcat:
mbeanregistry:
enabled: true
logging:
level:
org.hibernate.sql: TRACE
Loading