Spring

[Spring] Spring Boot 이메일 인증 구현하기 + Redis (2)

코냥이 2024. 1. 28. 22:52

 

 

 

 

 

들어가기

이전 포스팅에서 Spring Boot로 이메일 인증 요청 API를 구현했다.

이번에는 인증 코드에 만료 시간을 부여해서 Redis를 통해 인증 코드를 관리하는 로직을 추가하려고 한다.

먼저 Redis에 대해 가볍게 알아보자 !

 

 

 

 

 

 

💡 Redis란?


Redis (Remote Dictionary Server)
: Key-Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터베이스 관리 시스템이다.
데이터베이스, 캐시, 메시지 브로커로 사용되며, 인메모리(In-Memory) 데이터 구조를 가진 저장소이다.

 

 

출처 : Redis 공식 사이트

 

 

 

✔️ 인메모리(In-Memory) 데이터 구조란?

  • 컴퓨터 메인 메모리(RAM)에 데이터를 올려서 사용하는 구조이다.
  • SSD, HDD 같은 저장 공간에서 데이터를 가져오는 것보다 RAM에 올려진 데이터를 가져오는 속도가 훨씬 빠르다.
  • 인메모리 구조 특성상, 서버에 장애가 발생될 경우 데이터 유실이 발생될 수 있다.



✔️ Redis 특징

  • Key-Value 형식이기 때문에 쿼리를 작성할 필요가 없다.
  • 데이터를 디스크에 올리는 방식이 아니라 메모리에서 처리하기 때문에 속도가 빠르다. (인메모리)
  • String, List, Set 등의 자료 구조를 지원한다.
  • 싱글 스레드로, 한 번에 하나의 명령만 처리한다.

 

 

✔️ Redis 사용 시 주의할 점

  • 인메모리 구조를 가지기 때문에 서버에 장애가 발생됐을 경우에 대한 대비를 해야 한다.
  • 메모리 관리가 중요하다.
  • 싱글 스레드 특성상, 한 번에 하나의 명령만 처리하기 때문에 처리하는데 시간이 오래 걸리는 요청/명령은 피해야 한다.

 

 

✔️ Redis 사용 이유

이메일 인증 코드 만료를 위해 Redis 방식을 선택했다. 이유가 무엇일까!

  1. 메모리에 데이터를 저장하기 때문에 빠른 속도로 데이터에 접근할 수 있다. 따라서, Redis를 캐시로 사용하면 매번 디스크 기반 데이터베이스에서 데이터를 읽어오는 비용을 아낄 수 있다.
  2. Redis는 TTL(Time-To-Live) 기능을 제공한다. 따라서, 데이터의 만료 시간을 설정할 수 있기 때문에 인증 코드 관리에 적합하다.

 

 

 

 

💡 Redis로 인증 코드 관리하기


1. docker-compose

  redis:
    container_name: 컨테이너 이름
    image: redis:latest
    ports:
      - 6379:6379
    volumes:
      - ./redis/data:/data
      - ./redis/conf/redis.conf:/usr/local/conf/redis.conf
    labels:
      - "name=redis"
      - "mode=standalone"
    restart: always
    command: redis-server /usr/local/conf/redis.conf

 

 

2. build.gradle

Redis 의존성을 추가한다.

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

 

 

3. application.yml

redis:
  host: localhost
  port: 6379

 

 

4. RedisConfig

@Configuration
public class RedisConfig {

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

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

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

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

 

 

5. RedisRepository

public interface RedisRepository {

    String getData(String key);

    boolean existData(String key);

    void setDataExpire(String key, String value, long timeout);

    void deleteData(String key);
}

 

 

6. RedisRepositoryImpl

@Repository
public class RedisRepositoryImpl implements RedisRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public RedisRepositoryImpl(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public String getData(String key) {
        ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }

    @Override
    public boolean existData(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    @Override
    public void setDataExpire(String key, String value, long timeout) {
        ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        Duration duration = Duration.ofMillis(timeout);
        valueOperations.set(key, value, duration);
    }

    @Override
    public void deleteData(String key) {
        redisTemplate.delete(key);
    }
}

 

 

7. RedisService

@Service
public class RedisService {

    private final RedisRepositoryImpl redisRepositoryImpl;


    public RedisService(RedisRepositoryImpl redisRepositoryImpl) {
        this.redisRepositoryImpl = redisRepositoryImpl;
    }

    @Transactional(readOnly = true)
    public String getData(String key) {
        return redisRepositoryImpl.getData(key);
    }

    @Transactional(readOnly = true)
    public boolean existData(String key) {
        return redisRepositoryImpl.existData(key);
    }

    @Transactional(readOnly = false)
    public void setDataExpire(String key, String value, long timeout) {
        redisRepositoryImpl.setDataExpire(key, value, timeout);
    }

    @Transactional(readOnly = false)
    public void deleteData(String key) {
        redisRepositoryImpl.deleteData(key);
    }
}

 

 

8. EmailController

@GetMapping(value = "/auth", name = "이메일 인증 코드 확인")
public DataResponse<DataResponseCode> checkMailCode(@RequestParam("receiver") String receiver, @RequestParam("code") String code) {
    DataResponse<DataResponseCode> response = emailService.verifyEmailCode(receiver, code);
    return response;
}

 

 

9. EmailService

@Slf4j
@Service
public class EmailService {

	...
    
    private final RedisService redisService;
    
	...
    
    @Transactional(readOnly = false)
    public DataResponse<DataResponseCode> sendMail(String receiver) {
        try {
        	emailCode = createKey();
            MimeMessage message = createMessage(receiver);

            if (redisService.existData(receiver)) { // 기존에 발급 받았던 인증 코드 삭제
                redisService.deleteData(receiver);
            }

            javaMailSender.send(message);

            redisService.setDataExpire(receiver, emailCode, 3 * 60 * 1000L); // 만료 시간 3분

            return new DataResponse<>(UserSignUpResponseCode.SUCCESS);
        } catch (MessagingException | UnsupportedEncodingException | MailException e) {
            e.printStackTrace();
            return new DataResponse<>(UserSignUpResponseCode.MAIL_SEND_FAILED);
        }
    }
    
    /**
     *  메일 인증 코드 확인
     */
    @Transactional(readOnly = true)
    public DataResponse<DataResponseCode> verifyEmailCode(String email, String code) {
        String userMailCode = redisService.getData(email);

        if (userMailCode == null) {
            return new DataResponse<>(UserSignUpResponseCode.EXPIRED_AUTH_MAIL_CODE);
        }

        if (userMailCode.equals(code)) { // 올바른 인증 코드인 경우
            return new DataResponse<>(UserSignUpResponseCode.SUCCESS);
        }

        else { // 올바르지 않은 인증 코드인 경우
            return new DataResponse<>(UserSignUpResponseCode.INVALID_AUTH_MAIL_CODE);
        }
    }
    
    ...
}

 

 

 

 

 

 

출처

 

 

 

긴 글 읽어주셔서 감사합니다 🍀

잘못 작성된 내용은 피드백 주시면 반영하겠습니다 😎