springboot

문자 sms 인증 + db 활용 고민

shinminkyoung 2025. 3. 10. 22:39

내가 구현해야 할 것

문자인증 시 들어오는 값은 [국가번호 + 전화번호]
국가번호는 3개 중 하나로 파싱 후 이에 따라 국적 분류해야함

문자인증 연결은 1. 회원가입 2. 전화번호 수정
mysql에 sms_code 속성 추가한 후 이를 저장한 뒤, 비교해서 같은 값이면 인증 성공 (시간 초과 시 실패)
인증 다시 할 때마다 db에 있는 값 갱신

1. sms 인증 코드 구현 (mysql 코드 저장 포함)
2. 국가번호 파싱 -> 국가 저장 포함 -> sms 인증 연결
 
 
1. [국가번호 + 전화번호] 파싱
// 국가 번호별 국적 매핑
public static final Map<String, String> COUNTRY_NATIONALITY_MAP = Map.of(
        "+82", "South Korea",  // 대한민국
        "+1", "United States", // 미국
        "+81", "Japan"         // 일본
);

// 국가 코드 추출
public static final Pattern PHONE_PATTERN = Pattern.compile("^(\\+\\d{1,3})\\s?\\d+");

public String extractCountryCode(String phoneNumber) {
    Matcher matcher = PHONE_PATTERN.matcher(phoneNumber);
    if (matcher.find()) {
        return matcher.group(1); // 국가 코드 반환
    }
    return ""; // 기본값
}
 
파싱한 값을 저장
// 전화번호에서 국가번호 파싱
String countryCode = extractCountryCode(phoneNumber);
String nationality = COUNTRY_NATIONALITY_MAP.getOrDefault(countryCode, "Unknown");
 
 
2. 문자인증
 
 

세상에서 가장 안정적이고 빠른 메시지 발송 플랫폼 - 쿨에스엠에스

손쉬운 결제 전용계좌, 신용카드, 계좌이체 등 국내 결제 뿐만 아니라 해용신용카드로 한번의 카드번호 등록으로 자동충전까지 지원합니다. 전용계좌, 신용카드, 계좌이체 등 다양한 결제 방식

coolsms.co.kr

 

coolsms 인증 API를 사용.

 

1) 웹사이트에서 번호 등록 후, api key를 발급받음

 

2) yml 파일에 secret 정보 저장

coolsms:
  apikey: #####
  apisecret: #####
  fromnumber: #####

 

3) MessageController.java

@RestController
@RequiredArgsConstructor
@Validated
@Slf4j
@RequestMapping("/api/auth/sms")
public class MessageController {

    private final MessageService messageService;

    // 인증번호 발송
    @GetMapping("/send")
    public BasicResponse<String> sendSMS(@RequestParam String phone) {  // 사용자가 작성한 번호
        // 예외 처리 한 버전
        try {
            String response = messageService.sendSMS(phone);
            return BasicResponse.onSuccess(response);
        } catch (Exception e) {
            log.error("문자 전송 실패: {}", e.getMessage());
            return BasicResponse.onFailure("400", "문자 전송 실패", null);
        }
    }

