여름부터 개발을 시작했던 플랭고가 이제 거의 배포 직전이라 자잘한 부분을 수정하고 있다. EC2에 백엔드 서버를 띄우고 애플에서 제공하는 테스트용 배포 플랫폼 TestFlight를 사용해 팀원들에게 앱을 배포하여 여러 기능을 테스트 하던 중 회원 가입 프로세스에 메일 인증 과정을 도입해야 하지 않겠냐는 피드백을 받았다.
문제 상황
메일 인증 로직은 개발을 처음 시작할 땐 분명 투두 리스트에 있었던 건데 개발을 바쁘게 진행하다보니 나중에 해야지 하고 미뤄두다가 결국 못하게 된 기능 중 하나이다. 어차피 배포하려면 꼭 필요한 기능이니 얼른 만들어보자고 마음을 먹었다.
현재는 유저가 앱을 통해 가입을 요청하면 메일 형식에 대한 검증만 수행하고 특별히 서버를 거쳐 메일을 인증하는 과정이 없다. 또한 회원 가입을 위해 필요한 값을 입력하는 중간에 서버와 통신을 하지 않다보니 메일에 대한 중복 체크도 맨 마지막에 가서야 한다는 유저 입장에서 보면 더 큰? 문제도 있다. (유저 입장에서 사용할 메일 입력은 가장 첫번째 단계)
해결 과정
1. 메일 인증 로직 도입
먼저 첫번째로 백엔드에 메일을 인증하는 로직을 추가했다. 앱에서 메일을 입력하고 인증을 요청하면 서버에서는 랜덤 토큰을 생성해 입력한 메일로 보내게 된다.
그럼 이렇게 html과 css로 한땀한땀 작성한 작고 소중한 메일이 오게 된다.
그리고 유저가 서버에 회원 가입 요청 시 앱에서 위 인증 코드를 입력해야만 회원 가입이 성공하도록 검증하는 코드를 추가했다.
2. 토큰 자동 삭제 기능 구현
위와 같이 개발을 마치고 나니 토큰이 얼마 동안 유효해야 할까? 에 대한 고민이 들었다. 이에 대한 제한이 없다면 오늘 메일 인증을 수행하고 발급받은 토큰으로 내일이나 모레 심지어 일주일 뒤에도 가입이 가능하다는 건데 그건 문제가 있을 것 같았다. 그래서 위 방법으로 발급 받은 토큰을 일정 시간이 지나면 삭제하는 로직을 구현하기로 했다.
이를 구현하기 위해 떠오른 방법은 크게 3가지 정도였는데 먼저 두 가지 방법은 1. 스프링 배치를 사용하거나 2. DB의 프로시저를 통해 주기적으로 생성된지 일정 시간이 지난 토큰을 삭제해주는 것이다. 하지만 첫번째 방법의 문제는 스프링 배치를 써본 적이 없다는 것이고 (이거 때문에 배치를 지금 공부하기는 시간이 부족하다고 생각했다) 두번째로 하자니 이메일 토큰을 저장하려고 RDB까지 쓰는게 맞나? 싶었다.
그래서 생각한 세번째 방법은 Redis를 사용하는 것이다. Redis는 key:value 형태로 데이터를 저장하므로 중요한 정보가 아닌 일회성 데이터인 토큰을 간단하게 저장하기에 좋아보였고 일정 시간이 지나면 자동으로 값을 지워주는 기능을 내장하고 있기 때문에 편리하게 사용할 수 있을 것 같았다. 그래서 아래와 같이 Redis에서 사용할 엔티티를 작성하고 TimeToLive
를 30분으로 설정해주어 유저가 메일을 인증한 뒤 30분이 지나면 더 이상 해당 토큰을 사용할 수 없도록 처리해주었다.
@Getter
@RedisHash(value = "emailToken", timeToLive = 30 * 60)
public class EmailToken {
@Id
private String email;
private String tokenValue;
private boolean authenticated;
@Builder
public EmailToken(String email, String tokenValue) {
this.email = email;
this.tokenValue = tokenValue;
this.authenticated = false;
}
...
아래와 같이 인증 코드 저장과 TTL이 잘 적용된 것을 확인할 수 있다.
3. 토큰 검증 API
사실 이대로만 해도 서버 입장에서는 메일이 유효한 유저가 가입되는 것을 보장할 수 있지만 처음에 말했던 문제점처럼 유저 입장에서는 회원 가입의 마지막 단계에 가야만 인증 토큰 유효 여부에 대한 응답을 받을 수 있으므로 회원 가입 첫 단계에서 토큰을 검증할 수 있는 API를 만들어야겠다고 생각했다.
위와 같이 GET 메서드로 메일 토큰에 대해 검증할 수 있도록 해주었다. 서비스에서 Redis에 저장된 토큰과 입력 값을 비교해 StatusResponseDto 라는 상태 값에 대한 DTO를 반환하도록 했고, 이 DTO가 가진 상태 코드에 따라 200 이나 400 코드를 클라이언트로 내려줄 수 있도록 컨트롤러에서는 ResponseEntity를 반환하도록 작성했다.
서비스에서 토큰을 검증하고 검증 결과에 따라 ok()
나 error()
메서드를 통해 상태 DTO를 생성해 반환하게 된다.
이런 식으로 상태를 내려주는게 맞는 방법인지는 모르겠다. 에러 메시지를 추가하거나 기타 보완할 부분이 있을 것같다. 다만 기존에는 토큰이 일치하지 않았을 때 예외를 던지고 기존에 사용하던 공통 예외 처리 로직이 에러 응답을 내리도록 할까도 싶었는데 이 API는 목적이 토큰 검증 이므로 토큰이 일치하지 않는다
라는게 예외 흐름이 아니라고 생각이 들었다. 따라서 공통 예외 처리 로직을 사용하면 정상 흐름을 제어하는데 예외를 사용하는 상황이 될 것 같아서 별도의 상태 DTO를 만들어 응답으로 내려주도록 했다.
결과
결과적으로 앱에서 유저 플로우는 아래와 같이 변경되었다.
사용자 입장에서 확실히 기존 방식보다 편리하게 가입할 수 있을 것 같다. 얼른 배포까지 진행해서 실제 피드백을 받아보고 싶다.