返回 Skill 列表
extension
分类: 开发与工程无需 API Key

handling-errors-with-erroror

在WPF MVVM的服务层错误处理中实现ErrorOr结果模式。在从服务返回错误而不是抛出异常时使用,或者将ErrorOr与FluentValidation和CommunityToolkit.Mvvm集成时使用。

person作者: jakexiaohubgithub

ErrorOr Result Pattern Guide

ErrorOr 2.x 기반 서비스 레이어 에러 처리 가이드.

NuGet Package

<PackageReference Include="ErrorOr" Version="2.0.*" />
  • .NET 6+ / .NET Standard 2.0 호환
  • ViewModel 프로젝트에서 안전하게 참조 가능 (WPF 의존성 없음)

1. 기본 패턴

Service Layer

namespace MyApp.Core.Services;

public sealed class UserService(IUserRepository repository)
{
    public ErrorOr<User> GetUser(int id)
    {
        var user = repository.FindById(id);

        if (user is null)
            return Error.NotFound("User.NotFound", "사용자를 찾을 수 없습니다.");
            // User not found.

        return user;
    }

    public ErrorOr<Created> CreateUser(string name, string email)
    {
        if (repository.ExistsByEmail(email))
            return Error.Conflict("User.DuplicateEmail", "이미 사용 중인 이메일입니다.");
            // Email is already in use.

        repository.Add(new User(name, email));
        return Result.Created;
    }
}

ViewModel (Match/Switch)

public sealed partial class UserViewModel : ObservableObject
{
    private readonly UserService _userService;

    [ObservableProperty] private string _name = string.Empty;
    [ObservableProperty] private string _errorMessage = string.Empty;
    [ObservableProperty] private bool _hasError;

    [RelayCommand]
    private void LoadUser(int userId)
    {
        _userService.GetUser(userId).Switch(
            user =>
            {
                HasError = false;
                Name = user.Name;
            },
            errors =>
            {
                HasError = true;
                ErrorMessage = errors.First().Description;
            });
    }
}

2. Error Types

| Factory Method | 용도 | |---------------|------| | Error.Failure() | 일반 실패 | | Error.Unexpected() | 예상치 못한 오류 | | Error.Validation() | 유효성 검증 실패 | | Error.Conflict() | 리소스 충돌 | | Error.NotFound() | 리소스 미발견 | | Error.Unauthorized() | 인증 실패 | | Error.Forbidden() | 권한 없음 |

// code, description, metadata 지정
// Specify code, description, metadata
var error = Error.Validation(
    code: "User.InvalidAge",
    description: "나이는 1~150 사이여야 합니다.",
    // Age must be between 1 and 150.
    metadata: new Dictionary<string, object> { ["Field"] = "Age" });

3. Then 체이닝

ErrorOr<string> result = _userService.GetUser(userId)
    .Then(user => user.Email)
    .FailIf(email => string.IsNullOrEmpty(email),
        Error.Validation("User.NoEmail", "이메일이 없습니다."))
    // User has no email.
    .Then(email => email.ToUpperInvariant());

4. FluentValidation 통합

namespace MyApp.Core.Extensions;

public static class FluentValidationExtensions
{
    public static List<Error> ToErrors(this FluentValidation.Results.ValidationResult result)
    {
        return result.Errors
            .Select(e => Error.Validation(e.PropertyName, e.ErrorMessage))
            .ToList();
    }
}

Service에서 사용

public ErrorOr<Updated> UpdateUser(UpdateUserRequest request)
{
    var validation = _validator.Validate(request);

    if (!validation.IsValid)
        return validation.ToErrors();

    // 비즈니스 로직 처리
    // Process business logic
    _repository.Update(request.ToUser());
    return Result.Updated;
}

5. 레이어별 에러 전략

┌─────────────────────────────────────────┐
│  ViewModel Layer                        │
│  ErrorOr<T>.Switch() → UI 상태 업데이트  │
│  ErrorOr<T>.Switch() → Update UI state  │
├─────────────────────────────────────────┤
│  Service Layer                          │
│  return ErrorOr<T> (예외 대신 결과 반환)  │
│  return ErrorOr<T> (result instead of   │
│  exceptions)                            │
├─────────────────────────────────────────┤
│  Infrastructure Layer                   │
│  try-catch → Error 변환                  │
│  try-catch → Convert to Error           │
└─────────────────────────────────────────┘
// Infrastructure → Service 경계에서 예외 변환
// Convert exceptions at Infrastructure → Service boundary
public ErrorOr<User> GetUserFromApi(int id)
{
    try
    {
        var user = _httpClient.GetFromJsonAsync<User>($"/api/users/{id}").Result;
        return user ?? Error.NotFound("User.NotFound", "사용자를 찾을 수 없습니다.");
        // User not found.
    }
    catch (HttpRequestException ex)
    {
        return Error.Unexpected("Api.Error", ex.Message);
    }
}

6. Common Mistakes

| 실수 | 올바른 방법 | |------|------------| | IsError 확인 없이 .Value 접근 | Match/Switch로 양쪽 경로 처리 | | 모든 곳에서 ErrorOr 사용 (과도한 적용) | Service Layer 경계에서만 사용 | | Exception과 ErrorOr 혼용 | 레이어별 명확한 규칙 수립 | | 빈 List<Error> 반환 | 에러 없으면 성공 값 반환 | | Error 객체를 UI에 직접 바인딩 | Error.Description 문자열로 변환 후 바인딩 | | ViewModel에서 성공만 처리 | Switch/Match로 에러 경로 필수 처리 |

Key Rules

  • Service Layer의 반환 타입으로 ErrorOr<T> 사용 (예외 대신)
  • ViewModel에서 Switch/Match로 양쪽 경로 필수 처리
  • FluentValidation 결과는 .ToErrors() 확장 메서드로 변환
  • Infrastructure 예외는 try-catchError 변환
  • ViewModel 프로젝트에서 안전하게 참조 가능 (BCL만 의존)
  • See also: validating-with-fluentvalidation (폼 검증), implementing-wpf-validation (WPF 기본 검증)

참고