[Project] SpringBoot-Redis : 이메일 검증 및 임시 member 저장

Updated:

Categories:

Tags: ,

📌 개인적인 공간으로 공부를 기록하고 복습하기 위해 사용하는 블로그입니다.
정확하지 않은 정보가 있을 수 있으니 참고바랍니다 :😸
[틀린 내용은 댓글로 남겨주시면 복받으실거에요]


회원 가입시 인증하는 절차를 위해서 아래 2가지 로직이 필요했고 이를 위해선 Redis를 사용하는 것이 필요했다.

  1. 이메일 + 인증코드 저장

    지난번 SMTP에 이어 이메일 인증 코드를 보낼 때 Redis에 email을 키로 저장 후 Value로 인증 코드를 저장한 다음 사용자가 인증코드 입력시 Redis에 저장된 코드와 비교하여 일치하면 회원가입 완료가 되고 redis에 저장된 값은 삭제된다.

  2. 이메일 + Member 저장

    member가 임시로 redis로 저장되고 이메일 인증이 완료되면 member가 가입 된 후 redis에서는 삭제되고 DB에 저장되는 로직

Redis 설치

노트북에 Ubuntu 설치되어 있어서 Ubuntu 에서 Redis 설치 후 서버를 열었다.

  1. ubuntu 에서 redis 설치

    1
    2
    3
    4
    5
    6
    
     # apt-get update
     $ sudo apt-get update
     $ sudo apt-get upgrade
        
     # redis 설치
     $ sudo apt-get install redis-server
    
  2. 자주 쓰게 되는 명령어

    1
    2
    3
    4
    5
    6
    7
    
     redis 시작 : sudo systemctl status redis-server
     cli 환경 열기 : redis-cli
        
     모든 키 조회 : keys *
     데이터 지우기 : FLUSHALL
     hash kye field 확인 : hkeys <key>
     hash key value 확인 : hvals <key>
    

Redis 관련 설정 및 클래스 생성

의존성 추가

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

application.yml

1
2
3
redis:
  host: localhost
  port: 6379

RedisConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
public class RedisConfig {
    @Value("${redis.host}")
    private String redisHost;

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // LettuceConnectionFactory를 사용하여 Redis와 연결
        return new LettuceConnectionFactory(redisHost,redisPort);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        return template;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
        return new StringRedisTemplate(connectionFactory);
    }

}

RedisUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequiredArgsConstructor
@Service
@Slf4j
public class RedisUtil {
    private final RedisTemplate<String, Object> redisTemplate;

// 문자열 데이터 설정
    public void setDataWithExpire(String key, String value, long duration) {
        ValueOperations<String, Object> valueOps = redisTemplate.opsForValue();
        Duration expireDuration = Duration.ofSeconds(duration);
        valueOps.set(key, value, expireDuration);
    }

    // 문자열 데이터 조회
    public String getData(String key) {
        ValueOperations<String, Object> valueOps = redisTemplate.opsForValue();
        return (String) valueOps.get(key);
    }
}

이메일 인증코드 검증

EmailService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 public String sendMail(EmailMessage emailMessage, String type) {
        String authNumber = createCode();
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        try {
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
            mimeMessageHelper.setTo(emailMessage.getTo());
            mimeMessageHelper.setSubject(emailMessage.getSubject());
            mimeMessageHelper.setText(setContext(authNumber, type), true);

            // 인증 코드 Redis에 저장
            String hashedEmail = emailMessage.getTo() +":auth";
            redisUtil.setDataWithExpire(hashedEmail, authNumber, authCodeExpiration);

            javaMailSender.send(mimeMessage);
            log.info("Success");
            return authNumber;

        } catch (MessagingException e) {
            log.info("fail");
            throw new RuntimeException(e);
        }
    }
  • Email 보내는 시점에 key는 email+”auth”, value는 authCode 그리고 만료시간을 10분으로 저장하였다.
  • 아래 메서드에서는 코드가 redis에 저장된 코드와 일치하는지 확인한다.

MemberController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @PostMapping("/signup")
    public ResponseEntity postMember(@RequestBody @Valid MemberPostDto postDto) {
        memberService.createMember(mapper.memberPostDtoToMember(postDto));
        return new ResponseEntity<>(HttpStatus.OK);
    }

    // 이메일 인증 창으로 넘어가서 인증코드 입력칸에 인증코드를 입력 후 확인 버튼을 누를때
    @PostMapping("/verify")
    public ResponseEntity registerMember(@RequestBody VerificationDto verificationDto) {
        URI location = URI.create("");
        if (emailService.verifyEmailCode(verificationDto)) {
            Member member = memberService.registerMember(verificationDto);

            location = UriComponentsBuilder
                    .newInstance()
                    .path(MEMBER_DEFAULT_URL + "/{memberId}")
                    .buildAndExpand(member.getMemberId())
                    .toUri();
        }
        return ResponseEntity.created(location).build();
    }
  • memberController에서 email 인 key를 통해 인증코드를 get한 다음, 동일하지 않거나 null일 경우에는 예외를 던지고 동일하다면 true를 반환하게 한다.
  • 그 후 memberController에서 true인지 false인지 판단 한 다음 인증코드를 위해 사용했던 이메일 key는 삭제하고 member등록이 진행된다.
  • 이메일 인증코드가 일치한다면 (true) memberService의 registerMember를 호출 한다.

