위치추적모듈 프로젝트에서 로그인, 결제 기능을 다루다 보니, 외부 API와 통신할 일이 많아졌고 트랜잭션 기능이 없는 외부 API 통신에 대해서는 어떻게 다루는 게 맞을까에 대한 고민을 하였다. 이번 게시글에서는 이에 대한 나의 생각을 기록해두려고 한다.
@RequiredArgsConstructor
@Service
public class KakaopayPaymentGatewayClient extends PaymentGatewayClient {
private final PaymentGatewayProperties paymentGatewayProperties;
private final RestTemplate restTemplate;
private final HttpSession httpSession;
private static final PaymentGatewayType PAYMENT_GATEWAY_TYPE = PaymentGatewayType.PG_KAKAOPAY;
@Override
protected PaymentGatewayType getSupportedPaymentGateway() {
return PAYMENT_GATEWAY_TYPE;
}
public KakaopayReadyResponse Ready(KakaopayReadyRequest req) {
try {
HttpEntity<Map<String, Object>> request = createReadyRequest(req);
KakaopayReadyResponse kakaopayReadyResponse = restTemplate.postForObject(
KAKAOPAY_READY_URL,
request,
KakaopayReadyResponse.class
);
handleResponse(kakaopayReadyResponse);
saveTid(req.getPartnerOrderId(), kakaopayReadyResponse.getTid());
return kakaopayReadyResponse;
} catch (Exception e) {
throw new KakaopayPaymentGatewayReadyFailureException(e);
}
}
private void saveTid(String partnerOrderId, String tid) {
httpSession.setAttribute(partnerOrderId, tid);
}
private HttpEntity<Map<String, Object>> createReadyRequest(KakaopayReadyRequest req) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("cid", CID.equalsIgnoreCase(req.getCid()) ? paymentGatewayProperties.getKakaopay().getCid() : paymentGatewayProperties.getKakaopay().getCcid());
parameters.put("partner_order_id", req.getPartnerOrderId());
parameters.put("partner_user_id", req.getPartnerUserId());
parameters.put("item_name", req.getItemName());
parameters.put("quantity", req.getQuantity());
parameters.put("total_amount", req.getTotalAmount());
parameters.put("tax_free_amount", req.getTaxFreeAmount());
parameters.put("approval_url", req.getApprovalUrl());
parameters.put("cancel_url", req.getCancelUrl());
parameters.put("fail_url", req.getFailUrl());
HttpHeaders headers = getHeaders();
return new HttpEntity<>(parameters, headers);
}
}
결제 기능을 위해 구현한 카카오페이 api 중 일부이다. 코드에서 볼 수 있듯이, 어떤 Exception이 발생해도 적절한 RuntimeException으로 반환해서 오류를 던진다.
@RequiredArgsConstructor
@Service
@Slf4j
@EnableConfigurationProperties(PaymentGatewayProperties.class)
public class KakaopayPaymentGatewayClient extends PaymentGatewayClient {
private final PaymentGatewayProperties paymentGatewayProperties;
private final RestTemplate restTemplate;
private final HttpSession httpSession;
private static final PaymentGatewayType PAYMENT_GATEWAY_TYPE = PaymentGatewayType.PG_KAKAOPAY;
@Override
protected PaymentGatewayType getSupportedPaymentGateway() {
return PAYMENT_GATEWAY_TYPE;
}
public ClientResponse<KakaopayReadyResponse> Ready(KakaopayReadyRequest req) {
try {
HttpEntity<Map<String, Object>> request = createReadyRequest(req);
KakaopayReadyResponse kakaopayReadyResponse = restTemplate.postForObject(
KAKAOPAY_READY_URL,
request,
KakaopayReadyResponse.class
);
handleResponse(kakaopayReadyResponse);
saveTid(req.getPartnerOrderId(), kakaopayReadyResponse.getTid());
return ClientResponse.success(kakaopayReadyResponse);
} catch (Exception e) {
log.error("Failed to process Kakaopay Ready for User ID: {}", req.getPartnerUserId(), e);
return ClientResponse.failure();
}
}
private void saveTid(String partnerOrderId, String tid) {
httpSession.setAttribute(partnerOrderId, tid);
}
private HttpEntity<Map<String, Object>> createReadyRequest(KakaopayReadyRequest req) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("cid", CID.equalsIgnoreCase(req.getCid()) ? paymentGatewayProperties.getKakaopay().getCid() : paymentGatewayProperties.getKakaopay().getCcid());
parameters.put("partner_order_id", req.getPartnerOrderId());
parameters.put("partner_user_id", req.getPartnerUserId());
parameters.put("item_name", req.getItemName());
parameters.put("quantity", req.getQuantity());
parameters.put("total_amount", req.getTotalAmount());
parameters.put("tax_free_amount", req.getTaxFreeAmount());
parameters.put("approval_url", req.getApprovalUrl());
parameters.put("cancel_url", req.getCancelUrl());
parameters.put("fail_url", req.getFailUrl());
HttpHeaders headers = getHeaders();
return new HttpEntity<>(parameters, headers);
}
프로젝트를 진행중에 외부 api 통신 실패 후 예외를 던지는 것이 적절하지 않다고 판단되어, 예외를 던지지 않도록 수정한 코드이다. api통신이 성공하였을 경우에는 성공 정보를 반환하고, api 통신이 실패했을 경우, 로그만 기록하고 실패 정보를 반환한다.
코드를 수정한 내용의 결론은 간단하다. API 통신이 실패했을 경우 RuntimeException
을 던지지 않고 성공 또는 실패 정보를 반환해주는 것이다. 이렇게 코드를 수정한 이유는 다음과 같다.
CustomRuntimeException
의 정보를 알도록 하는 것이 적절한가? 다른 개발자가 이 코드를 사용한다면 불편할 가능성이 크다.이처럼 비즈니스 로직에서 예외를 다루는 방법은 신중해야 한다. 특히, 다른 계층에서 발생하는 RuntimeException
이 비즈니스 로직에서 롤백을 유발할 수 있다는 점에서 문제가 될 수 있다. 비즈니스 로직이 실패했을 때 롤백 여부를 선택할 수 있도록 하는 것이 더 유연한 설계일 수 있다. 이에 대한 자세한 내용은 우아한 형제들 기술 블로그에서 참고할 수 있다.
또한, "모던 자바 인 액션"에서는 Optional<T>
를 활용하여 예외를 발생시키지 않고도 연산의 성공 여부를 표현할 수 있다고 언급한다. 즉, 호출자는 메서드 호출 결과로 빈 Optional
이 반환되는지를 확인함으로써 연산의 성공 또는 실패를 알 수 있다. 이러한 접근 방식은 예외를 전역적으로 던지는 대신, 필요에 따라 지역적으로만 예외를 사용하는 방법을 고려할 수 있다는 점에서 장점이 있다. 이처럼 Optional
을 사용하는 것은 예외를 처리하는 또 다른 실용적인 방법으로, 다른 컴포넌트에 영향을 미치지 않도록 설계할 수 있다.
물론 현시점에서 나의 생각일 뿐이고 언제나 바뀔 수 있다.