Skip to content

Commit

Permalink
Merge pull request #237 from Team-BC-1/feat/toss-payment-event-publish
Browse files Browse the repository at this point in the history
토스 API 호출 핸들러를 EventPublisher & Callback 을 통해 분리
  • Loading branch information
Mayst1232 authored Feb 5, 2024
2 parents 8798ba6 + dda8f40 commit a841c29
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import bc1.gream.domain.payment.toss.dto.response.TossPaymentSuccessResponseDto;
import bc1.gream.domain.payment.toss.entity.TossPayment;
import bc1.gream.domain.payment.toss.mapper.TossPaymentMapper;
import bc1.gream.domain.payment.toss.service.TossPaymentService;
import bc1.gream.domain.payment.toss.service.PaymentService;
import bc1.gream.domain.payment.toss.validator.TossPaymentRequestValidator;
import bc1.gream.global.common.RestResponse;
import bc1.gream.global.security.UserDetailsImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -27,7 +29,7 @@
@RequiredArgsConstructor
public class TossPaymentController {

private final TossPaymentService tossPaymentService;
private final PaymentService paymentService;

@PostMapping("/request")
@Operation(summary = "토스페이 결제 검증/확인 요청", description = "결제정보에 대한 검증/확인 이후 필요한 값들을 반환합니다.")
Expand All @@ -37,7 +39,7 @@ public RestResponse<TossPaymentInitialResponseDto> requestTossPayment(
) {
TossPaymentRequestValidator.validate(requestDto);
TossPayment payment = TossPaymentMapper.INSTANCE.fromTossPaymentInitialRequestDto(userDetails.getUser(), requestDto);
TossPaymentInitialResponseDto responseDto = tossPaymentService.requestTossPayment(payment);
TossPaymentInitialResponseDto responseDto = paymentService.requestTossPayment(payment);
return RestResponse.success(responseDto);
}

Expand All @@ -47,9 +49,18 @@ public RestResponse<TossPaymentSuccessResponseDto> requestFinalTossPayment(
@Schema(description = "토스 결제고유번호") @RequestParam String paymentKey,
@Schema(description = "서버 주분고유번호") @RequestParam Long orderId,
@Schema(description = "결제금액") @RequestParam Long amount
) {
TossPaymentSuccessResponseDto responseDto = tossPaymentService.requestFinalTossPayment(paymentKey, orderId, amount);
return RestResponse.success(responseDto);
) throws InterruptedException {
AtomicReference<TossPaymentSuccessResponseDto> responseDtoHolder = new AtomicReference<>();
Semaphore semaphore = new Semaphore(0);

paymentService.requestFinalTossPayment(paymentKey, orderId, amount, responseDto -> {
responseDtoHolder.set(responseDto);
semaphore.release();
});

semaphore.acquire();

return RestResponse.success(responseDtoHolder.get());
}

@GetMapping("/fail")
Expand All @@ -59,7 +70,7 @@ public RestResponse<TossPaymentFailResponseDto> requestFail(
@Schema(description = "에러 메세지") @RequestParam String errorMsg,
@Schema(description = "서버 주문고유번호") @RequestParam Long orderId
) {
TossPaymentFailResponseDto responseDto = tossPaymentService.requestFail(errorCode, errorMsg, orderId);
TossPaymentFailResponseDto responseDto = paymentService.requestFail(errorCode, errorMsg, orderId);
return RestResponse.success(responseDto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,25 @@

import bc1.gream.domain.payment.toss.dto.response.TossPaymentFailResponseDto;
import bc1.gream.domain.payment.toss.dto.response.TossPaymentInitialResponseDto;
import bc1.gream.domain.payment.toss.dto.response.TossPaymentSuccessResponseDto;
import bc1.gream.domain.payment.toss.entity.TossPayment;
import bc1.gream.domain.payment.toss.mapper.TossPaymentMapper;
import bc1.gream.domain.payment.toss.repository.TossPaymentRepository;
import bc1.gream.domain.payment.toss.service.event.TossPaymentSuccessCallback;
import bc1.gream.domain.payment.toss.service.event.TossPaymentSuccessEvent;
import bc1.gream.global.common.ResultCase;
import bc1.gream.global.exception.GlobalException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import net.minidev.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
public class TossPaymentService {
public class PaymentService {

private final TossPaymentRepository tossPaymentRepository;
private final ApplicationEventPublisher eventPublisher;

@Value("${payment.toss.test_client_api_key}")
private String testClientApiKey;
Expand Down Expand Up @@ -57,9 +52,16 @@ public TossPaymentInitialResponseDto requestTossPayment(TossPayment tossPayment)
* @return 토스페이 최종요청 결과
*/
@Transactional
public TossPaymentSuccessResponseDto requestFinalTossPayment(String paymentKey, Long orderId, Long amount) {
public void requestFinalTossPayment(String paymentKey, Long orderId, Long amount, TossPaymentSuccessCallback callback) {
this.verifyRequest(paymentKey, orderId, amount);
return this.sendFinalRequestToTossApi(paymentKey, orderId, amount);
eventPublisher.publishEvent(new TossPaymentSuccessEvent(
this,
paymentKey,
orderId,
amount,
successUrl,
testSecretApiKey,
callback)); // Provide method reference to the event listener
}

/**
Expand Down Expand Up @@ -88,7 +90,7 @@ public TossPaymentFailResponseDto requestFail(String errorCode, String errorMsg,
@Transactional
void verifyRequest(String paymentKey, Long orderId, Long amount) {
// 주문아이디 일치 검증
TossPayment tossPayment = findBy(orderId);
TossPayment tossPayment = this.findBy(orderId);
// 결제금액 일치 검증
if (tossPayment.getAmount().equals(amount)) {
tossPayment.setPaymentKey(paymentKey);
Expand All @@ -97,37 +99,6 @@ void verifyRequest(String paymentKey, Long orderId, Long amount) {
throw new GlobalException(ResultCase.UNMATCHED_PAYMENT_AMOUNT);
}

/**
* 토스페이API에 POST요청
*
* @param paymentKey 토스 결제고유번호
* @param orderId 서버 주문고유번호
* @param amount 결제액
* @return 토스페이API 요청결과
*/
@Transactional
TossPaymentSuccessResponseDto sendFinalRequestToTossApi(String paymentKey, Long orderId, Long amount) {
RestTemplate rest = new RestTemplate();

HttpHeaders headers = new HttpHeaders();

testSecretApiKey = testSecretApiKey + ":";
String encodedAuth = new String(Base64.getEncoder().encode(testSecretApiKey.getBytes(StandardCharsets.UTF_8)));
headers.setBasicAuth(encodedAuth);
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

JSONObject param = new JSONObject();
param.put("orderId", orderId);
param.put("amount", amount);

return rest.postForObject(
successUrl + paymentKey,
new HttpEntity<>(param, headers),
TossPaymentSuccessResponseDto.class
);
}

@Transactional(readOnly = true)
TossPayment findBy(Long orderId) {
return tossPaymentRepository.findByOrderId(orderId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package bc1.gream.domain.payment.toss.service.event;

import bc1.gream.domain.payment.toss.dto.response.TossPaymentSuccessResponseDto;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import net.minidev.json.JSONObject;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.web.client.RestTemplate;

@Component
public class TossPaymentEventListener {

@Async
@TransactionalEventListener
public void handleTossPaymentSuccess(TossPaymentSuccessEvent event) {
RestTemplate rest = new RestTemplate();
HttpHeaders headers = new HttpHeaders();

String testSecretApiKey = event.getTestSecretApiKey() + ":";
String encodedAuth = new String(Base64.getEncoder().encode(testSecretApiKey.getBytes(StandardCharsets.UTF_8)));
headers.setBasicAuth(encodedAuth);
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

JSONObject param = new JSONObject();
param.put("orderId", event.getOrderId());
param.put("amount", event.getAmount());

TossPaymentSuccessResponseDto responseDto = rest.postForObject(
event.getSuccessUrl() + event.getPaymentKey(),
new HttpEntity<>(param, headers),
TossPaymentSuccessResponseDto.class
);

// Use the functional interface to pass the result back to TossPaymentController
event.getCallback().handle(responseDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package bc1.gream.domain.payment.toss.service.event;

import bc1.gream.domain.payment.toss.dto.response.TossPaymentSuccessResponseDto;

@FunctionalInterface
public interface TossPaymentSuccessCallback {

void handle(TossPaymentSuccessResponseDto responseDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package bc1.gream.domain.payment.toss.service.event;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class TossPaymentSuccessEvent extends ApplicationEvent {

private final String paymentKey;
private final Long orderId;
private final Long amount;
private final String successUrl;
private final String testSecretApiKey;

private final TossPaymentSuccessCallback callback;

public TossPaymentSuccessEvent(Object source, String paymentKey, Long orderId, Long amount, String successUrl, String testSecretApiKey,
TossPaymentSuccessCallback callback) {
super(source);
this.paymentKey = paymentKey;
this.orderId = orderId;
this.amount = amount;
this.successUrl = successUrl;
this.testSecretApiKey = testSecretApiKey;
this.callback = callback;
}
}
3 changes: 2 additions & 1 deletion src/main/java/bc1/gream/global/common/ResultCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public enum ResultCase {
// 시스템 에러 500
SYSTEM_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5002, "알 수 없는 에러가 발생했습니다."),
// 잘못된 도메인 정렬값 입력 400
INVALID_ORDER_CRITERIA(HttpStatus.BAD_REQUEST, 5001, "유효하지 않은 도메인 정렬값"),
INVALID_ORDER_CRITERIA(HttpStatus.BAD_REQUEST, 5003, "유효하지 않은 도메인 정렬값"),
THREAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5004, "유효하지 않은 도메인 정렬값"),

// 쿠폰 6000번대
// 쿠폰이 존재하지 않을 때 404
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/bc1/gream/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package bc1.gream.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@Configuration
public class AsyncConfig {

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public ResponseEntity<RestResponse<List<InvalidInputResponseDto>>> handlerValida
return RestResponse.error(ResultCase.INVALID_INPUT, invalidInputList);
}

/**
* Thread 오류 대한 핸들러
*
* @param ex Thread 오류에 따른 InterruptedException
* @return Thread 에러케이스와 에러리스폰스
*/
@ExceptionHandler(InterruptedException.class)
public ResponseEntity<RestResponse<ErrorResponseDto>> handlerInterruptedException(InterruptedException ex) {
return RestResponse.error(ResultCase.THREAD_ERROR, new ErrorResponseDto());
}

/**
* Business 오류 발생에 대한 핸들러
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package bc1.gream.domain.payment.toss.controller;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
Expand All @@ -17,7 +19,8 @@
import bc1.gream.domain.payment.toss.entity.OrderName;
import bc1.gream.domain.payment.toss.entity.PayType;
import bc1.gream.domain.payment.toss.entity.TossPayment;
import bc1.gream.domain.payment.toss.service.TossPaymentService;
import bc1.gream.domain.payment.toss.service.PaymentService;
import bc1.gream.domain.payment.toss.service.event.TossPaymentSuccessCallback;
import bc1.gream.global.security.WithMockCustomUser;
import bc1.gream.test.UserTest;
import com.fasterxml.jackson.core.type.TypeReference;
Expand Down Expand Up @@ -46,7 +49,7 @@ class TossPaymentControllerTest {
@Autowired
private ObjectMapper objectMapper;
@MockBean
private TossPaymentService tossPaymentService;
private PaymentService paymentService;

@BeforeEach
void setUp() {
Expand Down Expand Up @@ -74,7 +77,7 @@ void setUp() {
.userNickname(UserTest.TEST_USER_NICKNAME)
.paymentHasSuccess(true)
.build();
given(tossPaymentService.requestTossPayment(any(TossPayment.class)))
given(paymentService.requestTossPayment(any(TossPayment.class)))
.willReturn(responseDto);

// WHEN
Expand Down Expand Up @@ -132,12 +135,17 @@ void setUp() {
.card(expectedPaymentCard)
.type("NORMAL")
.build();
doAnswer(invocation -> {
TossPaymentSuccessCallback callback = invocation.getArgument(3);
callback.handle(responseDto);
return null;
}).when(paymentService).requestFinalTossPayment(
eq(paymentKey),
eq(orderId),
eq(amount),
any(TossPaymentSuccessCallback.class));

// WHEN
when(tossPaymentService.requestFinalTossPayment(paymentKey, orderId, amount))
.thenReturn(responseDto);

// THEN
// WHEN, THEN
mockMvc.perform(
get(url)
.param("paymentKey", paymentKey)
Expand Down Expand Up @@ -183,7 +191,7 @@ void setUp() {
.build();

// WHEN
when(tossPaymentService.requestFail(errorCode, errorMsg, orderId))
when(paymentService.requestFail(errorCode, errorMsg, orderId))
.thenReturn(responseDto);

// THEN
Expand Down

0 comments on commit a841c29

Please sign in to comment.