본문 바로가기

Spharos Academy/Spharos Academy - Project

[ Auth-Service ] CoolSMS 구현

 

💬 CoolSMS란 ?

: 문자 메시지(SMS, LMS, MMS) 발송 API 서비스를 제공하는 플랫폼

 


🔧 CoolSMS의 핵심 기능

기능 설명
SMS 90자 이내 단문 메시지 전송
LMS 2,000자 이내 장문 메시지 전송
MMS 이미지 등 첨부해서 전송 가능
카카오 알림톡 카카오톡 기반 메시지 전송 (사전 인증 필요)
국제문자 발송 해외 번호로 문자 전송 가능
문자 수신 수신용 번호를 통해 수신된 문자 수집

 

 

📦 개발자 관점에서 사용법

  1. 회원가입 후 API Key, API Secret 발급
  2. Java SDK, REST API 등 사용하여 문자 전송
  3. 인증번호 발송, 이벤트 알림 등 다양한 곳에 활용 가능

 

✅ 언제 쓰면 좋아?

  • 회원가입 시 인증번호 전송
  • 이벤트/쿠폰 알림 문자
  • 주문/배송 상태 안내
  • 긴급공지 알림

✅  CoolSMS 사용하기 위한 사전 준비 

(1) CoolSMS 접속 후 "개발/연동"클릭 

 

 

(2) 개발/연동 > API Key 관리에서 새로운 API KEY를 발급

SECRET 조회 버튼 클릭 후 이메일 인증 후 인증 코드 작성 하면 API SECRET 생성 완료

 


✅  발신 번호 등록 방법

(1) SOLAPI 접속 > 대시보드 > 발신번호 관리

 

 

(2) 번호인증 만료 되서 만료일 갱신 버튼 클릭  (없다면 새 발신번호 등록 클릭) 

 

 

(3) 본인 인증하기

 

 

 

(4) 본인인증 완료 후 활성화 상태로 바뀜 

 


 

🔗 CoolSMS Springboot 프로젝트에 적용 

(1) CoolSMS 의존성 추가 

// CoolSMS
implementation 'net.nurigo:sdk:4.3.0'

 

 

(2) application.yml에 key, secret, sender-number 작성 

👉 휴대폰 번호는 반드시 '01012345678' 형태로 입력 (하이픈 작성 x) 

cools:
  api:
    key: "your api key"
    secret: "your api secret"
    sender-number: "your sender number"

 

 

(3) SmsCertificationUtil (인증번호 문자(SMS)를 발송하는 유틸 클래스) 

@Component
public class SmsCertificationUtil {

    // 🔐 환경 변수로 API Key 설정
    @Value("${cools.api.key}")
    private String apiKey;

    @Value("${cools.api.secret}")
    private String apiSecret;

    @Value("${cools.api.sender-number}")
    private String senderNumber;

    DefaultMessageService messageService;

    // 🚀 초기화 작업 - @PostConstruct
    @PostConstruct
    public void init() {
        this.messageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecret, "https://api.coolsms.co.kr");
    }

    // 📩 문자 전송 메서드
    public void sendSMS(String to, String certificationCode){
        Message message = new Message(); // 새 메시지 객체 생성
        message.setFrom(senderNumber); // 발신자 번호 설정
        message.setTo(to); // 수신자 번호 설정
        message.setText("본인확인 인증번호는 " + certificationCode + "입니다."); // 메시지 내용 설정

        this.messageService.sendOne(new SingleMessageSendingRequest(message)); // 메시지 발송 요청
    }
}

 


🌟 인증 코드 보내고 인증 확인 받는 코드 작성 

📌 SmsService (interface) 

public interface SmsService {

    void sendSms(RequestSendSmsCodeDto requestSmsDto);

    void verifySmsCode(RequestVerificationSmsDto requestVerificationSmsDto);
}

 

 

📌 SmsServiceImpl (SmsService 구현체) 

