[생각정리] 외부 api 통신 실패 후 예외를 던지는게 맞을까?

jeyong·2024년 5월 14일
0

공부 / 생각 정리  

목록 보기
72/120
post-custom-banner

위치추적모듈 프로젝트에서 로그인, 결제 기능을 다루다 보니, 외부 API와 통신할 일이 많아졌고 트랜잭션 기능이 없는 외부 API 통신에 대해서는 어떻게 다루는 게 맞을까에 대한 고민을 하였다. 이번 게시글에서는 이에 대한 나의 생각을 기록해두려고 한다.

1. 기존 코드

@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으로 반환해서 오류를 던진다.

2. 바뀐 코드

@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 통신이 실패했을 경우, 로그만 기록하고 실패 정보를 반환한다.

3. 생각 정리

코드를 수정한 내용의 결론은 간단하다. API 통신이 실패했을 경우 RuntimeException을 던지지 않고 성공 또는 실패 정보를 반환해주는 것이다. 이렇게 코드를 수정한 이유는 다음과 같다.

  • API 통신이 실패했다고 재시도하는 것이 항상 적절한가? 혹은 비즈니스 로직이 실패했다고 무조건 예외를 던지는 것이 맞는가?
  • API 통신을 사용하는 비즈니스 로직에서 CustomRuntimeException의 정보를 알도록 하는 것이 적절한가? 다른 개발자가 이 코드를 사용한다면 불편할 가능성이 크다.
  • API 통신의 문제가 발생해도 비즈니스 로직을 계속 실행하고 싶은 경우에는 어떻게 해야 할까?
  • API 통신의 실패 원인에 대해 비즈니스 로직이나 클라이언트가 정말로 궁금해할까? 대부분은 성공 또는 실패 여부만 관심을 가질 것이다.

이처럼 비즈니스 로직에서 예외를 다루는 방법은 신중해야 한다. 특히, 다른 계층에서 발생하는 RuntimeException이 비즈니스 로직에서 롤백을 유발할 수 있다는 점에서 문제가 될 수 있다. 비즈니스 로직이 실패했을 때 롤백 여부를 선택할 수 있도록 하는 것이 더 유연한 설계일 수 있다. 이에 대한 자세한 내용은 우아한 형제들 기술 블로그에서 참고할 수 있다.

또한, "모던 자바 인 액션"에서는 Optional<T>를 활용하여 예외를 발생시키지 않고도 연산의 성공 여부를 표현할 수 있다고 언급한다. 즉, 호출자는 메서드 호출 결과로 빈 Optional이 반환되는지를 확인함으로써 연산의 성공 또는 실패를 알 수 있다. 이러한 접근 방식은 예외를 전역적으로 던지는 대신, 필요에 따라 지역적으로만 예외를 사용하는 방법을 고려할 수 있다는 점에서 장점이 있다. 이처럼 Optional을 사용하는 것은 예외를 처리하는 또 다른 실용적인 방법으로, 다른 컴포넌트에 영향을 미치지 않도록 설계할 수 있다.

물론 현시점에서 나의 생각일 뿐이고 언제나 바뀔 수 있다.

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.
post-custom-banner

0개의 댓글