FAQ
일반 질문
섹션 제목: “일반 질문”Q: 값 객체와 엔티티의 차이점은 무엇인가요?
섹션 제목: “Q: 값 객체와 엔티티의 차이점은 무엇인가요?”A: 값 객체는 값으로 식별하고, 엔티티는 고유 ID로 식별합니다.
| 특성 | 값 객체 | 엔티티 |
|---|---|---|
| 식별 | 값으로 식별 | 고유 ID로 식별 |
| 동등성 | 모든 속성이 같으면 동등 | ID가 같으면 동등 |
| 불변성 | 항상 불변 | 변경 가능 |
| 생명주기 | 없음 | 있음 |
// 값 객체: 값이 같으면 동등var email1 = Email.Create("user@example.com");var email2 = Email.Create("user@example.com");// email1 == email2 (true)
// 엔티티: ID가 같으면 동등var user1 = new User(id: 1, name: "Alice");var user2 = new User(id: 1, name: "Bob");// user1 == user2 (true, 이름이 달라도)Q: Fin와 Validation<Error, T>는 언제 사용하나요?
섹션 제목: “Q: Fin와 Validation<Error, T>는 언제 사용하나요?”A: 검증 간 의존성 여부에 따라 선택합니다.
| 타입 | 실행 방식 | 오류 처리 | 사용 시기 |
|---|---|---|---|
Fin<T> | 순차 (Bind) | 첫 번째 오류에서 중단 | 의존성 있는 검증 |
Validation<Error, T> | 병렬 (Apply) | 모든 오류 수집 | 독립적인 검증 |
// Fin<T>: 순차 검증 - A가 실패하면 B는 실행 안 함ValidateA().Bind(_ => ValidateB()).Bind(_ => ValidateC());
// Validation: 병렬 검증 - 모든 검증 실행, 오류 수집(ValidateA(), ValidateB(), ValidateC()).Apply((a, b, c) => new Result(a, b, c));Q: 값 객체에 비즈니스 로직을 넣어도 되나요?
섹션 제목: “Q: 값 객체에 비즈니스 로직을 넣어도 되나요?”A: 네, 해당 값에 관련된 로직은 값 객체에 포함하는 것이 좋습니다.
public sealed class Money : ComparableValueObject{ public decimal Amount { get; } public string Currency { get; }
// ✅ 적절함: 금액 관련 연산 public Money Add(Money other) => Currency == other.Currency ? new Money(Amount + other.Amount, Currency) : throw new InvalidOperationException("다른 통화");
// ✅ 적절함: 포맷팅 public string ToFormattedString() => $"{Amount:N2} {Currency}";
// ❌ 부적절함: 외부 시스템 의존 public async Task<decimal> GetExchangeRate() { /* API 호출 */ }}Q: private 생성자와 Create 팩토리 메서드를 사용하는 이유는?
섹션 제목: “Q: private 생성자와 Create 팩토리 메서드를 사용하는 이유는?”A: 항상 유효한 상태를 보장하기 위해서입니다. private 생성자는 검증을 우회한 객체 생성을 차단하고, Create 팩토리 메서드는 검증을 통과한 경우에만 인스턴스를 반환합니다.
// ❌ public 생성자: 유효하지 않은 객체 생성 가능public class Email{ public Email(string value) { Value = value; }}var invalid = new Email("not-an-email"); // 유효하지 않음!
// ✅ private 생성자 + Create: 검증 후에만 생성public sealed class Email : SimpleValueObject<string>{ private Email(string value) : base(value) { }
public static Fin<Email> Create(string? value) => CreateFromValidation( ValidationRules<Email>.NotNull(value) .ThenNotEmpty() .ThenMaxLength(255), v => new Email(v));}var result = Email.Create("not-an-email"); // Fail 반환구현 질문
섹션 제목: “구현 질문”Q: EF Core에서 값 객체를 어떻게 저장하나요?
섹션 제목: “Q: EF Core에서 값 객체를 어떻게 저장하나요?”A: 세 가지 방법이 있습니다.
1. OwnsOne (권장)
modelBuilder.Entity<User>() .OwnsOne(u => u.Email, email => { email.Property(e => e.Value).HasColumnName("Email"); });2. Value Converter
modelBuilder.Entity<User>() .Property(u => u.Email) .HasConversion( e => (string)e, s => Email.CreateFromValidated(s));3. OwnsMany (컬렉션)
modelBuilder.Entity<Order>() .OwnsMany(o => o.LineItems);Q: JSON 직렬화에서 값 객체를 어떻게 처리하나요?
섹션 제목: “Q: JSON 직렬화에서 값 객체를 어떻게 처리하나요?”A: JsonConverter를 구현합니다.
public class EmailJsonConverter : JsonConverter<Email>{ public override Email Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { var value = reader.GetString(); return Email.Create(value!) .IfFail(e => throw new JsonException(e.Message)); }
public override void Write(Utf8JsonWriter writer, Email email, JsonSerializerOptions options) { writer.WriteStringValue((string)email); }}Q: 값 객체 생성 시 예외를 던져도 되나요?
섹션 제목: “Q: 값 객체 생성 시 예외를 던져도 되나요?”A: 예외 대신 Fin<T>나 Validation을 반환합니다. 예외는 검증된 값에서만 내부용으로 허용합니다.
// ❌ 예외 사용public static Email Create(string value){ if (!IsValid(value)) throw new ArgumentException("Invalid email"); return new Email(value);}
// ✅ 결과 타입 사용public static Fin<Email> Create(string value){ if (!IsValid(value)) return Error.New("Invalid email"); return new Email(value);}
// ⚠️ 검증된 값에서만 예외 허용 (내부용)public static Email CreateFromValidated(string value) => new(value);Q: 값 객체에 ID를 포함해도 되나요?
섹션 제목: “Q: 값 객체에 ID를 포함해도 되나요?”A: 아니요, ID가 있으면 엔티티입니다.
// ❌ 값 객체에 ID 포함public sealed class Email : SimpleValueObject<string>{ public Guid Id { get; } // 이러면 엔티티!}
// ✅ 값 객체는 값만 포함public sealed class Email : SimpleValueObject<string>{ // ID 없음, 값으로만 식별}성능 질문
섹션 제목: “성능 질문”Q: 값 객체를 많이 생성하면 성능 문제가 있나요?
섹션 제목: “Q: 값 객체를 많이 생성하면 성능 문제가 있나요?”A: 대부분의 경우 문제가 되지 않습니다. 값 객체는 작은 객체이고 .NET GC가 효율적으로 처리합니다. 고성능이 필요하면 record struct로 스택 할당을 고려할 수 있습니다.
// 힙 할당 (class 기반)public sealed class Email : SimpleValueObject<string> { }
// 스택 할당 가능 (struct 기반) - 고성능 필요시public readonly record struct EmailStruct(string Value);Q: GetHashCode()가 자주 호출되면 문제가 되나요?
섹션 제목: “Q: GetHashCode()가 자주 호출되면 문제가 되나요?”A: 불변 객체이므로 해시 코드를 필드에 캐싱하면 됩니다.
public abstract class ValueObject{ private int? _cachedHashCode;
public override int GetHashCode() { return _cachedHashCode ??= ComputeHashCode(); }
private int ComputeHashCode() { return GetEqualityComponents() .Aggregate(17, (hash, obj) => HashCode.Combine(hash, obj?.GetHashCode() ?? 0)); }}테스트 질문
섹션 제목: “테스트 질문”Q: 값 객체 테스트에서 무엇을 검증해야 하나요?
섹션 제목: “Q: 값 객체 테스트에서 무엇을 검증해야 하나요?”A: 생성 검증(유효/무효 입력), 값 동등성(같은 값 동등, 다른 값 비동등, 해시코드 일관성), 불변성(연산 후 원본 변경 없음), 그리고 해당 시 비교/정렬 순서를 검증합니다.
[Fact]public void Create_WithValidEmail_ShouldSucceed(){ var result = Email.Create("user@example.com"); result.IsSucc.ShouldBeTrue();}
[Fact]public void Equals_WithSameValue_ShouldBeTrue(){ var email1 = Email.CreateFromValidated("user@example.com"); var email2 = Email.CreateFromValidated("user@example.com"); email1.ShouldBe(email2);}Q: 아키텍처 테스트로 값 객체 규칙을 강제할 수 있나요?
섹션 제목: “Q: 아키텍처 테스트로 값 객체 규칙을 강제할 수 있나요?”A: 네, ArchUnitNET을 사용합니다.
[Fact]public void ValueObjects_ShouldBeSealed(){ var rule = Classes() .That().AreAssignableTo(typeof(ValueObject)) .Should().BeSealed();
rule.Check(Architecture);}
[Fact]public void ValueObjects_ShouldNotHavePublicConstructors(){ var rule = Classes() .That().AreAssignableTo(typeof(ValueObject)) .Should().NotHavePublicConstructors();
rule.Check(Architecture);}Q: ValidationRules와 raw Validation<Error, T>는 언제 사용하나요?
섹션 제목: “Q: ValidationRules와 raw Validation<Error, T>는 언제 사용하나요?”A: 단일 필드 순차 검증에는 ValidationRules<T>를, 복합 필드 병렬 검증에는 raw Validation을 사용합니다.
| 방식 | 특징 | 사용 시기 |
|---|---|---|
ValidationRules<T> | 타입 이름 자동 포함, 체이닝 | 단일 필드 순차 검증 |
raw Validation<Error, T> | 유연한 조합, Apply/Bind | 복합 필드, 커스텀 로직 |
// ValidationRules<T>: 단일 필드 순차 검증 (간결)public static Fin<Email> Create(string? value) => CreateFromValidation( ValidationRules<Email>.NotNull(value) .ThenNotEmpty() .ThenMaxLength(255), v => new Email(v));
// raw Validation: 복합 값 객체에서 Apply 조합public static Fin<Money> Create(decimal amount, string currency) => CreateFromValidation( (ValidateAmount(amount), ValidateCurrency(currency)) .Apply((a, c) => (a, c)), t => new Money(t.a, t.c));설계 질문
섹션 제목: “설계 질문”Q: 값 객체가 너무 많아지면 복잡해지지 않나요?
섹션 제목: “Q: 값 객체가 너무 많아지면 복잡해지지 않나요?”A: 검증 규칙이 있거나, 여러 곳에서 재사용되거나, 비즈니스 의미가 있는 값에만 적용합니다. 단순 문자열을 개별 타입으로 분리하는 것은 과도합니다.
// ❌ 과도함: 단순 문자열에 별도 타입public sealed class FirstName : SimpleValueObject<string> { }public sealed class LastName : SimpleValueObject<string> { }public sealed class MiddleName : SimpleValueObject<string> { }
// ✅ 적절함: 복합 값 객체로 그룹화public sealed class FullName : ValueObject{ public string First { get; } public string Last { get; } public string? Middle { get; }}Q: 값 객체 간의 의존성은 어떻게 처리하나요?
섹션 제목: “Q: 값 객체 간의 의존성은 어떻게 처리하나요?”A: 이미 검증된 값 객체를 속성으로 포함하는 합성 방식을 사용합니다.
public sealed class Order : ValueObject{ public OrderId Id { get; } // 다른 값 객체 public Money TotalAmount { get; } // 다른 값 객체 public ShippingAddress Address { get; } // 다른 값 객체
public static Validation<Error, Order> Create( OrderId id, Money totalAmount, ShippingAddress address) { // 각 값 객체는 이미 유효함 return new Order(id, totalAmount, address); }}마무리
섹션 제목: “마무리”더 많은 질문이 있다면:
- GitHub Issues에 질문 등록
- Stack Overflow에 태그
value-objects,languageext사용 - 커뮤니티 토론 참여