본문으로 건너뛰기

Apply 병렬 검증

사용자가 회원가입 폼에서 이메일, 비밀번호, 이름, 나이를 모두 잘못 입력했다고 가정합니다. Bind 패턴을 사용하면 이메일 오류만 보고하고 중단됩니다. 사용자는 이메일을 고치고, 다시 제출하고, 이번에는 비밀번호 오류를 받고… 이 과정을 네 번 반복해야 합니다. Apply 패턴은 모든 검증을 동시에 실행하여 모든 에러를 한 번에 수집합니다.

  • 서로 독립적인 검증 규칙들을 동시에 실행하는 Apply 연산자의 병렬 실행 메커니즘을 이해할 수 있습니다.
  • 모든 검증 실패를 한 번에 수집하여 사용자에게 완전한 피드백을 제공하는 에러 수집 패턴을 구현할 수 있습니다.
  • 복수개의 에러를 구조화된 방식으로 처리하는 ManyErrors 타입을 활용할 수 있습니다.

이전 단계인 Bind 순차 검증에서는 의존적인 검증 규칙들을 순차적으로 실행하는 방법을 학습했습니다. 하지만 서로 독립적인 정보들을 검증해야 하는 상황에서는 다른 접근이 필요합니다.

사용자 경험 측면에서, 여러 필드에 잘못된 값을 입력했을 때 모든 문제점을 한 번에 보여주는 것이 훨씬 효율적입니다. 이메일 형식이 틀렸다고 해서 비밀번호나 이름 검증을 중단할 이유가 없습니다. 서로 독립적인 검증들을 동시에 실행하면 전체 검증 시간도 단축할 수 있고, 모든 검증 실패를 한꺼번에 수집하여 구조화된 에러 정보를 제공할 수도 있습니다.

Apply 병렬 검증 패턴은 독립적인 검증 규칙들을 병렬로 실행하여 이 모든 요구를 충족합니다.

Apply는 서로 의존성이 없는 검증 규칙들을 동시에 실행합니다. 어느 하나가 실패해도 나머지 검증은 계속 진행되며, 실패한 결과들은 모두 수집됩니다.

다음 코드는 순차 실행 방식의 한계를 보여줍니다.

// 이전 방식 (문제가 있는 방식) - 순차적으로 실행하여 비효율적
public static Validation<Error, UserRegistration> ValidateOld(string email, string password, string name, string ageInput)
{
var emailResult = ValidateEmail(email);
if (emailResult.IsFail) return emailResult; // 조기 중단으로 다른 검증 생략
var passwordResult = ValidatePassword(password);
if (passwordResult.IsFail) return passwordResult; // 조기 중단으로 다른 검증 생략
// 사용자가 모든 문제를 한 번에 파악할 수 없음
}

LanguageExt에서 Apply 병렬 검증을 구현하는 두 가지 방법이 있습니다.

여러 Validation을 튜플로 묶어서 한 번에 Apply를 호출하는 방식입니다.

public static Validation<Error, (string Email, string Password, string Name, int Age)> Validate(
string email, string password, string name, string ageInput) =>
(ValidateEmailFormat(email), ValidatePasswordStrength(password), ValidateNameFormat(name), ValidateAgeFormat(ageInput))
.Apply((validEmail, validPassword, validName, validAge) =>
(Email: validEmail, Password: validPassword, Name: validName, Age: validAge))
.As();

간결하고 직관적이며, 검증 개수가 명확하게 드러나므로 대부분의 상황에서 권장됩니다.

fun 함수를 사용하여 Currying 방식으로 개별 Apply를 체이닝하는 방식입니다.

using static LanguageExt.Prelude;
public static Validation<Error, (string Email, string Password, string Name, int Age)> Validate(
string email, string password, string name, string ageInput) =>
fun((string e, string p, string n, int a) => (Email: e, Password: p, Name: n, Age: a))
.Map(f => Success<Error, Func<string, string, string, int, (string, string, string, int)>>(f))
.Apply(ValidateEmailFormat(email))
.Apply(ValidatePasswordStrength(password))
.Apply(ValidateNameFormat(name))
.Apply(ValidateAgeFormat(ageInput));

