[Project] SpringBoot-Redis : 이메일 검증 및 임시 member 저장
Categories: Project
📌 개인적인 공간으로 공부를 기록하고 복습하기 위해 사용하는 블로그입니다.
정확하지 않은 정보가 있을 수 있으니 참고바랍니다 :😸
[틀린 내용은 댓글로 남겨주시면 복받으실거에요]
회원 가입시 인증하는 절차를 위해서 아래 2가지 로직이 필요했고 이를 위해선 Redis를 사용하는 것이 필요했다.
-
이메일 + 인증코드 저장
지난번 SMTP에 이어 이메일 인증 코드를 보낼 때 Redis에 email을 키로 저장 후 Value로 인증 코드를 저장한 다음 사용자가 인증코드 입력시 Redis에 저장된 코드와 비교하여 일치하면 회원가입 완료가 되고 redis에 저장된 값은 삭제된다.
-
이메일 + Member 저장
member가 임시로 redis로 저장되고 이메일 인증이 완료되면 member가 가입 된 후 redis에서는 삭제되고 DB에 저장되는 로직
Redis 설치
노트북에 Ubuntu 설치되어 있어서 Ubuntu 에서 Redis 설치 후 서버를 열었다.
-
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
-
자주 쓰게 되는 명령어
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 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에 객체 데이터를 설정해줄 수 있는 메서드를 추가로 만들어 주어야 한다.
- 추가 설정 필요
-
RedisUtils에 객체를 저장할 수 있는 메서드 추가 생성
- Member는 해시를 사용해서 memberInfo라는 필드에 회원의 정보를 저장하게 설정할 건데 해시를 사용하는 이유는 회원의 정보를 그룹화 할 수 있고 이로 인해 메모리를 효율적으로 사용할 수 있기 때문이다.
-
- 해결
- 해당 메서드를 만들고 나서는 해결이 되었으나 아래에 또 다른 문제가 발생했다.
직렬화 문제 (@JsonBackReference / @JsonManagedReference )
-
원인
양방향 연관관계 매핑시 순환참조를 막기 위해서 연관관계 매핑할 때
@JsonBackReference
와@JsonManagedReference
애너테이션을 달았는데 그게 너무 많아서 어떤 값을 역직렬화 해야하는지 혼선이 와서 그렇다고 한다. - 개선
- 해당 애너테이션 옆에 참조할 이름을 명시해 주면 된다.
- 모든 클래스의 연관관계 매핑을 아래와 같이 수정함.
- 대표적으로 Post 클래스의 연관관계는 아래와 같이 수정했다.
- post와 heart의 연관관계에서는 “post-heart”라고 명시해두었고
-
Heart에도 마찬가지로 “post-heart”라고 명시함
- 대표적으로 Post 클래스의 연관관계는 아래와 같이 수정했다.
-
해결
문제 없이 member 가 잘 만들어진다.
Leave a comment