사용자 관리 도메인 값 객체
사용자 관리 도메인에서 자주 사용되는 값 객체 구현 예제입니다.
학습 목표
섹션 제목: “학습 목표”- Email - 형식 검증 및 정규화가 필요한 단순 값 객체
- Password - 보안 해시와 검증 로직을 캡슐화한 값 객체
- PhoneNumber - 국제 형식을 지원하는 전화번호 값 객체
- Username - 예약어 검증이 포함된 사용자명 값 객체
dotnet run예상 출력
섹션 제목: “예상 출력”=== 사용자 관리 도메인 값 객체 ===
1. Email (이메일)──────────────────────────────────────── 정규화: user@example.com 로컬 파트: user 도메인: example.com 마스킹: u***r@example.com 잘못된 형식: 유효한 이메일 형식이 아닙니다.
2. Password (비밀번호)──────────────────────────────────────── 비밀번호 생성: 성공 표시: ******** 검증(맞음): True 검증(틀림): False 약한 비밀번호: 비밀번호는 대문자, 소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다.
3. PhoneNumber (전화번호)──────────────────────────────────────── 정규화: +821012345678 포맷팅: 010-1234-5678 국가 코드: +82 마스킹: +82 ***-****-5678
4. Username (사용자명)──────────────────────────────────────── 사용자명: john_doe123 예약어: 'admin'은(는) 예약된 사용자명입니다. 잘못된 형식: 사용자명은 영문자로 시작하고, 영문자, 숫자, 밑줄(_), 하이픈(-)만 포함할 수 있습니다.값 객체 설명
섹션 제목: “값 객체 설명”이메일 주소를 표현하는 단순 값 객체입니다.
특징:
- 정규 표현식을 통한 형식 검증
- 소문자로 정규화하여 일관성 유지
- LocalPart와 Domain 분리 접근
- 마스킹 기능 (개인정보 보호)
public static Fin<Email> Create(string? value){ if (string.IsNullOrWhiteSpace(value)) return DomainErrors.Empty;
var normalized = value.Trim().ToLowerInvariant();
if (normalized.Length > 254) return DomainErrors.TooLong;
if (!Pattern.IsMatch(normalized)) return DomainErrors.InvalidFormat;
return new Email(normalized);}Password
섹션 제목: “Password”비밀번호를 표현하는 값 객체입니다. 평문이 아닌 해시값을 저장합니다.
특징:
- 비밀번호 강도 검증 (대/소문자, 숫자, 특수문자)
- 해시 기반 저장 (평문 노출 방지)
- 검증 메서드 제공
- ToString에서 마스킹 출력
public static Fin<Password> Create(string? plainText){ // 강도 검증 var hasUpperCase = plainText.Any(char.IsUpper); var hasLowerCase = plainText.Any(char.IsLower); var hasDigit = plainText.Any(char.IsDigit); var hasSpecialChar = plainText.Any(c => !char.IsLetterOrDigit(c));
var score = new[] { hasUpperCase, hasLowerCase, hasDigit, hasSpecialChar } .Count(x => x);
if (score < 3) return DomainErrors.WeakPassword;
return new Password(HashPassword(plainText));}PhoneNumber
섹션 제목: “PhoneNumber”국제 형식을 지원하는 전화번호 값 객체입니다.
특징:
- 국가 코드와 국내 번호 분리
- 다양한 입력 형식 정규화
- 로케일별 포맷팅
- 마스킹 기능
public static Fin<PhoneNumber> Create(string? value, string defaultCountryCode = "82"){ if (string.IsNullOrWhiteSpace(value)) return DomainErrors.Empty;
var digits = new string(value.Where(char.IsDigit).ToArray());
// 국내 번호 0 제거 if (digits.StartsWith("0")) digits = digits[1..];
if (digits.Length < 9 || digits.Length > 11) return DomainErrors.InvalidFormat;
return new PhoneNumber(defaultCountryCode, digits);}Username
섹션 제목: “Username”예약어 검증이 포함된 사용자명 값 객체입니다.
특징:
- 형식 규칙 (영문자로 시작, 특수문자 제한)
- 예약어 목록 검증
- 소문자로 정규화
- 길이 제한
private static readonly HashSet<string> ReservedNames = new(StringComparer.OrdinalIgnoreCase){ "admin", "administrator", "root", "system", "null", "undefined", "api", "www", "mail", "ftp", "support", "help"};
public static Fin<Username> Create(string? value){ // ... if (ReservedNames.Contains(normalized)) return DomainErrors.Reserved(normalized);
return new Username(normalized);}핵심 패턴
섹션 제목: “핵심 패턴”1. 정규화 (Normalization)
섹션 제목: “1. 정규화 (Normalization)”입력값을 일관된 형식으로 변환하여 동등성 비교의 정확성을 보장합니다.
// Email: 소문자 정규화var normalized = value.Trim().ToLowerInvariant();
// PhoneNumber: 숫자만 추출var digits = new string(value.Where(char.IsDigit).ToArray());2. 마스킹 (Masking)
섹션 제목: “2. 마스킹 (Masking)”개인정보 보호를 위해 민감한 정보를 가립니다.
public string Masked => $"{local[0]}***{local[^1]}@{Domain}";3. 보안 해싱 (Security Hashing)
섹션 제목: “3. 보안 해싱 (Security Hashing)”비밀번호와 같은 민감한 데이터는 해시로 저장합니다.
private static string HashPassword(string plainText){ using var sha256 = SHA256.Create(); var bytes = Encoding.UTF8.GetBytes(plainText + "salt"); var hash = sha256.ComputeHash(bytes); return Convert.ToBase64String(hash);}4. 예약어 검증 (Reserved Word Validation)
섹션 제목: “4. 예약어 검증 (Reserved Word Validation)”시스템에서 특별한 의미를 가지는 값을 제외합니다.
private static readonly HashSet<string> ReservedNames = new(StringComparer.OrdinalIgnoreCase){ "admin", "administrator", "root", "system"};FAQ
섹션 제목: “FAQ”Q1: Email에서 소문자로 정규화하는 이유는 무엇인가요?
섹션 제목: “Q1: Email에서 소문자로 정규화하는 이유는 무엇인가요?”A: RFC 표준상 이메일의 로컬 파트는 대소문자를 구분할 수 있지만, 대부분의 메일 서버는 대소문자를 구분하지 않습니다. 소문자로 정규화하면 User@Example.com과 user@example.com이 동일한 값 객체로 인식되어 동등성 비교가 정확해집니다.
Q2: Password 값 객체가 해시값을 저장하는 이유는 무엇인가요?
섹션 제목: “Q2: Password 값 객체가 해시값을 저장하는 이유는 무엇인가요?”A: 평문 비밀번호를 메모리에 유지하면 로그 출력이나 직렬화 시 노출될 위험이 있습니다. 값 객체가 생성 시점에 해시로 변환하여 저장하면, ToString()이나 디버거에서도 원본 비밀번호가 노출되지 않습니다.
Q3: Username에서 예약어 목록을 HashSet으로 관리하는 이유는 무엇인가요?
섹션 제목: “Q3: Username에서 예약어 목록을 HashSet으로 관리하는 이유는 무엇인가요?”A: 예약어 검증은 사용자 등록 시마다 실행됩니다. List의 Contains는 O(n)이지만 HashSet의 Contains는 O(1)이므로, 예약어가 많아져도 성능이 일정합니다. StringComparer.OrdinalIgnoreCase를 사용하여 대소문자 구분 없이 비교합니다.
Q4: 마스킹 기능은 모든 값 객체에 필요한가요?
섹션 제목: “Q4: 마스킹 기능은 모든 값 객체에 필요한가요?”A: 아닙니다. 개인정보가 포함된 값 객체(Email, PhoneNumber, AccountNumber 등)에만 필요합니다. ProductCode나 OrderStatus 같은 비민감 데이터에는 마스킹이 불필요합니다.
다음 장에서는 일정/예약 도메인의 값 객체를 학습합니다.