JAVA/Spring Boot

[ Java ] - SMS 전송하기 (feat. naver cloud platform)

algml0703 2023. 5. 17. 22:49
반응형

  SMS 전송하기 (feat. naver cloud platform)

1. naver cloud platform 회원 가입 및 로그인

https://www.ncloud.com/ 옆의 링크를 통해 naver cloud에 들어가서 회원가입 및 로그인을 한다. 기본적으로 네이버 아이디가 있는 경우 네이버 아이디를 통한 간편 가입이 가능하다.

2. accessKey, secretKey 발급받기

로그인 후 상단의 메뉴바에서 마이페이지 - 계정 관리 - 인증키 관리가 있는데, 인증키 관리를 클릭하여 준다. 비밀번호 확인 과정이 있는데, 비밀번호를 입력해주면 아래와 같이 Api 인증키 관련 페이지가 나오는데 여기서 신규 API 인증키 생성을 클릭하여 생성하여 준다. 

기본적으로  생성된 AccessKeyId와 Secret Key는 블로그에 노출되거나, gitnub에 올라가는 등 외부에 유출되지 않도록 주의하여 관리하여야 한다.

3. serviceId 생성하기 (= 프로젝트 생성)

https://www.ncloud.com/product/applicationService/sens해당 링크를 통해 들어가면 naver cloud에서 제공하는 sns 서비스 관련 메인 페이지로 이동하는데, 해당 화면 가운데에 이용 신청하기를 클릭하여 준다. 해당 버튼 클릭 시 아래와 같은  페이지로 이동된다.

위의 페이지에서 프로젝트 생성하기를 클릭하며 또 다시 아래와 같이 프로젝트 생성을 위한 설정창이 나오는데 여기서 서비스 Type을 SMS로 선택하여 주고, 그 외에 이름이나 설명은 자율적으로 입력하여 준 후 생성하기를 누른다.

프로젝트를 생성하면, 아래에 생성된 프로젝트가 나타나고, 왼쪽의 키 모양을 누르면 serviceId를 확인할 수 있다.

그러면 현재까지 accessKey, secretKey, serviceId를 확보한 상태이다.

4. 발신번호 등록하기

우선 본격적으로 코드 작업하기에 앞서 발신 번호를 등록해두어야 한다. 등록해두지 않은 발신 번호로 SMS 발송 요청 시 401에러가 발생한다.

naver cloud platform의 우측 상단에 콘솔 버튼을 눌러 콘솔 페이지로 이동한 후 좌측 메뉴에서 Services - Application Services - Simple & Easy Notification Service를 클릭한다.

다시 좌측에 보면 Simple & Easy Notification Service - SMS - Calling Number 로 들어가서 화면 가운데에 발신 번호 등록을 선택하면 아래와 같이 두 가지 등록 방법이 있는데 간단하게 핸드폰 인증을 선택하여 발신 등록을 원하는 휴대폰 번호로 인증을 하여 등록한다.

정상적으로 발신번호 등록이 되면 가운데에 발신번호 조회에서 등록된 발신 번호를 확인할 수 있다.

5. 코드로 구현하기

사실 코드 구현의 경우 네이버 클라우드 api 문서에 잘 나와있다. 

4-1. 요청 헤더에 담을 시그니처 값 생성하는 클래스

참조: https://api.ncloud-docs.com/docs/common-ncpapi4

import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@Component
public class SignatureUtil {
    public String makeSignature(String timestamp, String accessKey, String secretKey, String url) {
        String space = " ";
        String newLine = "\n";
        String method = "POST";

        String message = new StringBuilder()
                .append(method)
                .append(space)
                .append(url)
                .append(newLine)
                .append(timestamp)
                .append(newLine)
                .append(accessKey)
                .toString();

        SecretKeySpec signingKey = null;
        try {
            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;
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException(e);
        }
    }
}

4-2. 요청 시  body값에 담을 클래스(DTO: Data Transfer Object)

