Spring OpenFeign
을 사용하여 헤더 설정에 관해 삽질한 과정을 기록한 글입니다.
이번에 문자, 카카오톡 발신 기능을 만들게 되었다.
이를 위한 기술 스택으로 네이버클라우드(ncloud)
에서 제공하는 알림톡, SMS api
와
Spring OpenFeign
라이브러리를 채택하게 되었다.
- 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
으로 구현한 카카오 로그인 코드를 보자.
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
으로 구현하면 이렇게 일일히 다 구현하지 않아도 된다.
@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 공식 문서를 참고하시기 바랍니다.
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
을 사용한 메시지 발신에 성공했다.😀)
방금전에는 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
에 관한 이해도가 많이 올라간게 많이 체감이 되었고,
특히나 헤더 등록을 위한 feign
의 requestTemplate
에서 메서드 정보를 담고있다는게 흥미로웠다.
스프링 mvc에서 호출된 메서드 정보를 담아서 넘겨주고
넘어온 데이터로 인터셉터 경로 필터링을 하거나 특정 데이터를 필터에서 담아서
컨트롤러로 넘겨주는 과정과 비슷하게 느껴졌다.
사용하는 기술의 패턴이 유사한 느낌이랄까 ㅋㅋㅋ