요즘 개인적으로 특정 시간에 알림 메시지를 받아야 할 상황이 생겼다. 사실 간단하게 스마트폰 알람 주기를 설정해서 해결해도 되는 문제였지만, 일상 속에서 생긴 불편했던 점을 개발을 통해서 해결해보고 싶은 생각이 들었다. 그래서 프로그램을 직접 만들어서 SMS로 알림 시스템을 구현해 보기로 결정했다.
필요한 기능 정리 및 기술 선택
내 요구사항은 "주중(월~금)에 오후 5시 50분에 알림 문자 받기" 였다. 그럼 서버에 프로그램을 배포했을 때 crontab 같은 기능을 사용해서 내가 원하는 타이밍에 SMS를 보내는 일을 해야 한다. 그리고 가장 중요한 건 SMS로 문자 메시지를 받아야 한다. 나는 crontab은 Spring Boot에서 Spring Scheduler를 사용하기로 결정했고, SMS는 네이버 클라우드 플랫폼의 Simple & Easy Notification Service에 있는 SMS API를 사용하기로 결정했다. 마지막으로 네이버로 SMS 전송요청을 할 때는 WebClient를 사용하기로 결정했다.
Spring Scheduler를 사용하기로 한 이유는 의존성을 추가하지 않아도 Spring Boot Starter에서 기본적으로 제공해주기 때문이다. 그리고 사용 방법도 간단했기 때문에 빠르게 스케줄링 프로그램은 개발할 수 있는 점도 큰 매력으로 다가왔다.
네이버 클라우드에서 SMS API를 사용하기로 한 이유는 예전에 네이버에서 다른 API를 사용했을 때 정말 편하게 사용한 기억이 있었고, SMS API 문서를 봤을 때 정말 깔끔하고 보기 쉽게 정리된 것을 보고 잘 사용할 수 있겠다는 생각이 들어서 결정하게 됐다.
마지막으로 WebClient를 사용하는 이유는 예전에 RestTemplate을 사용해서 웹 요청을 보냈던 기억이 있는데, 객체 생성을 하고 웹 요청을 보내기까지 세팅하는 부분이 좀 불편했기 때문이다. 그래서 이번엔 WebClient를 사용하려고 한다. Spring 5 버전부터 등장한 HTTP 라이브러리라고 하는데, Spring에서도 RestTemplate 대신에 WebClient 사용을 권장한다고 한다. 그리고 WebClient는 비동기적으로 요청하는 Non-Blocking 방식을 사용한다. 응답을 기다리지 않고 비동기적으로 로직을 수행하기 때문에 효율적이고, 대용량 요청에 유리하다고 한다.
사실 지금 하는 프로젝트에서 이런 장점을 바라보고 WebClient를 사용하는 것은 아니지만, 이번 기회에 새로운것을 써본다는 생각으로 구현해 볼 생각이다.
개발 시작
가장 먼저 네이버 클라우드 플랫폼에서 Simple & Easy Notification Servce를 찾고 프로젝트 생성을 한다. 그리고 프로젝트 콘솔에서 서비스 키를 발급받는다.
그리고 계정 관리 -> 인증키 관리로 들어가서 Access Key ID, Secret Key를 발급받는다.
이제 스프링 부트 프로젝트를 생성한다.
프로젝트를 생성한 후에 Spring Scheduling을 사용하기 위해 Application Class에 [ic]@EnableScheduling[/ic] 어노테이션을 추가한다.
@EnableScheduling
@SpringBootApplication
public class HrdNotificationApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(HrdNotificationApplication.class);
}
}
이제 네이버 클라우드 SMS API 문서를 보면서 SMS 전송 요청을 보낼때 필요한 DTO와 헤더 설정 등을 확인해야 한다.
https://api.ncloud-docs.com/docs/ai-application-service-sens-smsv2
SMS API
api.ncloud-docs.com
문서를 보면 API 요청을 할 때 header에 꼭 넣어야할 항목들이 있다. 아래 링크에 가면 어떻게 생성해야 하는지 언어별로 자세히 나와있다.
https://api.ncloud-docs.com/docs/common-ncpapi
Ncloud API
api.ncloud-docs.com
그럼 이것들을 참고해서 요청 DTO를 생성하고 [ic]@Scheduled[/ic] 어노테이션을 사용해서 스케줄링 메서드를 구현하면 된다.
@Slf4j
@Service
@RequiredArgsConstructor
public class NotificationService {
@Value("${sms.from}")
private String FROM;
@Value("${ncp.access-key-id}")
private String NCP_ACCESS_KEY;
@Value("${ncp.secret-key}")
private String NCP_SECRET_KEY;
@Value("${ncp.notification.service-id}")
private String NCP_SERVICE_ID;
private String NCP_API_URL = "https://sens.apigw.ntruss.com";
private final HrdStudentRepository hrdStudentRepository;
@Scheduled(cron = "0 50 17 * * *")
public void sendSms() throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
// 주말이면 return 하기
// DB에서 SMS 보내야할 사람들의 데이터 가져오기
// 사람들 데이터(휴대폰 번호 등)로 네이버 SMS API로 보낼 DTO 만들기
// 네이버 SMS API로 웹 요청(WebClient 사용)
send(requestSmsDto, new Timestamp(System.currentTimeMillis()).getTime());
}
private Map<String, Object> send(RequestSmsDto requestSmsDto, long timestamp) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
WebClient webClient = WebClient.builder()
.baseUrl(NCP_API_URL)
.defaultHeader("x-ncp-apigw-timestamp", String.valueOf(timestamp))
.defaultHeader("x-ncp-iam-access-key", NCP_ACCESS_KEY)
.defaultHeader("x-ncp-apigw-signature-v2", makeSignature(String.valueOf(timestamp), "/sms/v2/services/" + NCP_SERVICE_ID + "/messages", HttpMethod.POST))
.build();
Map<String, Object> response = webClient.post()
.uri("/sms/v2/services/" + NCP_SERVICE_ID + "/messages")
.bodyValue(requestSmsDto)
.retrieve()
.bodyToMono(Map.class)
.block();
return response;
}
private String makeSignature(String timestampString, String urlString, HttpMethod httpMethod) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
String space = " ";
String newLine = "\n";
String method = httpMethod.name();
String accessKey = NCP_ACCESS_KEY;
String secretKey = NCP_SECRET_KEY;
String message = new StringBuilder()
.append(method)
.append(space)
.append(urlString)
.append(newLine)
.append(timestampString)
.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"));
return Base64.encodeBase64String(rawHmac);
}
}
나는 위와 같은 방식으로 구현했다. [ic]sendSMS()[/ic] 메서드에 있는 주석에는 각자 필요한 비즈니스 로직을 작성하면 될 것 같다.
배포 후 결과
이렇게 간단한 방법으로 개발을 마치고 서버에 배포해보았다. 비록 너무나도 간단하고 작은 서비스이지만 내가 필요로 한 서비스를 직접 만들어서 배포했다는 게 참 재미있고 뿌듯했다. 그리고 내 요구사항에 맞게 주중 17시 50분마다 SMS 메시지가 도착하고 있다.
서버에 로그도 찍어봤는데 잘 동작하고 있다.
느낀점
가장 먼저 이런 유용한 API를 제공해 주는 회사가 있다는 게 감사했다.
그리고 간단한 프로그램이라서 Service 계층이 간단하게 구현될 줄 알았는데 막상 다 개발해 놓고 보니까 WebClient로 웹 요청하는 메서드와 SMS API 요청할 때 access key, secret key, service key 등을 사용해서 헤더를 만들어 주는 부분들은 다른 클래스에 구현하고 책임을 좀 분리해서 구현해야겠다고 생각이 들었다. 그리고 로깅 구현도 좀 더 공부하고 알아본 후에 더 좋은 방식으로 구현해보고 싶다.
댓글