Spring Feign을 활용한 동적 헤더 설정하기 - nCloud SMS와 카카오톡 알림톡 API 사용

jonghyun.log·2023년 10월 20일
0
post-custom-banner

Spring OpenFeign 을 사용하여 헤더 설정에 관해 삽질한 과정을 기록한 글입니다.

이번에 문자, 카카오톡 발신 기능을 만들게 되었다.
이를 위한 기술 스택으로 네이버클라우드(ncloud)에서 제공하는 알림톡, SMS api
Spring OpenFeign 라이브러리를 채택하게 되었다.

Feign 이란?

  • Feign 은 Netflix 에서 개발된 Http client binder 입니다.
  • Feign 을 사용하면 웹 서비스 클라이언트를 보다 쉽게 작성할 수 있습니다.
  • Feign 을 사용하기 위해서는 interface 를 작성하고 annotation 을 선언 하기만 하면됩니다.
    • 마치 Spring Data JPA 에서 실제 쿼리를 작성하지 않고 Interface 만 지정하여 쿼리실행 구현체를 자동으로 만들어주는 것과 유사합니다.
  • 설명보다는 소스코드를 한번 보고, RestTemplate 을 만들어 보셨던 분들은 많은 코드의 축소를 느끼게 되실겁니다.(잘 모르겠다 하시면, 동일한 호출을 RestTemplate 으로 만들어서 보시면 아시게 될거라 생각합니다.)
    출처 : https://techblog.woowahan.com/2630/ , https://techblog.woowahan.com/2657/

feign에 대한 자세한 설명은 넘어가고 간략하게 설명하면 위에 나와있는대로
외부 api 호출에 관한 메서드를 인터페이스로 선언만 하면 메서드 선언문을 보고 그에 맞춰서
Spring Data Jpa 처럼 구현체를 만들어준다.

이를 위해 간단한 코드 예시를 통해 feign이 무엇인지 알아보자.

코드 간단 예시

RestTemplate 으로 구현한 카카오 로그인

예를들어 restTemplate 으로 구현한 카카오 로그인 코드를 보자.

public String getKakaoToken(String code, String redirectUrl) throws ParseException {
        // 인가코드로 토큰받기
        String host = "https://kauth.kakao.com/oauth/token";

        RestTemplate rt = new RestTemplate();
        rt.setRequestFactory(new HttpComponentsClientHttpRequestFactory()); // restTemplate 에러 메세지 확인 설정

        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/x-www-form-urlencoded");

        MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
        param.add("grant_type", "authorization_code");
        param.add("client_id", clientId);
        param.add("redirect_uri", redirectUrl); //로컬, 개발, 운영 서버 테스트에서 계속 변경할 수 있게 Redirect Url 파라미터로 받아서 적용
        param.add("code", code);
        param.add("client_secret", client_secret);

        HttpEntity<MultiValueMap<String, String>> req = new HttpEntity<>(param, headers);
        ResponseEntity<String> res = rt.exchange(host,
                HttpMethod.POST,
                req,
                String.class);

        JSONParser jsonParser = new JSONParser();
        JSONObject parse = (JSONObject) jsonParser.parse(res.getBody());

        return (String) parse.get("access_token");
    }

직접 경로, 헤더를 세팅하고
api 응답으로 받아온 json 데이터를 객체로 파싱하는 작업까지 해줘야한다.
하지만! feign 으로 구현하면 이렇게 일일히 다 구현하지 않아도 된다.

Spring OpenFeign 으로 구현한 카카오 로그인

@FeignClient(value = "auth-kakao", url = "https://kauth.kakao.com/")
public interface KakaoAuthClient {
    @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    KakaoTokenResponse generateToken(@RequestParam(name = "grant_type") String grantType,
                                     @RequestParam(name = "client_id") String clientId,
                                     @RequestParam(name = "redirect_uri") String redirectUri,
                                     @RequestParam(name = "code") String code,
                                     @RequestParam(name = "client_secret") String clientSecret);
}

feign 을 사용하면 요청 메서드의 내부를 구현할 필요없이 interface로 메서드 선언만 해주면 된다.

feign 사용을 위한 자세한 내용과 설명은 위의 우아한 형제들 기술블로그Spring OpenFeign 공식 문서를 참고하시기 바랍니다.

FeignClient 메서드에 헤더 추가하기

Ncloud(네이버 클라우드) 에서 제공하는 BizMessage - SMS 발신, 알림톡, 친구톡(카카오톡) 발신 API를 사용하려면
공통적으로 헤더에 발급받은 accessKey, secretKey 를 가지고 암호화 한 값을 헤더에 포함시켜서 api를 보내야 한다.