    // 인증번호 검증
    @GetMapping("/verify")
    public BasicResponse<Boolean> verifySMSCode(@RequestParam String phone, @RequestParam String code) {
        // 예외 처리 한 버전
        try {
            boolean isValid = messageService.verifySMSCode(phone, code);
            if (isValid) {
                return BasicResponse.onSuccess(true);
            } else {
                return BasicResponse.onFailure("401", "인증 실패", false);
            }
        } catch (Exception e) {
            log.error("문자인증 검증 중 오류 발생: {}", e.getMessage());
            return BasicResponse.onFailure("500", "서버 오류", false);
        }
    }
}

 

 4) MessageService.java

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MessageService {

    private final DefaultMessageService messageService; // DefaultMessageService 주입
    private final UserRepository userRepository;

    @Value("${coolsms.apikey}")
    private String apiKey;

    @Value("${coolsms.apisecret}")
    private String apiSecret;

    @Value("${coolsms.fromnumber}")
    private String fromNumber;

    // 인증번호 생성
    private String createRandomNumber() {
        Random rand = new Random();
        StringBuilder randomNum = new StringBuilder();
        for (int i = 0; i < 6; i++) { // 6자리 인증번호 생성
            randomNum.append(rand.nextInt(10));
        }
        return randomNum.toString();
    }

    // 인증번호 전송 & MySQL에 저장
    @Transactional
    public String sendSMS(String phoneNumber) {
        String randomNum = createRandomNumber();
        System.out.println("생성된 인증번호: " + randomNum);

        // SMS 객체 생성
        Message message = new Message();
        message.setFrom(fromNumber);
        message.setTo(phoneNumber);
        message.setType(MessageType.SMS);
        message.setText("[인증번호] " + randomNum);

        try {
            SingleMessageSentResponse response = messageService.sendOne(new SingleMessageSendingRequest(message));
            System.out.println("SMS 전송 성공: " + response);

            // MySQL에 인증번호 저장 (기존 번호 갱신)
            Optional<User> user = userRepository.findByPhoneNumber(phoneNumber);
            if (user.isPresent()) {
                User updatedUser = user.get().toBuilder()
                        .smsCode(randomNum)
                        .smsCodeExpiry(LocalDateTime.now().plusMinutes(5))  // 5분제한
                        .build();
                userRepository.save(updatedUser);
            } else {
                User newUser = User.builder()
                        .phoneNumber(phoneNumber)
                        .smsCode(randomNum)
                        .smsCodeExpiry(LocalDateTime.now().plusMinutes(5))
                        .build();
                userRepository.save(newUser);
            }

            return "문자 전송이 완료되었습니다.";
        } catch (Exception e) {
            System.err.println("SMS 전송 실패: " + e.getMessage());
            return "문자 전송 실패";
        }
    }

    // 인증번호 검증
    public boolean verifySMSCode(String phoneNumber, String inputCode) {
        Optional<User> user = userRepository.findByPhoneNumber(phoneNumber);
        if (user.isPresent()) {
            User foundUser = user.get();
            if (foundUser.getSmsCode().equals(inputCode) &&
                    foundUser.getSmsCodeExpiry().isAfter(LocalDateTime.now())) {
                User updatedUser = foundUser.toBuilder()
                        .smsCode(null)
                        .smsCodeExpiry(null)
                        .build();
                userRepository.save(updatedUser);
                return true;
            }
        }
        return false;
    }
}

 

 

문자인증은 이렇게 구현할 수 있다. 

 

리팩터링 할 것

현재, 유저 테이블에 sms_code, code_expiry를 새로운 속성을 두어 저장하고 있다.

그러나, 효율성 측면에서

1. user 테이블에서 분리해 sms_code, code_expiry 속성을 관리

2. 기존 db인 mysql이 아닌 Redis를 따로 두고 사용

이 더 좋을 것 같다.

 

문자 인증 제한 시간은 5분으로 두어 하나의 속성으로 값을 할당하고 수정하는 것은 비효율적이다.

 

자세히, 비용 측면에서 Redis vs. MySQL 문자 인증 저장을 비교

저장 비용 디스크 기반 저장 (저렴) 메모리 기반 저장 (비쌈)
읽기 속도 느림 (디스크 I/O 발생) 빠름 (RAM 기반)
쓰기 속도 느림 (트랜잭션, 인덱싱 영향) 빠름 (메모리 쓰기)
삭제 비용 상대적으로 높음 (쿼리 실행 필요) 낮음 (TTL 활용 가능)
운영 비용 기존 DB 유지 (추가 비용 없음) Redis 서버 추가 필요 (운영비 증가)
확장성 다중 노드 확장 가능 클러스터링 필요 (비용 증가)

비용을 절약하려면 MySQL, 성능을 높이려면 Redis를 사용하는 게 맞는 거 같다.