[Spring Boot] SMS 전송 - NAVER SMS API 연동
진행중인 프로젝트 내에 SMS 서비스를 구현할 일이 생겼다.
개인 공부용 무료 SMS 서비스를 찾다가 NAVER CLOUD PLATFORM에서 지원하는 SMS API 서비스를 알게 됐다.
대부분 coolsms를 많이들 이용했는데 개인적으로 네이버가 친숙한 플랫폼이기도 하고, 매월 50건의 무료메세지 발송 및 첫 가입 시 10만 크레딧을 제공한다고 하여 선택하게 됐다. 해당 크레딧은 네이버 클라우드 플랫폼에서 제공하는 다른 솔루션에도 사용할 수 있으니 유용하게 쓰시길 바란다. (단, 실제로 결제가 가능한 카드를 연동해야 한다.)
또한 네이버에서 API 문서를 잘 구성해놓은 것도 채택에 큰 비중을 차지했다. 따라서 필자는 네이버 클라우드 플랫폼에서 제공하는 NAVER API 가이드에 따라 차근차근 진행해볼 예정이다.
NAVER SMS API 사용을 위한 환경구성
먼저, 네이버 클라우드 플랫폼에 가입해야 한다.
해당 링크로 들어가 가입한 후, 이곳을 클릭해 콘솔로 편하게 이동해보자.
Simple & Easy Notification Service (SMS 서비스의 이름이다.) 의 Home을 찾아가 프로젝트 생성하기를 누른다.
원하는 설정값을 입력하여 생성해보자. (필자는 SMS만 다룰것이므로 한 곳만 체크했다)
생성한 후에는 왼쪽 NAV바의 Project를 클릭하면 방금 만든 프로젝트를 조회할 수 있다.
만들어진 프로젝트의 SMS 버튼을 눌러 발신 번호를 등록하자.
(++2015년부터 발신번호 사전등록제가 시행되어 사전등록된 발신번호로만 문자 발송이 가능하다.)
SMS > CallingNumber 에서 발신번호 등록이 가능하다.
등록이 완료되었다. 해당 발신번호는 이후 API 요청에 필요하므로 까먹지 말고 기억해두자~ (보통 본인 번호를 넣을테니 까먹을 이유는 없겠지만)
환경 변수 설정하기 - KEY & ServiceId
이제 본격적인 API 요청을 위한 KEY와 ID값을 발급받아야 한다.
일반적인 경우와 다르게, 네이버는 토근 발급 없이 헤더에 명시한 값들만 넣어주면 한번에 API 기능들을 요청할 수 있다!
즉, 대부분의 경우는
- 발급받은 key 값으로 서비스에 token 요청
- 획득한 token을 헤더에 넣어 API 요청을 위한 입장권처럼 사용
네이버 SMS API의 경우 1번 요청을 패스하고 헤더에 규정된 값을 넣어 바로 본격적인 요청이 가능하다.
먼저, KEY값을 구해보자.
마이페이지 > 계정 관리 > 인증키관리 > 신규 API 인증키 생성
생성된 AccessKey와 SecretKey를 잘 백업해두자.
다음은 ServiceId를 가져오자. 해당 값은 SMS서비스 프로젝트를 만들때 이미 발급되었다.
프로젝트 콘솔화면 << 으로 들어가 서비스ID(열쇠모양)를 클릭하면 서비스ID를 발급받을 수 있다.
이제 필요한 환경구성은 끝났다!
SMS 전송을 위한 헤더 구성
아까 언급한대로, SMS API 요청을 하기 위해선 NAVER에서 지정해둔 포맷에 따라 헤더를 구현해야 한다.
각 헤더를 좀 더 풀어 설명해보자면
- 현재 시간, JAVA에서는 System.currentTimeMillis()로 간단하게 구현 가능
- 아까 발급받은 AccessKey값
- 아까 발급받은 SecretKey값을 적절하게 암호화 한 것
띠용? 암호화라고? 걱정하지마라 링크로 들어가면 암호화하는 코드도 아~주 상세하게 나와있다!
아래 링크의 makeSigniture() 메소드를 거의 그대로 참고하면 된다.
https://api.ncloud-docs.com/docs/common-ncpapi
그 외에 헤더에 관련된 자세한 정보도 나와있으니 참고하면 된다.
먼저 프로젝트의 환경설정 파일에서 아까 발급받은 KEY값과 serviceID를 설정하자.
(직접 코드에 그대로 붙여넣어 사용하는 것은 지양하도록 하자.. 아주 별로다..)
application.properties
마지막 senderPhone은 아까 프로젝트에서 설정한 발신번호를 기입하면 된다.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
프로젝트에 필요한 Dependency들도 미리 설정해두자.
(maven인 경우 httpclient, thymeleaf, spring-web, lombok을 해당 형식에 맞게 설정하면 된다.)
이제 3가지 헤더 중 암호화가 필요한 마지막 헤더 구성을 위한 메소드를 Service단에 구현해보자.
SmsService.java
@Slf4j
@RequiredArgsConstructor
@Service
public class SmsService {
@Value("${naver-cloud-sms.accessKey}")
private String accessKey;
@Value("${naver-cloud-sms.secretKey}")
private String secretKey;
@Value("${naver-cloud-sms.serviceId}")
private String serviceId;
@Value("${naver-cloud-sms.senderPhone}")
private String phone;
....
}
먼저 설정한 환경 변수들을 맨 위에 선언해주고,
public String makeSignature(Long time) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
String space = " ";
String newLine = "\n";
String method = "POST";
String url = "/sms/v2/services/"+ this.serviceId+"/messages";
String timestamp = time.toString();
String accessKey = this.accessKey;
String secretKey = this.secretKey;
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;
}
Signature 필드 값 생성을 위한 메서드를 작성하자.
이제 API요청을 위한 헤더 구성이 끝났다.
메시지 발송 - Request & Response 생성
헤더 구성이 끝났으니, 메시지 요청을 위한 Request 객체와 API 요청 반환값을 담아올 Response 객체를 생성해두자.
요청, 반환에 쓰이는 필드값은 공식 문서에 아주 상세히 나와있다. 참고하자.
https://api.ncloud-docs.com/docs/ai-application-service-sens-smsv2#메시지발송
메시지 요청에 필요한 항목이 많아보이지만, 공식 문서에서 보면 알 수 있듯이 모두 필수 값이 아니다.
SMS전송이기 때문에 LMS, MMS등에 필요한 요청 항목은 모두 제외했다. 또한 요청 항목의 messages부분은 DTO로 관리하여 그때 그때 맞는 발신자(to)와 내용(content)를 담도록 구현 하자.
(subject의 경우는 LMS, MMS에서만 사용 가능하므로 제외한다.)
MessageDTO
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Builder
public class MessageDTO {
String to;
String content;
}
SmsRequestDTO
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Builder
public class SmsRequestDTO {
String type;
String contentType;
String countryCode;
String from;
String content;
List<MessageDTO> messages;
}
SmsResponseDTO
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Builder
public class SmsResponseDTO {
String requestId;
LocalDateTime requestTime;
String statusCode;
String statusName;
}
메시지 발송 - Service 메소드 생성
이제 정말로 메시지 발송을 구현할 시간이다! Service단에 메시지 발송 메소드를 추가하자.
SmsService.java
public SmsResponseDTO sendSms(MessageDTO messageDto) throws JsonProcessingException, RestClientException, URISyntaxException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
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", accessKey);
headers.set("x-ncp-apigw-signature-v2", makeSignature(time));
List<MessageDTO> messages = new ArrayList<>();
messages.add(messageDto);
SmsRequestDTO request = SmsRequestDTO.builder()
.type("SMS")
.contentType("COMM")
.countryCode("82")
.from(phone)
.content(messageDto.getContent())
.messages(messages)
.build();
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(request);
HttpEntity<String> httpBody = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
SmsResponseDTO response = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/"+ serviceId +"/messages"), httpBody, SmsResponseDTO.class);
return response;
}
8번째 줄에서 아까 선언해둔 makeSignature 메소드를 사용해 3 번째 헤더를 생성하는 것을 볼 수 있다!
메시지 발송 - Controller
해당 API 동작을 테스트하는 방법은 POSTMAN 등 여러 방식이 있겠지만,
필자는 view를 직접 만들어 테스트해보도록 하겠다.
(귀찮은 사람들은 postman을 통해 controller에서 설정한 url로 테스트 가능하다.)
SmsController.java
package com.example.demo.controller;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.client.RestClientException;
import com.example.demo.dto.MessageDTO;
import com.example.demo.dto.SmsResponseDTO;
import com.example.demo.service.SmsService;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class SmsController {
private final SmsService smsService;
@GetMapping("/send")
public String getSmsPage() {
return "sendSms";
}
@PostMapping("/sms/send")
public String sendSms(MessageDTO messageDto, Model model) throws JsonProcessingException, RestClientException, URISyntaxException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
SmsResponseDTO response = smsService.sendSms(messageDto);
model.addAttribute("response", response);
return "result";
}
}
메시지 발송 - View
타임리프 view 템플릿을 사용하였다!
sendSms.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
<meta charset="UTF-8">
<title>Send Mail</title>
<script type="text/javascript" src="/scripts/jquery-ui/jquery.min.js"></script>
<script type="text/javascript" src="/scripts/common/common-ui.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div>
<h1>텍스트 메일 보내기</h1>
<form th:action="@{/sms/send}" method="post">
<table>
<tr class="form-group">
<td>발송할 전화번호</td>
<td>
<input type="text" class="form-control" name="to" placeholder="이메일 주소를 입력하세요">
</td>
</tr>
<tr class="form-group">
<td>내용</td>
<td>
<textarea class="form-control" name="content" placeholder="보낼 내용을 입력하세요"> </textarea>
</td>
</tr>
</table>
<button class="btn btn-default">발송</button>
</form>
</div>
</body>
</html>
result.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h2>SMS 내역 조회</h2>
<table class="table table-bordered">
<thead>
<tr>
<th>requestId</th>
<th>요청 시간</th>
<th>Status Code</th>
<th>Status Name</th>
</tr>
</thead>
<tbody>
<tr >
<td th:text="${response.requestId}"></td>
<td th:text="${response.requestTime}"></td>
<td th:text="${response.statusCode}"></td>
<td th:text="${response.statusName}"></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
실행 결과
이제 실행결과를 확인해보자.
전송
무사히 전송된 것을 볼 수 있다.
직접 NAVER CLOUD PLATFORM 콘솔 화면에 들어가도 전송 내역을 더 깔끔한 ui로 확인 가능하다.
개발하며 외부 API를 사용할수록 기본적인 작동 방식이 다 비슷하구나.. 라는 점을 느낀다.
각 서비스 주체에서 발급받은 KEY값을 가지고 제시하는 규격에 맞게 요청하면 손쉽게 받아올 수 있는 것 같다.
따라서 API 공식문서를 잘! 읽어봐야 한다는 점을 새삼 느꼈다.
(사실 필자의 경우 예시에 나와있는 url을 그대로 가져다 써서 401오류로 1시간을 날렸다..)
NAVER SMS API 정복!