Framework & Library/Spring

[Spring] Spring Boot Redis 사용하기 / 이메일 인증 유효시간

마볼링 2023. 12. 31. 09:55
Redis는 key-value 구조로 데이터를 저장하는 In-memory 기반의 데이터베이스 시스템입니다.
Spring boot 에선 어떻게 구현하는지 알아봅시다.

 

 

1. 먼저 Redis란?

먼저 Redis가 어떤건지는 블로그 다른 게시글에 작성해놨습니다.

 

[CS / DataBase] 인메모리 데이터 저장소 Redis, 왜 사용할까?

개발을 하면서 트랜잭션의 속도를 어떻게 하면 줄일 수 있을까? 고민하면서 찾아보면 Redis라는 시스템을 접하게 된다. 그럼 이 Redis는 무엇일까? 1. 레디스(Redis) 란? Redis의 풀네임에서 할 수 있듯

repeater2487.tistory.com

 

 

2. Spring Boot에서 Redis 사용하기

2 - 1. 기본 설정

[ 의존성추가 ]

저는 대게 gradle를 사용하기 때문에 build.gradle에서 dependencies에 추가하였습니다.

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

[ Properties 추가 ]

의존성뿐만 아니라 Properties도 설정해줘야합니다.

spring.redis.host=localhost
spring.redis.port=6379

 

만일 AWS에 ElastiCache를 이용하여 Redis 서버를 구현하였다면 host에 기본 엔드포인트를 적으시면 됩니다.

 

 

2 - 2. Config 설정

본격적인 redis 설정을 해봅시다.

 

Java의 Redis Client 라이브러리는 2개가 있습니다.

  • Jedis
  • Lettuce

이 포스트에선 Lettuce로 설정을 해보겠습니다.

참고 : Jedis와 Lettuce 성능 비교

@Configuration
@EnableRedisRepositories
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // Redis 연결 팩토리 설정
        redisTemplate.setConnectionFactory(redisConnectionFactory()); 

        // 직렬화
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}
  • Lettuce로 사용할 예정이라 LettuceConnectionFactory를 생성해 반환해줍니다.
  • Redis는 Repository를 사용하는 방법과 Template를 사용하는 방법이 있는데 이번 포스트에서는 Repository를 사용할 예정이기 때문에 @EnableRedisRepositories 어노테이션 추가해줍니다.
  • 위에서 setKeySerializersetValueSerializer 설정해주는 이유는 
    • Redis에 저장된 binary형태의 데이터를 String으로 변환시켜서 눈으로 확인하기 위함입니다.
    • Redis에 데이터가 binary형태로 저장되는 이유는 Spring Data Redis는 기본 직렬화 방식 JdkSerializationRedisSerializer이기 때문입니다.
    • 그래서 RedisTemplate는 객체를 자동으로 직렬화, 역직렬화 하여 binary 데이터를 redis에 저장을 합니다.

 

2 - 3. Entity / Repository

Repository로 사용하면 데이터를 Entity로 변환하여 쉽게 사용할 수 있습니다.

저는 Redis를 프로젝트에 메일 인증 인증번호 유효기간을 위해 사용하였습니다.

 

[ Entity ]

Entity로 사용하기 위해 @RedisHash 어노테이션을 추가합니다.

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@RedisHash(value = "email", timeToLive = 360)
public class Mail {

    @Id
    private Long id;

    @Indexed
    private String mail;

    private Integer number;

    private LocalDateTime regDate;

    public Mail(String mail, int number) {
        this.mail = mail;
        this.number = number;
        this.regDate = LocalDateTime.now();
    }
}
  • @Id : Id가 키 값이 됩니다.
    • import 주의 
      • import org.springframework.data.annotation.Id; (O)
      • import jakarta.persistence.Id; (X)
    • 생성자가 없으면 자동으로 Id를 생성해서 저장합니다.
  • @Indexed : 값으로 검색을 할 시에 추가합니다.
  • @TimeToLive : 만료시간을 설정합니다. (초단위)
    • 해당 프로젝트에서는 6분후 데이터가 사라집니다.

 

[ Repository ]

public interface RedisRepository extends CrudRepository<Mail, Long> {

    List<Mail> findAllByMail(String mail);
}
  • CrudRepositry를 상송받습니다.
  • findAllByMail 처럼 메소드를 만들어서 사용할 수 있습니다.

 

2 - 4. 실사용

[ 인증 번호 전송시 Redis에 저장]

MailService에서 인증 번호를 보낼 때 Redis에 저장합니다.

public int sendMail(String mail){
    MimeMessage message = createMail(mail);
    javaMailSender.send(message);
    saveRedis(mail, number);
    return number;
}

public void saveRedis(String mail, int number) {
    Mail email = new Mail(mail, number);
    repository.save(email);
}

 

 

[ 인증 번호 체크시 Redis에서 확인]

인증 번호 체크시 Reids에서 데이터를 호출해 확인합니다.

3분이내 받은 값만 인증번호 체크 성공으로 넘어갑니다.

@PutMapping("")
public ResponseEntity<?> checkMail(@RequestBody MailCheckDto mailCheckDto) {
    Member tempMember = Member.builder().username(mailCheckDto.getMail()).build();
    List<Mail> mailList = repository.findAllByMail(mailCheckDto.getMail());
    boolean check = false;
    if (mailList.isEmpty()) {
        throw new CustomIllegalArgumentException("메일 인증번호 체크 에러",
                "진행된 메일 인증번호 체크가 없습니다.", tempMember);
    } else {
        for (Mail mail : mailList) {
            if (mail.getNumber() == mailCheckDto.getNumber()) {
                LocalDateTime now = LocalDateTime.now();
                LocalDateTime regDate = mail.getRegDate();
                Duration duration = Duration.between(regDate, now);
                if (duration.toMinutes() < 3) {
                    log.info("이메일 인증번호 체크 성공");
                    check = true;
                } else {
                    throw new CustomIllegalArgumentException("메일 인증번호 체크 에러",
                            "만료된 인증번호 입니다.", tempMember);
                }
            }
        }
    }

    if (check) {
        return new ResponseEntity<>(true, HttpStatus.OK);
    } else {
        throw new CustomIllegalArgumentException("메일 인증번호 체크 에러",
                "인증번호가 일치하지 않습니다.", tempMember);
    }
}

 

find가 아닌 findAll을 쓴 이유

: 클라이언트(사용자)가 여러번의 인증번호를 전송하고 체크를 요청 했을 때, 일치하는 인증번호를 찾아 로직을 수행하기 위함입니다.