*DTO : 클래스 내부에 특별한 로직이 담겨있지 않고 데이터 교환 시 사용되는 객체

참조: https://api.ncloud-docs.com/docs/ai-application-service-sens-smsv2#%EB%A9%94%EC%8B%9C%EC%A7%80%EB%B0%9C%EC%86%A1

@AllArgsConstructor
@Getter
@Setter
public class Messages {
    String to; // 발신번호
    String content; // 내용
}
@AllArgsConstructor
@Getter
@Setter
public class SmsRequest {
    String type; //
    String contentType;
    String from;
    String content;
    List<Messages> messages;

}

4-3. 응답 객체 클래스

public class Response {
    String requestId;
    String requestTime;
    String statusCode;
    String statusName;
}

 

4-2. 실질적으로 sms를 발송하는 클래스 (feat. WebClient)

참조: https://api.ncloud-docs.com/docs/ai-application-service-sens-smsv2#%EB%A9%94%EC%8B%9C%EC%A7%80%EB%B0%9C%EC%86%A1

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static java.lang.String.valueOf;

@Service
public class SmsSender {
    private final WebClient webClient;
    private final SignatureUtil signatureUtil;
    @Value("${spring.naver.secretkey}")
    private String secretKey;
    @Value("${spring.naver.sms.serviceid}")
    private String serviceId;
    private String accessKey;

    SmsSender(@Value("${spring.naver.accesskey}")String accessKey) {
        this.accessKey = accessKey;
        this.webClient = WebClient.builder()
                .baseUrl("https://sens.apigw.ntruss.com")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name())
                .defaultHeader("x-ncp-iam-access-key",accessKey)
                .build();
        this.signatureUtil = new SignatureUtil();
    }

    public void sendSms () {
        String timeStamp = valueOf(System.currentTimeMillis());
        String basePath = "/sms/v2/services/";
        String fullPath = basePath+serviceId+"/messages";
        String signature = signatureUtil.makeSignature(timeStamp, accessKey, secretKey, fullPath);
        List<Messages> messages = new ArrayList<>();
        Messages message = new Messages("[수신번호]","");
        messages.add(message);
        SmsRequest smsRequest = new SmsRequest("SMS","COMM","[발신번호]","반갑습니다.", messages);
        webClient
                .method(HttpMethod.POST)
                .uri(uriBuilder -> uriBuilder.path(fullPath).build())
                .header("x-ncp-apigw-timestamp", timeStamp)
                .header("x-ncp-apigw-signature-v2", signature)
                .body(BodyInserters.fromValue(smsRequest))
                .retrieve()
                .bodyToMono(SmsRequest.class)
                .block();
    }
}

* 참고로 SmsSender 클래스의 생성자 함수에서 accessKey를 클래스 필드를 통해 값을 넣지 않고, 별도로 생성자 함수에서 @Value를 통해 인자로 받아 값을 넣는 이유는 스프링 빈의 생명주기에 의한 것이다. 스프링 빈 생명주기는 컨테이너 생성 -> 스프링 빈 생성 -> 의존 관계 주입 -> 초기화 콜백 -> 사용 소멸 전 콜백 -> 스프링 종료 순으로 이루어지는데,  application.yml에 설정된 값이 등록되기 전에 객체가 생성되었기 때문에 값이 주입되지 않는 것이다. 이런 경우에는 생성자에 해당 값을 인자로 넣어주는 생성자 주입 방식이나, @PostConstruct 등의 어노테이션을 사용하여 문제를 해결할 수 있다. 나는 생성자 주입 방식을 사용하였다.

4-2. 해당 Sms 클래스 요청 api

@RestController
@RequiredArgsConstructor
public class TestController {
    private final SmsSender smsSender;
    @GetMapping
    public void test() {
        smsSender.sendSms();
    }
}

이와 같이 구현한 후 서버 실행히켜 해당 api에 접속 시 정상적으로 메시지가 전송되는 것을 확인할 수 있다.

 

반응형