2편에서 http client로 NCP 알림톡을 발송했다면 이번엔 feign를 활용해 알림톡을 발송하는 방법과 코드 예시를 소개할 예정이다.
기존 http client 코드를 활용하려고 했더니 feign과 안맞는 부분이 있어 해당 부분들을 수정해 코드를 작성했다.
원래는 jsonobject를 만들어 바디 값을 담았다면 feign에서는 dto를 만들어 바디 값을 담았고 이때 ncp에서 요구하는 형식에 맞게 바디 값 구조를 그대로 직관적으로 맞춰 설정하는게 중요했다.
또한, serviceId를 환경 변수로 받아와 해당 주소로 api 요청을 보낼때 ':'이 다른 값으로 자동 인코딩되며 이를 ncp에서 그대로 인식하고 오류가 발생했다. 구글링을 통해 ColonInterceptor을 만들어 해결했다.
먼저 흐름을 크게 보면 open feign 관련 의존성 추가와 공통 설정을 프로젝트에 해주고 ncp 알림톡 관련해 필요한 설정들을 따로 더 추가한다.
설정들을 끝냈으면 필요한 정보들을 담아 @FeignClient 인터페이스를 활용해 api 요청을 보내면 된다.
필자는 필요한 정보들을 담는 용도로 helper를 사용해 정보들을 세팅했다.
공식 문서 링크는 아래와 같다.
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/
코드 예시
1. 의존성 추가
feign을 사용하기 위한 최소한의 의존성은 아래와 같다.
* Maven
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.4</version>
</dependency>
* Gradle
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '3.1.4'
필자의 프로젝트에 들어간 의존성은 아래와 같다.
dependencies {
api 'io.github.openfeign:feign-httpclient:12.1'
api 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.4'
api 'io.github.openfeign:feign-jackson:12.1'
}
2. 공통 설정
public interface BaseFeignClientPackage {}
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import feign.Logger.Level;
import feign.codec.Decoder;
import feign.jackson.JacksonDecoder;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableFeignClients(basePackageClasses = BaseFeignClientPackage.class)
public class FeignCommonConfig {
@Bean
public Decoder feignDecoder() {
return new JacksonDecoder(customObjectMapper());
}
@Bean
Level feignLoggerLevel() {
return Level.FULL;
}
}
3. ncp 관련 설정
에러 디코더 커스터마이징
(각자의 프로젝트에 맞게 에러 처리 넘겨주시면 됩니다!)
import feign.FeignException;
import feign.Response;
import feign.codec.ErrorDecoder;
public class NcpErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400) {
switch (response.status()) {
case 401 -> throw OtherServerUnauthorizedException.EXCEPTION;
case 403 -> throw OtherServerForbiddenException.EXCEPTION;
case 404 -> throw OtherServerNotFoundException.EXCEPTION;
case 500 -> throw OtherServerInternalSeverErrorException.EXCEPTION;
default -> throw OtherServerBadRequestException.EXCEPTION;
}
}
return FeignException.errorStatus(methodKey, response);
}
}
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.codec.ErrorDecoder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.*;
@Import(NcpErrorDecoder.class)
public class NcpConfig {
@Bean
@ConditionalOnMissingBean(value = ErrorDecoder.class)
public NcpErrorDecoder commonFeignErrorDecoder() {
return new NcpErrorDecoder();
}
@Bean
public RequestInterceptor basicAuthRequestInterceptor() {
return new ColonInterceptor();
}
public static class ColonInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.uri(template.path().replaceAll("%3A", ":"));
}
}
}
4. 알림톡 발송 코드 작성
body 값 담는 dto 생성
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
public class MessageDto {
@Getter
@Builder
public static class AlimTalkBody {
private String plusFriendId;
private String templateCode;
private List<AlimTalkMessage> messages;
}
@Getter
@Builder
@AllArgsConstructor
public static class AlimTalkMessage {
private String to;
private String content;
}
}
api 요청 정보들 세팅하는 helper 작성
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
@Helper
public class NcpHelper {
private final NcpClient ncpClient;
private final String serviceID;
private final String ncpAccessKey;
private final String ncpSecretKey;
private final String plusFriendId;
public NcpHelper(
NcpClient ncpClient,
@Value("${ncp.service-id}") String serviceID,
@Value("${ncp.access-key}") String ncpAccessKey,
@Value("${ncp.secret-key}") String ncpSecretKey,
@Value("${ncp.plus-friend-id}") String plusFriendId) {
this.ncpClient = ncpClient;
this.serviceID = serviceID;
this.ncpAccessKey = ncpAccessKey;
this.ncpSecretKey = ncpSecretKey;
this.plusFriendId = plusFriendId;
}
public void sendNcpAlimTalk(String to, String templateCode, String content) {
// signature 생성
String alimTalkSignatureRequestUrl = "/alimtalk/v2/services/" + serviceID + "/messages";
String[] signatureArray =
makePostSignature(ncpAccessKey, ncpSecretKey, alimTalkSignatureRequestUrl);
// 바디 생성
MessageDto.AlimTalkBody body = makeBody(templateCode, to, content);
ncpClient.sendAlimTalk(
serviceID,
"application/json; charset=UTF-8",
ncpAccessKey,
signatureArray[0],
signatureArray[1],
body);
}
public MessageDto.AlimTalkBody makeBody(String templateCode, String to, String content) {
MessageDto.AlimTalkMessage alimTalkMessage =
MessageDto.AlimTalkMessage.builder().to(to).content(content).build();
List<MessageDto.AlimTalkMessage> alimTalkMessages = new ArrayList<>();
alimTalkMessages.add(alimTalkMessage);
return MessageDto.AlimTalkBody.builder()
.plusFriendId(plusFriendId)
.templateCode(templateCode)
.messages(alimTalkMessages)
.build();
}
public String[] makePostSignature(String accessKey, String secretKey, String url) {
String[] result = new String[2];
try {
String timeStamp =
String.valueOf(Instant.now().toEpochMilli()); // current timestamp (epoch)
String space = " "; // space
String newLine = "\n"; // new line
String method = "POST"; // method
String message =
new StringBuilder()
.append(method)
.append(space)
.append(url)
.append(newLine)
.append(timeStamp)
.append(newLine)
.append(accessKey)
.toString();
SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8"));
String encodeBase64String = Base64.encodeBase64String(rawHmac);
result[0] = timeStamp;
result[1] = encodeBase64String;
} catch (Exception ex) {
throw new DuDoongDynamicException(0, "400", ex.getMessage());
}
return result;
}
}
feign client 생성
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
@FeignClient(
name = "NcpClient",
url = "https://sens.apigw.ntruss.com",
configuration = NcpConfig.class)
public interface NcpClient {
@PostMapping(
path = "/alimtalk/v2/services/{serviceId}/messages",
consumes = "application/json; charset=UTF-8")
void sendAlimTalk(
@PathVariable("serviceId") String serviceId,
@RequestHeader("Content-Type") String contentType,
@RequestHeader("x-ncp-iam-access-key") String ncpAccessKey,
@RequestHeader("x-ncp-apigw-timestamp") String timeStamp,
@RequestHeader("x-ncp-apigw-signature-v2") String signature,
@RequestBody MessageDto.AlimTalkBody alimTalkBody);
}
참고 사이트
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/
https://techblog.woowahan.com/2630/
https://velog.io/@haron/Feign-client-%EC%A0%81%EC%9A%A9%EA%B8%B0
https://wildeveloperetrain.tistory.com/172
https://bepoz-study-diary.tistory.com/414
https://stackoverflow.com/questions/40262132/how-to-add-a-request-interceptor-to-a-feign-client
https://stackoverflow.com/questions/61830167/disable-feign-encoding-of-pathvariables