[Spring Boot] 네이버 SENS 로 문자(SMS) 발송하기
🌈 Naver Simple & Easy Notification Service
네이버 클라우드 플랫폼의 SENS 는 문자 전송 API 를 제공하고 있다.

🌈 사전 준비 사항
1. 먼저, 네이버 클라우드 플랫폼에서 회원가입을 진행한 후에 마이페이지 > 계정 관리 > 인증키 관리 로 들어간다.
여기서 신규 API 인증키 생성 을 선택해서 ✨Access Key, Secret Key✨를 발급 받자.
(Access Key, Secret Key 는 외부에 노출되지 않도록 .yaml 파일과 깃허브 Secrets 에 저장했다.)

2. 이제 서비스 > Simple & Easy Notification Service 로 들어가서 프로젝트 생성하기 를 선택해서 프로젝트를 생성해준다.

서비스 타입을 SMS 로 지정해서 프로젝트를 생성하고 나면, ✨Service ID✨ 를 발급 받을 수 있다. Service ID 도 마찬가지로 .yaml 파일과 깃허브 Secrets 에 저장해두었다.
application.yaml
ncp:
sms:
access-key: {액세스 키}
secret-key: {시크릿 키}
service-id: {서비스 아이디}
send-from: {발신자 번호}
🌈 코드 작성
실제로 SMS 전송을 진행할 Service 클래스의 코드를 작성하기에 앞서, 필요한 다른 4개의 클래스들을 작성할 것이다.
💫 SmsProperties
위에서 언급했던 application.yaml 파일에 숨겨둔 값들을 SMS 전송 시 사용하기 위해서 불러오는 클래스이다.
@ConfigurationProperties("ncp.sms")
@Component
@Data
public class SmsProperties {
private String accessKey;
private String secretKey;
private String serviceId;
private String sendFrom;
}
💫 SmsRequest
네이버 SENS 의 SMS API 요청에 들어갈 Body 값들이다. (설명은 주석에 달아두었다.)
참고로 type 에는 SMS, LMS, MMS 가 들어갈 수 있는데, 나는 SMS 를 선택할 것이다. (SMS는 짧은 텍스트 메시지, LMS는 긴 텍스트 메시지, MMS는 이미지 등 멀티미디어 파일을 포함하는 메시지 서비스이다.)
contentType 에는 COMM: 일반메시지, AD: 광고메시지 가 들어갈 수 있다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class SmsRequest {
String type; // SMS Type (SMS, LMS, MMS)
String contentType; // 메시지 Type (COMM, AD)
String countryCode; // 국가 번호
String from; // 발신자
String content; // 기본 메시지 내용
List<Message> messages;
}
💫 Message
위의 SmsRequest 에 필드로 포함되어 있는 메시지 객체이다. (실제로 전송할 SMS 정보를 담고 있다고 보면 된다.)
SmsRequest 에 있는 content 는 기본 메시지 내용이라면, 여기에 있는 content 는 개별 수신자에게 전송할 메시지 내용이다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class Message {
String to; // 수신자
String content; // 개별 메시지 내용
}
💫 SmsResponse
요청에 대한 응답으로 받을 객체이다. (설명은 주석을 참고)
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class SmsResponse {
String requestId; // 요청 아이디
LocalDateTime requestTime; // 요청 시간
String statusCode; // 202: 성공, 그 외: 실패
String statusName; // success: 성공, fail: 실패
}
이제, 진짜 SMS 전송을 진행할 SmsSender 클래스를 작성해보자.
💫 SmsSender
위에서 SmsRequest 클래스를 작성하면서 API 요청 Body 에는 뭐가 들어가는지 대충 봤었다.
그럼 Header 에는 뭐가 들어갈까?

보다시피 signature 항목에는 암호화한 내용이 들어가야 한다. 다행히 해당 코드는 네이버 SENS 에서 예시를 제공하고 있다.
나의 경우 아래와 같이 signature 를 만드는 메소드를 구현했다. (time 은 매개변수로 받도록 수정했다.)
public String makeSignature(Long time) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
String space = " ";
String newLine = "\n";
String method = "POST";
String url = "/sms/v2/services/" + smsProperties.getServiceId() + "/messages";
String timestamp = time.toString();
String accessKey = smsProperties.getAccessKey();
String secretKey = smsProperties.getSecretKey();
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);
return encodeBase64String;
}
이제 SMS 전송 메소드를 구현해보자.
헤더를 먼저 만들고, 바디를 만든 후에 둘을 HttpEntity 로 합쳐서 요청을 보낸다.
public SmsResponse sendSms(Message message) throws JsonProcessingException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException,
URISyntaxException, RestClientException {
Long time = System.currentTimeMillis();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("x-ncp-apigw-timestamp", time.toString());
headers.set("x-ncp-iam-access-key", smsProperties.getAccessKey());
headers.set("x-ncp-apigw-signature-v2", makeSignature(time));
List<Message> messages = new ArrayList<>();
messages.add(message);
SmsRequest smsRequest = SmsRequest.builder()
.type("SMS")
.contentType("COMM")
.countryCode("82")
.from(smsProperties.getSendFrom())
.content("[Repick]")
.messages(messages)
.build();
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(smsRequest);
HttpEntity<String> httpRequest = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
SmsResponse smsResponse = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/" + smsProperties.getServiceId() + "/messages"),
httpRequest, SmsResponse.class);
return smsResponse;
}
SmsSender 의 ✨전체 코드✨는 이렇다.
@Service
@RequiredArgsConstructor
public class SmsSender {
private final SmsProperties smsProperties;
public SmsResponse sendSms(Message message) throws JsonProcessingException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException,
URISyntaxException, RestClientException {
Long time = System.currentTimeMillis();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("x-ncp-apigw-timestamp", time.toString());
headers.set("x-ncp-iam-access-key", smsProperties.getAccessKey());
headers.set("x-ncp-apigw-signature-v2", makeSignature(time));
List<Message> messages = new ArrayList<>();
messages.add(message);
SmsRequest smsRequest = SmsRequest.builder()
.type("SMS")
.contentType("COMM")
.countryCode("82")
.from(smsProperties.getSendFrom())
.content("[Repick]")
.messages(messages)
.build();
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(smsRequest);
HttpEntity<String> httpRequest = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
SmsResponse smsResponse = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/" + smsProperties.getServiceId() + "/messages"),
httpRequest, SmsResponse.class);
return smsResponse;
}
public String makeSignature(Long time) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
String space = " ";
String newLine = "\n";
String method = "POST";
String url = "/sms/v2/services/" + smsProperties.getServiceId() + "/messages";
String timestamp = time.toString();
String accessKey = smsProperties.getAccessKey();
String secretKey = smsProperties.getSecretKey();
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);
return encodeBase64String;
}
}
🌈 테스트
테스트용 컨트롤러를 만들어서 테스트 해 본 결과,
문자가 잘 온다!

+이후 리팩토링
리픽에서 상품 주문 내역 + 무통장입금 계좌 + 입금 기한 안내 를 SMS 로 발송하려다보니, 길이 제한(최대 80 byte 까지만 전송 가능)이 턱없이 부족했다. 따라서 이후에 SmsRequest 의 type을 LMS(최대 2,000 byte 까지 전송 가능) 로 변경하였다.
참고:
https://born2bedeveloper.tistory.com/67
https://koogood.tistory.com/27
https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2