Member정보를 Redis에 저장

MemberService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Service
public class MemberService {
    private final MemberRepository repository;
    private final PasswordEncoder passwordEncoder;
    private final JwtAuthorityUtils jwtAuthorityUtils;
    private final RedisUtil redisUtil;

    public MemberService(MemberRepository repository, PasswordEncoder passwordEncoder, JwtAuthorityUtils jwtAuthorityUtils, EmailService emailService, RedisUtil redisUtil) {
        this.repository = repository;
        this.passwordEncoder = passwordEncoder;
        this.jwtAuthorityUtils = jwtAuthorityUtils;
        this.redisUtil = redisUtil;
    }

    public void createMember(Member member) {
        verifiedExistNickname(member.getNickname());
        verifiedExistEmail(member.getEmail());

        String key = member.getEmail();
        
        //redis에 저장
        redisUtil.setHashValueWithExpire(key, "memberInfo", member, 600);

    }
    
    //  인증코드 확인 후 회원 등록 여부 결정
    public Member registerMember(VerificationDto verificationDto) {
        String key = verificationDto.getEmail();
        Member member = redisUtil.getHashValue(key, "memberInfo", Member.class);
        if (member == null) {
            throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
        }
				redisTemplate.delete(key);
        member.setPassword(passwordEncoder.encode(member.getPassword()));
        List<String> roles = jwtAuthorityUtils.createRoles(member.getEmail());
        member.setRoles(roles);
        return repository.save(member);
    }
}

  • createMember 메서드에서 key는 email, value로는 member를 저장하고 hashKey로 memberInfo 를 사용한다.
  • 인증코드가 맞다면 registerMember 메서드가 실행되고 redis에서 member를 가져왔을 때 member가 없으면 예외가 던져지고 있다면 member가 DB에 저장되는 로직이다.
  • member가 있다면 redis에서 key를 삭제한다.
  • redis에 member가 저장될 때 아래와 같이 저장되어있다.
    • key

      key는 내가 입력한 email+”:email” 이다.

    • hasykey

    • value

Trouble shooting

ClassCastException 발생

  1. 원인
    • 첫번 째 , 직렬화 문제 직렬화 메서드 생성
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
     private void setSerializers(RedisTemplate<?, Object> template) {
         // key와 hashKey는 StringRedisSerializer를 사용
         StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
         template.setKeySerializer(stringRedisSerializer);
         template.setHashKeySerializer(stringRedisSerializer);
                
         // value와 hashValue는 Jackson2JsonRedisSerializer를 사용
         Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
         ObjectMapper objectMapper = new ObjectMapper();
         objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
         objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
         objectMapper.registerModule(new JavaTimeModule());
                
         jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
                
         template.setValueSerializer(jackson2JsonRedisSerializer);
         template.setHashValueSerializer(jackson2JsonRedisSerializer);
                
         template.afterPropertiesSet();
             }
    
    • 두번 째 , Redis 설정할 때 이메일 검증때 만들었던 Temaplate을 그대로 사용하려 해서 오류가 발생하였다.

    • Member 라는 객체를 String 으로 저장하려고 해서 ClassCastException이 발생했고 해결하기 위해서는 RedisUtils에 객체 데이터를 설정해줄 수 있는 메서드를 추가로 만들어 주어야 한다.

  2. 추가 설정 필요
    • RedisUtils에 객체를 저장할 수 있는 메서드 추가 생성

      • Member는 해시를 사용해서 memberInfo라는 필드에 회원의 정보를 저장하게 설정할 건데 해시를 사용하는 이유는 회원의 정보를 그룹화 할 수 있고 이로 인해 메모리를 효율적으로 사용할 수 있기 때문이다.
  3. 해결
    • 해당 메서드를 만들고 나서는 해결이 되었으나 아래에 또 다른 문제가 발생했다.


직렬화 문제 (@JsonBackReference / @JsonManagedReference )

  1. 원인

    양방향 연관관계 매핑시 순환참조를 막기 위해서 연관관계 매핑할 때 @JsonBackReference@JsonManagedReference 애너테이션을 달았는데 그게 너무 많아서 어떤 값을 역직렬화 해야하는지 혼선이 와서 그렇다고 한다.

  2. 개선
    • 해당 애너테이션 옆에 참조할 이름을 명시해 주면 된다.
    • 모든 클래스의 연관관계 매핑을 아래와 같이 수정함.
      • 대표적으로 Post 클래스의 연관관계는 아래와 같이 수정했다.
        • post와 heart의 연관관계에서는 “post-heart”라고 명시해두었고

      • Heart에도 마찬가지로 “post-heart”라고 명시함

  3. 해결

    문제 없이 member 가 잘 만들어진다.

Project 카테고리 내 다른 글 보러가기

Leave a comment