@Service
@RequiredArgsConstructor
public class SmsServiceImpl implements SmsService {

    private final SmsCertificationUtil smsCertificationUtil;

    private final RedisUtil<String> redisUtil;

    @Override
    public void sendSms(RequestSendSmsCodeDto requestSmsDto) {

        String certificationCode = createCertificationCode();

        String phoneNumber = requestSmsDto.getPhoneNumber();
        SendPurpose purpose = requestSmsDto.getSendPurpose();

        String textMessage = switch (purpose) {
            case FIND_EMAIL -> "[VYBZ] 이메일 찾기 인증번호 " + certificationCode + "입니다. (15분 이내 인증하시길 바랍니다.)";
            case FIND_PASSWORD -> "[VYBZ] 비밀번호 찾기 인증번호 " + certificationCode + "입니다. (15분 이내 인증하시길 바랍니다.)";
            default -> "[VYBZ] 회원가입 인증번호 " + certificationCode + "입니다. (15분 이내 인증하시길 바랍니다.)";
        };

        smsCertificationUtil.sendSMS(phoneNumber, textMessage);

        redisUtil.save("sms:" + requestSmsDto.getPhoneNumber(), certificationCode, 15, TimeUnit.MINUTES);
    }

    @Override
    public void verifySmsCode(RequestVerificationSmsDto requestVerificationSmsDto) {

        final String number = requestVerificationSmsDto.getPhoneNumber();
        final String redisCode = redisUtil.get("sms:" + number);

        if (redisCode == null) {
            throw new BaseException(BaseResponseStatus.EXPIRED_SMS_CODE);
        }

        if (!redisCode.equals(requestVerificationSmsDto.getVerificationSmsCode())) {
            String smsFailKey = "SMSVerify:" + number;

            if (redisUtil.increase(smsFailKey, 5L, TimeUnit.MINUTES) >= 10) {
                redisUtil.delete("sms:" + number);
                redisUtil.delete(smsFailKey);
                throw new BaseException(BaseResponseStatus.SMS_CODE_VERIFICATION_LIMITED);
            }
            throw new BaseException(BaseResponseStatus.INVALID_SMS_CODE);
        }

        SendPurpose purpose = requestVerificationSmsDto.getSendPurpose();

        switch (purpose) {
            case FIND_EMAIL:
                redisUtil.save("find-email-sms-Verified:" + number, "true", 10, TimeUnit.MINUTES);
                break;
            case FIND_PASSWORD:
                redisUtil.save("find-password-sms-Verified:" + number, "true", 10, TimeUnit.MINUTES);
                break;
            case SIGN_UP:
            default:
                redisUtil.save("sign-up-sms-Verified:" + number, "true", 10, TimeUnit.MINUTES);
                break;
        }

        redisUtil.delete("sms:" + number);
        redisUtil.delete("SMSVerify:" + number);
    }

    private String createCertificationCode() {
        return String.valueOf((int)((Math.random() * 900000) + 100000)); // 6자리 랜덤 숫자
    }
}

 

 

📌 SmsController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/busker")
public class SmsController {

    private final SmsService smsService;

    @Operation(summary = "Send SMS Code API", description = "전화번호 인증 코드 발송", tags = {"SMS-service"})
    @PostMapping("/sms-code")
    public BaseResponseEntity<Void> sendSMS(
            @Valid @RequestBody RequestSendSmsCodeVo requestSendSmsCodeVo
    ) {
        smsService.sendSms(RequestSendSmsCodeDto.from(requestSendSmsCodeVo));

        return new BaseResponseEntity<>(BaseResponseStatus.SMS_CODE_SUCCESS);
    }