출처 : https://api.ncloud-docs.com/docs/common-ncpapi

이 글은 feign 을 사용하며 삽질한 내용에 집중해서 작성되었으므로
네이버 클라우드 API에 관한 자세한 설명은 하지않겠습니다.
자세한 내용은 네이버 클라우드 API 문서를 참고하시기 바랍니다.

이것을 위해 FeignClient 인터페이스에서 api 호출 메서드에 헤더를 추가해주는 작업을 해줘야한다.

위의 우아한 형제들 기술블로그에 따르면 별도의 config 클래스를 정의하고
@Bean으로 등록된 RequestInterCeptor 를 통해 request Header 에 값을 추가하는게 가능하다고 한다.

feign 인터페이스에 정의된 메서드 별로 헤더를 추가하는 방법도 있지만,
이 글에서는 인터페이스 클래스에 정의된 모든 메서드에 공통으로 적용되는 방법을 사용했습니다.

이를 위해 FeignClient 의 헤더에 값을 추가해줄 config 클래스를 정의하고 api 문서에 정의된 대로 암호화된 값을 헤더에 추가하는 코드를 작성한다.

@Slf4j
public class BizMsgApiHeaderFeignConfiguration {

    @Value("${sms.request.signature-url}")
    private String SMS_SIGNATURE_URL;

    @Value("${sms.request.access-key}")
    private String SMS_API_ACCESS_KEY;

    @Value("${sms.request.secret-key}")
    private String SMS_API_SECRET_KEY;

    @Value("${sms.request.encoding-algorithm}")
    private String ENCODING_ALGORITHM;


    @Bean
    public RequestInterceptor requestInterceptor() {
					// 여기서 헤더에 값 추가
        return requestTemplate -> {
            requestTemplate.header("Content-Type", "application/json; charset=utf-8");
            requestTemplate.header("x-ncp-iam-access-key", SMS_API_ACCESS_KEY);
            requestTemplate.header("x-ncp-apigw-timestamp", utcMilliSeconds().toString());
            requestTemplate.header("x-ncp-apigw-signature-v2", getSignature());
        };
    }

    private Long utcMilliSeconds() {
        return Instant.now().toEpochMilli();
    }

    private String getSignature() {
        try {
            return makeSignature();
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
			throw new BizMsgApiMethodNotFoundException(ErrorCode.FEIGN_API_NOT_FOUND, requestTemplate.url());
        }
    }

    private String makeSignature() throws NoSuchAlgorithmException, InvalidKeyException {
        String space = " ";                    // one space
        String newLine = "\n";                    // new line
        String method = "POST";                    // method

        String message = method +
                space +
                SMS_SIGNATURE_URL +
                newLine +
                utcMilliSeconds() +
                newLine +
                SMS_API_ACCESS_KEY;

        SecretKeySpec signingKey = new SecretKeySpec(SMS_API_SECRET_KEY.getBytes(StandardCharsets.UTF_8), ENCODING_ALGORITHM);
        Mac mac = Mac.getInstance(ENCODING_ALGORITHM);
        mac.init(signingKey);

        byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));

        return Base64.encodeBase64String(rawHmac);
    }

}

ncloud api 문서에 나와있는 헤더 암호화 예제 코드를 조금 수정해서 사용한 코드입니다.

그리고 위의 코드를 @FeignClient 에너테이션의 인자로 등록하여 메서드 인터페이스에 전달하면 된다.

@FeignClient(value = "api-SMS", url = "https://sens.apigw.ntruss.com/sms/v2/services/{발급 받은 id 값을 넣어주세요}", configuration = BizMsgApiHeaderFeignConfiguration.class)
public interface SmsApiClient {

    @PostMapping("/messages")
    void sendMessage(SmsSendApiRequest smsSendApiRequest);
}

이 코드를 사용해서 요청을 보내면

이렇게 로그를 통해 SMS(문자) 발송 api 요청을 보내는데 성공했음을 알 수 있다.
위의 빨간색으로 표시한 부분이 request header로 추가된 값이며
그 밑의 {”type” : “SMS”, …} 부분이 request body 에 들어가는 값이다.


(feign을 사용한 메시지 발신에 성공했다.😀)

FeignClient 메서드에 따라 헤더값을 동적으로 추가하기

방금전에는 feign 을 통해 SMS(문자) 발송 기능을 위한 헤더를 추가해 보았다.
하지만, 개발을 하다보니 이번에는 문자만이 아닌 카카오톡도 발송하는 기능을 추가하게 되었다.

