이메일 인증은 메일 주소가 존재하는지 확인하고 실제 메일의 소유 여부를 확인하기 위해 수행한다. 이전 회사에는 이메일 인증을 진행하지 않았는데 가끔씩 아이디나 비밀번호를 까먹었다는 문의를 주시는 고객님이 있어 제공하고 싶었던 기능이였다.
이메일 인증 기능을 구현한다.
먼저 판매자의 상태와 인증을 위한 토큰을 추가하여 이메일 인증을 하지 않으면 로그인할 수 없도록 수정하였다. 인증을 위해 일회성으로 사용되는 토큰을 데이터베이스에 저장하는 것을 선호하지는 않지만 간단하게 구현하기 위하여 임의의 토큰을 생성하여 인증하는 방식으로 구현하였다. 실제 업무에서 구현한다면 캐시 저장소를 이용하거나 Stateless하게 토큰을 생성하는 방식을 고려할 수 있다.
@Entity
class Seller(
...
@Enumerated(EnumType.STRING)
var status : Status = Status.AUTHENTICATION_REQUIRED
) {
...
val token : String = UUID.randomUUID().toString()
fun approve(token: String) {
if(this.token != token) {
throw InvalidAuthenticationTokenException()
}
this.status = Status.ACTIVE
}
fun isActive() : Boolean = this.status == Status.ACTIVE
}
private fun authenticate(email: String, password: String) {
...
if(!seller.isActive()) {
throw AuthenticationRequiredException()
}
}
이메일은 구글 SMTP를 이용하여 메일을 발송하도록 구현하였고 비지니스로직과 결합도를 낮추기 위해 이벤트 기반으로 구성하였다. 자세한 이메일 연동 및 테스트는 이전 포스팅을 참고 바란다.
@Service
class SellerCommandService(
...
private val eventPublisher: EventPublisher
) {
@Transactional
fun welcome(email: String, password: String, name: String): Seller {
...
eventPublisher.publish(SellerCreatedEvent(seller.email, seller.name, seller.token))
...
}
}
회원 가입시 이벤트가 발생하고 정의된 이벤트 리스너를 통해 인증 메일이 발송되도록 한다.
@Component
class SellerEventListener(
private val sender: MailSender
) {
@Value("\${host}")
lateinit var host: String
@EventListener
fun handler(event: SellerCreatedEvent) {
val link = "$host/apis/sellers/approve?email=${event.email}&token=${event.token}"
sender.send(event.email, "[에츠] 회원 인증 메일입니다.", link)
}
}
이메일을 통해 수신한 인증 URL을 접속하면 계정이 승인 되도록 구성하였다.
@Transactional(readOnly = false)
fun approve(email: String, token : String) {
var seller = sellerRepository.findByEmail(email)
?: throw UsernameNotFoundException("존재하지 않는 이메일입니다. : $email")
seller.approve(token)
sellerRepository.save(seller)
}
앞서 구축한 이메일 샌드박스 환경과 연동하여 통합테스트를 작성하였다.
@Test
fun `정상적으로 로그인 하는 경우`() {
val 이메일 = "seok2@kakao.com"
`회원가입 및 인증`(이메일, 셀러_비밀번호, 셀러_이름)
`로그인`(이메일, 셀러_비밀번호) Then {
statusCode(HttpStatus.SC_OK)
header("Authorization", notNullValue())
} Extract {
header("Authorization")
}
}
@Test
fun `이메일 인증을 하지 않은 경우 로그인할 수 없다`() {
val 이메일 = "seok2@kakao.com"
`회원가입`(이메일, 셀러_비밀번호, 셀러_이름)
`로그인`(이메일, 셀러_비밀번호) Then {
statusCode(HttpStatus.SC_FORBIDDEN)
}
}
이메일 인증은 본인 인증이나 인증서를 통한 인증과 달리 가입자 본인 확인이 어렵기 때문에 마케팅이나 알림성 정보를 전달하는 접점을 확보하기 위해 주로 이용 된다.
하지만 과연 이메일도 제대로 입력하지 않는 고객에게 아무리 마케팅을하고 정보를 전달한다고 하여 서비스를 제대로 이용할까하는 의문이 들었다.
서비스에 가입해주셔서 감사하지만.......
너무나 당연하게 많은 사이트에서 이메일 인증을 요구하니 관성적으로 필요하다고 생각하고 있는 것은 아닐지 고민해보았고 일반적인 고객을 위하여 가입에 불필요한 허들을 둘 필요가 없다고 생각했다.
이에 인증 기능은 다시 제거하고 웰컴 이메일을 발송할 수 있도록 기능으로 변경하였다. 이벤트를 통하여 비지니스로직과의 결합도를 낮춰놓았기때문에 변경은 매우 어렵지 않고 가능했다.
개인적으로 통신사 회원가입시 이메일을 잘못 입력한채 가입하여 몇년째 서비스를 이용할때 마다 아이디 찾기를 이용하는 슬픈 전설이 있다.
위 예제의 사용한 소스 코드는 GitHub에서 확인할 수 있습니다.