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를 사용하는 게 맞는 거 같다.