회원가입 기능 설계 - ttasjwi/board-system GitHub Wiki

개요

  • 회원가입을 위해서는 사용자에 관한 정보가 필요한데, 사용자는 우리 서비스에게 다음 파라미터를 전달해야한다.
    • username : 아이디
    • password : 비밀번호
    • email : 이메일
    • nickname : 닉네임

userId, username 의 차이

  • userId
    • 사용자 정보는 관계형 데이터베이스에 저장되어지고, 고유키(PK)가 필요하다. 이 값은 userId 라는 컬럼으로 별도로 관리되어진다.
    • 사용자 고유 식별자로서, 자주 변경되어선 안 된다.
    • 사용자들이 이 값을 알아도 상관없고, 몰라도 상관은 없다. 그래도 직접적으로 사용자가 사용하는 페이지에 노출될 필요는 없다.
    • 예) userId: 12312413
  • username
    • 사용자 입장에서는 데이터베이스에 어떤 식별자로 저장되는지를 기억하는 것은 불편하다.
    • 자신이 기억하기 쉬운 식별명을 가지는 것이 서비스 이용에서 더 편리하다. 사용자 입장에서는 이 값을 아이디로 인식하게 하는 것이 보안상 좋다.
    • username 을 변경할 수 없게 하는 서비스도 있고(네이버 등...), 변경할 수 있게 하는 서비스도 있다.(트위터 등...) 우리 서비스는 변경 가능하게 하려고 한다.
    • 예) username: ttasjwi1234

password (비밀번호)

image

  • 사용자를 로그인 시키려면 비밀번호가 필요한데, 비밀번호 값은 결국 우리 서비스에서 저장해서 관리해야한다.
  • 비밀번호는 민감한 값이므로 데이터베이스 테이블에 그대로 저장해선 안 된다. (보안상의 문제)
    • 혹시 모를 데이터 유출 사고로 인해 데이터가 유출될 수 있다.
    • 데이터 유출은 서비스 외부의 악성 해커들도 가능하지만, 우리 서비스 관리자들이 악의적인 목적으로 사용자의 비밀번호를 악용할 수 있다.
  • 그래서 사용자 비밀번호는 암호화를 해서 관리하고, 사용자가 로그인을 할 때마다 비밀번호 값을 우리 서비스에 전달하여, 이 값이 저장된 값과 일치하는 지 불일치하는 지 확인하도록 한다.
    private val idGenerator: IdGenerator = IdGenerator.create()

    fun create(
        email: String,
        rawPassword: String,
        username: String,
        nickname: String,
        currentTime: AppDateTime
    ): User {
        return User.create(
            userId = idGenerator.nextId(),
            email = email,
            password = passwordEncryptionPort.encode(rawPassword),
            username = username,
            nickname = nickname,
            registeredAt = currentTime,
        )
    }
  • 구체적인 코드를 보면 위와 같다. 회원 생성시, 사용자가 전달한 패스워드는 별도로 인코딩되어 생성된다.

가입 기능 흐름

    @Transactional
    fun register(command: RegisterUserCommand): User {
        checkDuplicate(command)
        checkEmailVerificationAndRemove(command)

        val user = createUser(command)
        userPersistencePort.save(user)
        return user
    }

    private fun checkDuplicate(command: RegisterUserCommand) {
        if (userPersistencePort.existsByEmail(command.email)) {
            throw DuplicateUserEmailException(command.email)
        }
        if (userPersistencePort.existsByUsername(command.username)) {
            throw DuplicateUserUsernameException(command.username)
        }
        if (userPersistencePort.existsByNickname(command.nickname)) {
            throw DuplicateUserNicknameException(command.nickname)
        }
    }

    private fun checkEmailVerificationAndRemove(command: RegisterUserCommand) {
        val emailVerification = emailVerificationPersistencePort.findByEmailOrNull(command.email)
            ?: throw EmailVerificationNotFoundException(command.email)

        // 이메일이 인증됐는지, 그리고 인증이 현재 유효한 지 확인
        emailVerification.throwIfNotVerifiedOrCurrentlyNotValid(command.currentTime)

        // 더 이상 이메일 인증이 필요 없으므로 말소
        emailVerificationPersistencePort.remove(emailVerification.email)
    }

    private fun createUser(command: RegisterUserCommand): User {
        return userCreator.create(
            email = command.email,
            rawPassword = command.rawPassword,
            username = command.username,
            nickname = command.nickname,
            currentTime = command.currentTime,
        )
    }
  • 중복 확인 : email, username, nickname 중복 여부를 확인한다.
  • 이메일 인증 유효성 확인 : email 을 통해 이메일 인증(EmailVerification)을 조회하고 인증이 현재 유효한 지 확인한다.
    • 확인된 이메일 인증은 만료한다.
  • 회원 생성 및 저장