네이버 클라우드 API 문서에 따르면
SMS 기능과 카카오톡 메시지 발송 기능 모두 동일한 헤더 암호화 방식을 요구하지만,

api 별로 헤더 암호화에 들어가는 고유한 값이 다르기에
암호화에 들어가는 값을 api에 맞는 값으로 넣어서 요청을 보내줄 필요가 있다.

위 사진의 signature-v2 암호화 값에 들어가는 값이 api 마다 다르다.

그래서, 헤더를 설정할 때 api에 맞는 값으로 암호화를 하는 과정이 필요하다.
단순히 생각해보면 SMS, 카카오톡 메시지 api 각각에 설정파일을 추가해서 처리하는 방법도 있다.

하지만, 그렇게 하면 단순히 암호화에 필요한 문자열 값만 바뀌고
나머지 로직은 동일하므로 중복되는 코드가 많아진다는 문제가 생겼다.

이를 위해 고민을 해본결과 위의 헤더 암호화 config 코드 중

@Bean
    public RequestInterceptor requestInterceptor() {
					// 여기서 헤더에 값 추가
        return requestTemplate -> {
            requestTemplate.header("Content-Type", "application/json; charset=utf-8");
            requestTemplate.header("x-ncp-iam-access-key", SMS_API_ACCESS_KEY);
            requestTemplate.header("x-ncp-apigw-timestamp", utcMilliSeconds().toString());
            requestTemplate.header("x-ncp-apigw-signature-v2", getSignature());
        };
    }

바로 이 부분의 requestTemplate 부분에 어떤 값들이 담겨져오는지 확인해보았다.

methodMetadata 안의 configKey 속성에 어떤 메서드에서 호출 되었는지에 관한 정보가 문자열로 담겨있었다.
이것에 착안해서, 헤더에 값을 등록하기전에 메서드의 정보에 따라 다른값을 암호화해서 헤더에 값을 추가하도록 변경했다.

@Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            String signatureUrl = indentifyMethod(requestTemplate);
            addValueHeader(requestTemplate, signatureUrl);
        };
    }

    private String indentifyMethod(RequestTemplate requestTemplate) {
        String configKey = requestTemplate.methodMetadata().configKey();
        if (configKey.contains(ALIM_TALK_IDENTIFIER)) {
            return ALIMTALK_SIGNATURE_URL;
        }
        if (configKey.contains(SMS_IDENTIFIER)) {
            return SMS_SIGNATURE_URL;
        }
        throw new IllegalArgumentException("Unsupported API: " + requestTemplate.url());
    }

    private void addValueHeader(RequestTemplate requestTemplate, String signatureUrl) {
        requestTemplate.header("Content-Type", "application/json; charset=utf-8");
        requestTemplate.header("x-ncp-iam-access-key", BIZ_MESSAGE_API_ACCESS_KEY);
        requestTemplate.header("x-ncp-apigw-timestamp", utcMilliSeconds().toString());
        requestTemplate.header("x-ncp-apigw-signature-v2", getSignature(signatureUrl));
    }

이렇게 하면 런타임 환경에서 어떤 API 호출에 관한 정보를 읽어들이고
그 호출에 맞는 값으로 암호화를 진행하고 헤더에 추가하는 것이 가능하다.

이제, 이 헤더 설정 클래스를 다른 카카오톡 발신 인터페이스의 @FeignClient 에너테이션의 인자로 등록하자.

@FeignClient(value = "api-AlimTalk", url = "${biz-message.request.alimtalk-url}", configuration = BizMsgApiHeaderFeignConfiguration.class)
public interface AlimTalkApiClient {

    @PostMapping("/messages")
    void sendMessage(SendAlimTalkFeignRequest sendAlimTalkFeignRequest);
}

그 이후 등록한 메서드를 호출하면

로그에 알림톡 발송에 맞게 헤더가 추가된것이 확인이 가능하고

실제 카카오톡 발신에도 성공했다!

후기

feign 을 사용하면서 내 상황에 맞게 설정하고 삽질하면서

feign 에 관한 이해도가 많이 올라간게 많이 체감이 되었고,

특히나 헤더 등록을 위한 feignrequestTemplate 에서 메서드 정보를 담고있다는게 흥미로웠다.

스프링 mvc에서 호출된 메서드 정보를 담아서 넘겨주고

넘어온 데이터로 인터셉터 경로 필터링을 하거나 특정 데이터를 필터에서 담아서

컨트롤러로 넘겨주는 과정과 비슷하게 느껴졌다.

사용하는 기술의 패턴이 유사한 느낌이랄까 ㅋㅋㅋ

post-custom-banner

0개의 댓글