또는 Pure를 사용하여 더 간결하게 작성할 수 있습니다.

public static Validation<Error, (string Email, string Password, string Name, int Age)> Validate(
string email, string password, string name, string ageInput) =>
Pure<Validation<Error>, Func<string, string, string, int, (string, string, string, int)>>(
fun((string e, string p, string n, int a) => (Email: e, Password: p, Name: n, Age: a)))
.Apply(ValidateEmailFormat(email))
.Apply(ValidatePasswordStrength(password))
.Apply(ValidateNameFormat(name))
.Apply(ValidateAgeFormat(ageInput));

이 방식은 Currying을 통한 단계적 적용으로 유연성을 확보하며, 동적으로 검증 개수를 조절할 때 유용합니다.

다음 표는 두 가지 Apply 구현 방법의 특성을 비교합니다.

구분튜플 기반 Applyfun 기반 개별 Apply
코드 간결성간결하고 직관적상대적으로 장황함
타입 추론자동 추론fun이 타입 추론 지원
유연성고정된 검증 개수동적 검증 개수 가능
사용 시기대부분의 경우고급 합성, 동적 파라미터
학습 곡선낮음Currying 이해 필요

권장사항: 일반적인 경우 튜플 기반 Apply를 사용하세요. fun 기반 개별 Apply는 동적으로 검증을 조합해야 하거나 함수형 프로그래밍 패턴을 깊이 활용할 때 고려하세요.

Apply는 모든 검증 실패를 ManyErrors 타입으로 수집합니다. 다음 코드는 수집된 에러를 순회하며 사용자에게 표시하는 방법을 보여줍니다.

// ManyErrors를 통한 복수개 에러 처리
if (error is ManyErrors manyErrors)
{
Console.WriteLine($" → 총 {manyErrors.Errors.Count}개의 검증 실패:");
for (int i = 0; i < manyErrors.Errors.Count; i++)
{
var individualError = manyErrors.Errors[i];
if (individualError is ErrorCodeExpected errorCodeExpected)
{
Console.WriteLine($" {i + 1}. 에러 코드: {errorCodeExpected.ErrorCode}");
Console.WriteLine($" 현재 값: '{errorCodeExpected.ErrorCurrentValue}'");
}
}
}
=== 독립 검증 (Independent Validation) 예제 ===
사용자 등록 값 객체의 모든 검증 규칙을 병렬로 실행합니다.
--- 유효한 사용자 등록 ---
이메일: 'newuser@example.com'
비밀번호: 'newpass123'
이름: '홍길동'
나이: '25'
성공: 사용자 등록이 유효합니다.
→ 등록된 사용자: 홍길동 (newuser@example.com)
→ 모든 독립 검증 규칙을 통과했습니다.
--- 모든 검증 동시 실패 (Apply의 핵심) ---
이메일: ''
비밀번호: 'short'
이름: 'A'
나이: 'abc'
실패:
→ 총 4개의 검증 실패:
1. 에러 코드: DomainErrors.UserRegistration.EmailMissingAt
현재 값: ''
2. 에러 코드: DomainErrors.UserRegistration.PasswordTooShort
현재 값: 'short'
3. 에러 코드: DomainErrors.UserRegistration.NameTooShort
현재 값: 'A'
4. 에러 코드: DomainErrors.UserRegistration.AgeNotNumeric
현재 값: 'abc'

구현 시 세 가지 포인트에 주목합니다. 여러 검증을 튜플로 묶어서 Apply로 병렬 실행하고, 모든 검증 실패를 ManyErrors로 수집하며, 사용자에게 모든 문제점을 한 번에 표시합니다.

