휴대폰번호로 도착한 인증번호를 잠시 저장할 공간이 필요하지만 MySQL과 MariaDB와 같은 RDBMS를 사용하는 것은 비효율적이라 생각했다. 그래서 방법을 찾아보던 중, Redis에 대해 알게 되었다.
Redis
는 고성능의 키-값 저장소로 사용되는 오픈 소스 인메모리 데이터 구조 서버다. Redis는 Remote Dictionary Server의 약자로, 주로 빠른 데이터 접근 속도와 높은 처리량이 필요한 응용 프로그램에서 사용된다.
인메모리 데이터 저장: Redis는 모든 데이터를 메모리에 저장하고, 필요에 따라 디스크에 백업한다. 이로 인해 매우 빠른 데이터 접근이 가능하다.
다양한 데이터 구조: Redis는 문자열, 리스트, 셋, 해시, 정렬된 셋, 비트맵, 하이퍼로그로그 등의 다양한 데이터 구조를 지원한다. 이는 단순한 키-값 저장소 이상의 기능을 제공하며, 복잡한 데이터 처리 작업을 효율적으로 수행할 수 있다.
고가용성 클러스터: Redis는 Redis Cluster를 통해 데이터 분산과 샤딩을 지원하여 대규모 데이터를 효과적으로 처리할 수 있다.
트랜잭션: Redis는 MULTI, EXEC, DISCARD, WATCH 명령어를 통해 간단한 트랜잭션을 지원한다.
TTL (Time to Live): Redis는 각 키에 대해 TTL을 설정할 수 있습다. TTL이 만료되면 Redis는 자동으로 데이터를 삭제한다.
Redis는 이러한 특징들로 인해 캐싱, 세션 저장, 실시간 분석, 메시지 큐, 랭킹 시스템 등 다양한 용도로 널리 사용된다. 대표적인 사용 사례로는 웹 애플리케이션의 세션 관리, 실시간 채팅 애플리케이션, 게임 리더보드, 분석 데이터의 일시적 저장 등이 있다.
나는 1번과 5번의 특징을 보고 인증번호를 잠시동안 저장할 공간으로 Redis를 사용하게 되었다.
-build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
redis 의존성을 추가해준다.
@PostMapping("/verify")
public ResponseEntity<?> verifyCode(@RequestBody @Valid SmsVerifyDto smsVerifyDto){
boolean verify = smsService.verifyCode(smsVerifyDto);
if (verify) {
return ResponseEntity.ok("인증이 되었습니다.");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("인증에 실패했습니다.");
}
}
spring.data.redis.host=localhost
spring.data.redis.port=6379
Controller에 요청을 받을 코드를 추가해주자.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class SmsVerifyDto {
@NotNull(message = "휴대폰 번호를 입력해주세요.")
private String phoneNum;
@NotNull(message = "인증번호를 입력해주세요.")
private String certificationCode;
}
@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories // Redis 레포지토리 기능 활성화
public class RedisConfig {
private final RedisProperties redisProperties; // Redis 속성 정보 주입
@Bean // 스프링 컨텍스트에 RedisConnectionFactory 빈 등록
public RedisConnectionFactory redisConnectionFactory(){
// LettuceConnectionFactory를 사용하여 Redis 연결 팩토리 생성, 호스트와 포트 정보를 사용
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // RedisTemplate 인스턴스 생성
redisTemplate.setConnectionFactory(redisConnectionFactory()); // Redis 연결 팩토리 설정
redisTemplate.setKeySerializer(new StringRedisSerializer()); // 키를 문자열로 직렬화하도록 설정
redisTemplate.setValueSerializer(new StringRedisSerializer()); // 값을 문자열로 직렬화하도록 설정
return redisTemplate; // 설정이 완료된 RedisTemplate 인스턴스를 반환
}
}
@RequiredArgsConstructor
@Repository
public class SmsRepository {
private final String PREFIX = "sms:"; // Redis 키에 사용할 접두사
private final StringRedisTemplate stringRedisTemplate; // Redis 작업을 위한 StringRedisTemplate 객체
// SMS 인증 정보를 생성하는 메서드
public void createSmsCertification(String phone, String code){
int LIMIT_TIME = 3 * 60; // 인증 코드의 유효 시간(초), 3분 설정
stringRedisTemplate.opsForValue()
.set(PREFIX + phone, code, Duration.ofSeconds(LIMIT_TIME)); // Redis에 키와 값을 설정, 유효 시간도 함께 설정
}
// SMS 인증 정보를 가져오는 메서드
public String getSmsCertification(String phone){
return stringRedisTemplate.opsForValue().get(PREFIX + phone); // Redis에서 키에 해당하는 값을 가져옴
}
// SMS 인증 정보를 삭제하는 메서드
public void deleteSmsCertification(String phone){
stringRedisTemplate.delete(PREFIX + phone); // Redis에서 해당 키를 삭제
}
// 해당 키가 존재하는지 확인하는 메서드
public boolean hasKey(String phone){
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(PREFIX + phone)); // Redis에서 키의 존재 여부를 확인
}
}
@Service
public class SmsServiceImpl implements SmsService {
private final SmsCertificationUtil smsCertificationUtil; // SMS 인증 유틸리티 객체
private final SmsRepository smsRepository; // SMS 레포지토리 객체 (Redis)
private final AuthFeignClient authFeignClient; // 인증을 위한 Feign 클라이언트 객체
// 의존성 주입
public SmsServiceImpl(@Autowired SmsCertificationUtil smsCertificationUtil,
AuthFeignClient authFeignClient,
SmsRepository smsRepository) {
this.smsCertificationUtil = smsCertificationUtil;
this.smsRepository = smsRepository;
this.authFeignClient = authFeignClient;
}
@Override // SmsService 인터페이스의 메서드를 구현
public void SendSms(SmsRequestDto smsRequestDto) {
String phoneNum = smsRequestDto.getPhoneNum(); // SmsRequestDTO에서 전화번호를 가져옴
AuthResponseDto authResponseDto = authFeignClient.checkPhoneNum(phoneNum); // 전화번호의 가입 여부 확인
if (!authResponseDto.getNickname().equals("가입되지 않은 번호")) { // 이미 가입된 번호인지 확인
throw new IllegalArgumentException("이미 가입된 번호입니다."); // 가입된 번호일 경우 예외를 던짐
}
String certificationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000); // 6자리 인증 코드를 랜덤으로 생성
smsCertificationUtil.sendSMS(phoneNum, certificationCode); // SMS 인증 유틸리티를 사용하여 SMS 발송
smsRepository.createSmsCertification(phoneNum, certificationCode); // 인증 코드를 Redis에 저장
}
@Override // SmsService 인터페이스의 메서드를 구현
public boolean verifyCode(SmsVerifyDto smsVerifyDto) {
if (isVerify(smsVerifyDto.getPhoneNum(), smsVerifyDto.getCertificationCode())) { // 인증 코드 검증
smsRepository.deleteSmsCertification(smsVerifyDto.getPhoneNum()); // 검증이 성공하면 Redis에서 인증 코드 삭제
return true; // 인증 성공 반환
} else {
return false; // 인증 실패 반환
}
}
// 전화번호와 인증 코드를 검증하는 메서드
public boolean isVerify(String phoneNum, String certificationCode) {
return smsRepository.hasKey(phoneNum) && // 전화번호에 대한 키가 존재하고
smsRepository.getSmsCertification(phoneNum).equals(certificationCode); // 저장된 인증 코드와 입력된 인증 코드가 일치하는지 확인
}
}
이전 게시글의 Service 코드에서 많이 변경되었다.
나는 MSA 구조로 프로젝트를 개발하고 있어서 가입된 번호가 없다면 authserver에서 responseDto의 nickname에 가입되지 않은 번호를 넣고 반환해 문자를 보내게 했다.
모놀리식 구조의 경우는 userEntity.findByphoneNum
메소드를 사용하면 될 것이다.
문자를 전송하면 Redis에 Key : {PhoneNum}, Value : {certificationCode} 형식으로 3분동안 저장된다.
이 인증코드를 입력하게 되면 isVerfy 메서드에서 검증 후 성공 실패를 반환하게 된다.