카테고리:

업데이트:

1. 들어가기

이전 포스트에서는 enum을 이용해 분리한 서비스와 컨트롤러를 연결해보았습니다.

이번에는 Feign Client를 통해 비즈니스 로직에 포함되어 있는 HTTP 통신 로직을 분리해보겠습니다.

2. Feign Client 의존성 추가

build.gradle 파일을 열어서 의존성을 추가합시다.

  ext {
    set('springCloudVersion', "2022.0.3")
  }

  dependencies {
    implementation "org.springframework.cloud:spring-cloud-starter-openfeign"
  }

  ...
  dependencyManagement {
      imports {
          mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
      }
  }

Spring Cloud Version 확인

https://spring.io/projects/spring-cloud

3. Feign Client 인터페이스 생성

application.yml 파일을 열어서 토스페이와 카카오페이의 엔드포인트를 세팅합니다.

# application.yml
  easy-pay:
    toss:
      url: https://pay.toss.im/api/v2
    kakao:
      url: https://kapi.kakao.com/v1/payment

그리고 간편결제 별로 Client 인터페이스를 생성합니다.

// TossPayClient.java
  @FeignClient(value = "toss-client", url = "${easy-pay.toss.url}")
  public interface TossPayClient {

    @PostMapping(value = "/payments", consumes = MediaType.APPLICATION_JSON_VALUE)
    JSONObject request(@RequestBody TossPayRequest payRequest);

    @PostMapping(value = "/execute", consumes = MediaType.APPLICATION_JSON_VALUE)
    JSONObject approve(@RequestBody TossPayRequest payRequest);
  }
// KakaoPayClient.java
  @FeignClient(value = "kakao-client", url = "${easy-pay.kakao.url}")
  public interface KakaoPayClient {

    @PostMapping(value = "/ready", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    JSONObject request(@RequestHeader("Authorization") String authorization, @RequestBody KakaoPayRequest request);

    @PostMapping(value = "/approve", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    JSONObject approve(@RequestHeader("Authorization") String authorization, @RequestBody KakaoPayRequest request);
  }

한 가지 주의할 점은 카카오페이의 데이터는 Camel Case가 아닌 Snake Case입니다.

하지만, Java는 네이밍 룰로 Camel Case를 사용하는데요.

이럴 땐 Feign 요청 시 Snake Case로 변환하여 요청하도록 하면 됩니다.

이를 구현하기 위해서는 다음과 같이 작성할 수 있습니다.

// KakaoPayRequest.java
  @Builder
  public class KakaoPayRequest {

      /** 결제 승인 요청 토큰 */
      @FormProperty("pg_token")
      private String pgToken;

      ...
  }
// FeignSnakeCaseConfiguration.java
  public class FeignSnakeCaseConfiguration {

    // Feign 요청 시 FormProperty 값으로 encode
    @Bean
    Encoder feignFormEncoder() {
      return new FormEncoder();
    }
  }
  configuration = FeignSnakeCaseConfiguration.class

종합하면 다음과 같습니다.

// KakaoPayClient.java
  @FeignClient(value = "kakao-client", url = "${easy-pay.kakao.url}", configuration = FeignSnakeCaseConfiguration.class)
  public interface KakaoPayClient {

    @PostMapping(value = "/ready", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    JSONObject request(@RequestHeader("Authorization") String authorization, @RequestBody KakaoPayRequest request);

    @PostMapping(value = "/approve", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    JSONObject approve(@RequestHeader("Authorization") String authorization, @RequestBody KakaoPayRequest request);
  }

4. HTTP 통신 로직 분리

Feign Client 인터페이스도 생성했으니 비즈니스 로직에서 HTTP 통신 로직을 분리해봅시다.

기존 코드입니다.

  @Override
  public JSONObject request(EasyPay easyPay) {
    try {
      // Connection
      URL url = new URL("https://pay.toss.im/api/v2/payments");
      URLConnection connection = url.openConnection();
      connection.addRequestProperty("Content-Type", "application/json");
      connection.setDoOutput(true);
      connection.setDoInput(true);

      // 데이터 생성
      JSONObject jsonBody = new JSONObject();
      jsonBody.put("orderNo", createOrderNo());
      jsonBody.put("amount", easyPay.getProductPrice());
      jsonBody.put("amountTaxFree", "0");
      jsonBody.put("productDesc", easyPay.getProductName());
      jsonBody.put("apiKey", apiKey);
      jsonBody.put("autoExecute", false);
      jsonBody.put("retUrl", frontUrl + "/easy-pay/order-check/toss");
      jsonBody.put("retCancelUrl", frontUrl + "/easy-pay/close");

      // 요청
      BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream());

      bos.write(jsonBody.toJSONString().getBytes(StandardCharsets.UTF_8));
      bos.flush();
      bos.close();

      // JSONObject에 매핑
      BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
      JSONParser jsonParser = new JSONParser();
      JSONObject result = (JSONObject) jsonParser.parse(br.readLine());

      br.close();
      return result;
    } catch (Exception e) {
      return null;
    }
  }

HTTP 통신 로직을 분리하면 다음과 같습니다.

  @Override
  public JSONObject request(EasyPay easyPay) {
    TossPayRequest data = TossPayRequest.builder()
                                        .orderNo(createOrderNo())
                                        .amount(easyPay.getProductPrice())
                                        .amountTaxFree("0")
                                        .productDesc(easyPay.getProductName())
                                        .apiKey(apiKey)
                                        .autoExecute(Boolean.toString(false))
                                        .retUrl(frontUrl + "/easy-pay/order-check/toss")
                                        .retCancelUrl(frontUrl + "/easy-pay/close")
                                        .build();

    return tossPayClient.request(data);
  }

비즈니스 로직이 확실히 깔끔해진 것을 볼 수 있습니다.

5. 정리

여기까지 기존 코드를 가독성과 확장성을 고려해 리팩토링 해보았습니다.

만약, 페이코 간편결제를 추가하려면 enum에 PAYCO(PaycoService.class)를 추가하고

EasyPayService 인터페이스를 구현하는 PaycoService를 작성하면 편리하게 페이코를 확장할 수 있습니다.


🚩 리팩토링에 도움 주신 장지영 매니저님, 이재렬 매니저님 감사합니다. 😊

            
              📕 개인 기록용 블로그입니다.
              😊 오타나 잘못된 정보가 있을 경우 댓글이나 메일로 말씀해주시면 바로 수정하겠습니다! 😊
          

댓글남기기