02-Apply-Parallel-Validation/
├── Program.cs # 메인 실행 파일
├── ValueObjects/
│ └── UserRegistration.cs # 사용자 등록 값 객체 (Apply 패턴 구현)
├── ApplyParallelValidation.csproj
└── README.md # 메인 문서

UserRegistration 값 객체는 Apply를 사용하여 이메일, 비밀번호, 이름, 나이를 동시에 검증합니다.

public sealed class UserRegistration : ValueObject
{
public string Email { get; }
public string Password { get; }
public string Name { get; }
public int Age { get; }
// Apply를 통한 병렬 검증 구현
public static Validation<Error, (string Email, string Password, string Name, int Age)> Validate(
string email, string password, string name, string ageInput) =>
// 핵심 검증 규칙들을 병렬로 실행 (독립적 유효성 검사)
(ValidateEmailFormat(email), ValidatePasswordStrength(password), ValidateNameFormat(name), ValidateAgeFormat(ageInput))
.Apply((validEmail, validPassword, validName, validAge) =>
(Email: validEmail, Password: validPassword, Name: validName, Age: validAge))
.As();
// 독립적인 검증 메서드들
private static Validation<Error, string> ValidateEmailFormat(string email) =>
!string.IsNullOrWhiteSpace(email) && email.Contains("@") && email.Contains(".")
? email
: DomainErrors.EmailMissingAt(email);
private static Validation<Error, string> ValidatePasswordStrength(string password) =>
password.Length >= 8
? password
: DomainErrors.PasswordTooShort(password);
}

다음 표는 Bind 순차 검증과 Apply 병렬 검증의 차이를 비교합니다.

구분Bind 순차 검증Apply 병렬 검증
실행 방식순차적으로 체이닝하여 실행모든 검증을 동시에 실행
에러 처리첫 번째 실패에서 조기 중단모든 실패를 수집하여 반환
성능조기 중단으로 효율적병렬 실행으로 빠름
사용자 경험한 번에 하나의 문제만 확인모든 문제를 한 번에 확인

다음 표는 Apply 병렬 검증의 장단점을 정리합니다.

장점단점
모든 문제점을 한 번에 확인 가능에러 처리가 복잡함
병렬 실행으로 빠른 검증모든 에러를 메모리에 보관
모든 검증 실패를 수집검증 규칙이 독립적이어야 함

Q1: Apply와 Bind를 언제 선택해야 하나요?

섹션 제목: “Q1: Apply와 Bind를 언제 선택해야 하나요?”

A: 검증 규칙들 간의 의존성을 확인하세요. 서로 독립적이라면 Apply를, 이전 결과가 다음 검증에 영향을 미친다면 Bind를 사용합니다.

Q2: ManyErrors는 어떻게 처리하나요?

섹션 제목: “Q2: ManyErrors는 어떻게 처리하나요?”

A: ManyErrors 타입을 확인한 뒤 복수개의 에러를 순회하면서 각각을 처리합니다. ErrorCodeExpected 타입인지 확인하여 에러 코드와 현재 값을 표시하는 것이 좋습니다.

Q3: 모든 검증이 실패하는 경우는 어떻게 처리하나요?

섹션 제목: “Q3: 모든 검증이 실패하는 경우는 어떻게 처리하나요?”

A: ManyErrors를 통해 모든 실패를 수집하여 사용자에게 완전한 피드백을 제공합니다. 각 에러를 명확하게 구분하여 표시하면 사용자가 모든 문제점을 한 번에 파악하고 수정할 수 있습니다.

지금까지 Bind(순차)와 Apply(병렬)를 각각 독립적으로 살펴보았습니다. 하지만 실제 도메인에서는 독립적인 필드와 의존적인 필드가 하나의 객체에 공존합니다. 다음 장에서는 Apply와 Bind를 조합하여 이런 복합적인 검증 요구사항을 해결합니다.


3장: Apply와 Bind 조합