예약 프로젝트에서는 회원가입 시 입력되는 사용자 ID의 형식 유효성을 검증하고 있습니다.
초기 구현에서는 단일 값 검증이라는 특성을 고려하여 Kotlin의 value class를 활용했습니다.
하지만 value class는 타입 자체를 도메인 전반에 전달할 때 가장 큰 의미를 가지며, 현재 구조처럼 생성 직후 String으로 변환하여 사용하는 경우 타입 안전성이라는 장점을 활용하지 못합니다.
따라서 현재 요구사항에서는 불필요한 래핑 복잡도를 줄이고, 검증 책임을 저장 직전 서비스 계층으로 이동하는 방식으로 개선했습니다.
회원가입 검증에서 중요한 점은 클라이언트의 사전 요청이 아닌 실제 데이터가 저장되는 시점에서 검증이 보장되어야 한다는 점입니다.
기존 구조에서는 다음과 같은 문제가 있었습니다.
/check/validation/{username} API에서만 형식 검증 수행
실제 회원 생성 API(/create)에서는 중복 검사만 수행
클라이언트가 검증 API를 호출하지 않고 직접 요청하면 검증되지 않은 ID 저장 가능
검증 API와 저장 로직의 정제 기준 불일치 가능
따라서, 최종 신뢰 경계인 saveMember()에서 반드시 사용자 ID 검증을 수행하도록 변경했습니다.
object UsernameValidator {
private const val MIN_LENGTH = 4
private const val MAX_LENGTH = 12
private val USERNAME_REGEX =
Regex("(?=.*[A-Z])(?=.*\\d)[A-Za-z\\d@*^]{$MIN_LENGTH,$MAX_LENGTH}")
fun validate(username: String): String {
if (USERNAME_REGEX.matchEntire(username) == null) {
throw ReserveException(
HttpStatus.BAD_REQUEST,
ErrorCode.INVALID_USERNAME
)
}
return username
}
}
Service 코드
fun saveMember(memberRequest: MemberRequest): Member {
val username = UsernameValidator.validate(memberRequest.username)
if (memberRepository.existsByUsername(username)) {
throw ReserveException(
HttpStatus.CONFLICT,
ErrorCode.DUPLICATED_USERNAME
)
}
return memberRepository.save(
Member(
username = username,
password = memberRequest.password
)
)
}
중복 확인 API
fun checkUsername(username: String): String {
val validatedUsername = UsernameValidator.validate(username)
if (memberRepository.existsByUsername(validatedUsername)) {
throw ReserveException(
HttpStatus.CONFLICT,
ErrorCode.DUPLICATED_USERNAME
)
}
return validatedUsername
}
value class를 제거한 이유
Kotlin의 value class는 작은 값을 별도 타입으로 표현하면서 런타임에서는 내부 값으로 대체할 수 있는 기능입니다.
@JvmInline
value class Username(
val value: String
)
위와 같이 사용하면 nullable, 제네릭 타입 인자, 인터페이스 업캐스트 등의 박싱 상황을 제외하고는 JVM 수준에서 내부 값인 String으로 처리됩니다.
즉, 객체 래퍼 생성 없이 타입을 분리할 수 있다는 장점이 있습니다.
하지만 현재 구조에서는 CheckUsername.of(username).username처럼 생성 직후 다시 String으로 변환하고 있습니다.
결과적으로 모두 String 타입으로 유지되기 때문에 Username이라는 별도 타입을 활용한 컴파일 타임 타입 안전성 효과를 얻지 못합니다.
value class가 적합한 경우
향후 사용자 ID를 도메인 전체에서 별도의 타입으로 관리할 필요가 있다면 value class가 적합합니다.
@JvmInline
value class Username(
val value: String
)
data class Member(
val username: Username
)
fun findMember(username: Username)
위처럼 도메인 계층 전체에서 전달한다면 다음과 같은 장점을 얻을 수 있습니다.
다른 String 값과 혼용 방지
함수 파라미터 의미 명확화
컴파일 단계에서 잘못된 값 전달 방지
하지만, 현재 요구사항은 회원가입 입력값 검증이 목적이므로 검증 함수 방식이 더 적합합니다.
검증 정책
사용자 ID는 다음 조건을 만족해야 합니다.
4~12자
영문 대소문자 및 숫자 허용
대문자 최소 1개 포함
숫자 최소 1개 포함
허용하지 않는 특수문자 차단
또한, 입력값을 임의로 변형하는 방식은 사용자가 입력한 값과 실제 저장되는 값이 달라질 수 있으므로 제거했습니다.
회원가입 ID 검증에서는 value class보다 검증 함수 방식이 적합하다고 판단했습니다.
검증 책임을 저장 직전(saveMember)으로 이동
클라이언트 검증 우회 방지
데이터 저장 시 항상 검증 보장
value class 제거
검증 정책 단순화