    @Operation(summary = "Verify SMS Code API", description = "전화번호 인증 코드 검증", tags = {"SMS-service"})
    @PostMapping("/sms-verify")
    public BaseResponseEntity<Void> verifySMS(
            @Valid @RequestBody RequestVerificationSmsVo requestVerificationSmsVo
    ) {
        smsService.verifySmsCode(RequestVerificationSmsDto.from(requestVerificationSmsVo));

        return new BaseResponseEntity<>(BaseResponseStatus.SMS_CODE_VERIFICATION_SUCCESS);
    }
}

 

 

📌  RequestSendSmsCodeDto

@Getter
@NoArgsConstructor
public class RequestSendSmsCodeDto {

    private String phoneNumber;

    private SendPurpose sendPurpose;

    @Builder
    public RequestSendSmsCodeDto(String phoneNumber, SendPurpose sendPurpose) {
        this.phoneNumber = phoneNumber;
        this.sendPurpose = sendPurpose;
    }

    public static RequestSendSmsCodeDto from(RequestSendSmsCodeVo requestSmsVo) {
        return RequestSendSmsCodeDto.builder()
                .phoneNumber(requestSmsVo.getPhoneNumber())
                .sendPurpose(SendPurpose.valueOf(requestSmsVo.getPurpose().toUpperCase()))
                .build();
    }
}

 

 

 

📌  RequestVerificationSmsDto

@Getter
@NoArgsConstructor
public class RequestVerificationSmsDto {

    private String phoneNumber;

    private String verificationSmsCode;

    private SendPurpose sendPurpose;

    @Builder
    public RequestVerificationSmsDto(String phoneNumber, String verificationSmsCode
    , SendPurpose sendPurpose) {
        this.phoneNumber = phoneNumber;
        this.verificationSmsCode = verificationSmsCode;
        this.sendPurpose = sendPurpose;
    }

    public static RequestVerificationSmsDto from(RequestVerificationSmsVo requestVerificationSmsVo) {
        return RequestVerificationSmsDto.builder()
                .phoneNumber(requestVerificationSmsVo.getPhoneNumber())
                .verificationSmsCode(requestVerificationSmsVo.getVerificationSmsCode())
                .sendPurpose(SendPurpose.valueOf(requestVerificationSmsVo.getPurpose().toUpperCase()))
                .build();
    }
}

 

 

📌  RequestSendSmsCodeVo

@Getter
@NoArgsConstructor
public class RequestSendSmsCodeVo {

    @NotEmpty(message = "휴대폰 번호를 입력해주세요")
    private String phoneNumber;

    @NotBlank
    private String purpose;
}

 

 

📌  RequestVerificationSmsVo

@Getter
@NoArgsConstructor
public class RequestVerificationSmsVo {

    @NotBlank(message = "번호를 입력해주세요.")
    @Pattern(
            regexp = RegexPatterns.PHONE_NUMBER,
            message = "전화번호는 010-xxxx-xxxx 형식이어야 합니다."
    )
    private String phoneNumber;

    @NotBlank
    @Size(min=6, max=6, message = "인증번호는 6글자 입니다.")
    private String verificationSmsCode;

    @NotBlank(message = "비밀번호 확인을 위한 목적을 입력해주세요.")
    private String purpose;
}

 


 

💡 메시지 전송 결과 

 

 

 


🔥 "메시지 전송 실패 (번호도용문자 차단서비스에 가입되어 있어 메시지 전송 실패)" 에러

: 발신번호(보내는 번호)가 번호도용문자 차단서비스에 걸려서 막힌 경우를 의미

 

❗️ 이 문제는 CoolSMS뿐 아니라 모든 문자 전송 플랫폼에서 공통적으로 발생할 수 있는 이통사(KT, SKT, LGU+) 차단 정책이 원인

 

 

 


🔧  해결 방법 요약

🔐 "번호도용문자 차단서비스"는 통신사에 직접 요청해서 해지해야 함

 

📞  통신사별 고객센터

통신사 고객센터 번호 안내
SKT 114 또는 080-404-0050 "번호도용문자 차단 서비스 해지 요청"
KT 100 또는 080-2580-111
LG U+ 101 또는 080-525-0150