Redis는 key-value 구조로 데이터를 저장하는 In-memory 기반의 데이터베이스 시스템입니다.
Spring boot 에선 어떻게 구현하는지 알아봅시다.
1. 먼저 Redis란?
먼저 Redis가 어떤건지는 블로그 다른 게시글에 작성해놨습니다.
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로 설정을 해보겠습니다.
@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 어노테이션 추가해줍니다.
- 위에서 setKeySerializer, setValueSerializer 설정해주는 이유는
- 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를 생성해서 저장합니다.
- import 주의
- @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을 쓴 이유
: 클라이언트(사용자)가 여러번의 인증번호를 전송하고 체크를 요청 했을 때, 일치하는 인증번호를 찾아 로직을 수행하기 위함입니다.