다른 라이브러리와의 비교
January 13, 2026 · View on GitHub
목차
UniFP vs Unity-NOPE
성능 비교
NOPE의 성능문제를 개선했습니다.
1. Zero-GC 구조체 설계
- UniFP: 모든 핵심 타입이
readonly struct로 스택 할당됨 - Unity-NOPE:
Result<T,E>가readonly struct지만, 제네릭 에러 타입E가 박싱을 유발할 수 있음
2. 델리게이트 캐싱
- UniFP:
DelegateCache로 자주 사용되는 람다 재사용 → 힙 할당 방지 - Unity-NOPE: 델리게이트 캐싱 없음 → Update 루프에서 반복 생성
3. ResultPool & ListPool
- UniFP: 고빈도 시나리오를 위한 객체 풀링 내장
- Unity-NOPE: 풀링 메커니즘 없음
기능 비교
NOPE에 핵심적인 기능은 UniFP에도 구현되어있습니다. 다만 이름은 UniFP에서 C#사용자에 더 친숙하게 명명되었습니다.
UniFP의 Then = NOPE의 Bind, UniFP의 Filter = NOPE의 Ensure
고수준 기능 비교
| 기능 | UniFP | Unity-NOPE |
|---|---|---|
| Result 모나드 | Result<T> (단일 타입) | Result<T,E> (타입화된 에러) |
| Option 모나드 | Option<T> | Maybe<T> |
| 비동기 지원 | UniTask + Awaitable | UniTask + Awaitable |
| 에러 타입 | ErrorCode (구조체, 효율적) | E (제네릭, 유연하지만 박싱 가능) |
| 파이프라인 연산 | Then, Map, Filter, Recover, Do... | Bind, Map, Ensure, Tap, Finally... |
| 재시도 로직 | Retry, RetryWithBackoff, Repeat | 미지원 |
| 결과 결합 | ResultCombinators (Combine, Zip...) | Result.Combine, CombineValues |
| 컬렉션 순회 | SelectResults, CombineAll, Partition | 제한적 |
| 성능 최적화 | DelegateCache, Pools, Span 확장 | 기본 구조체만 |
| 디버깅 도구 | Trace, Breakpoint, SafeExecutor | 기본 Match만 |
메서드별 상세 비교
| 메서드 카테고리 | UniFP | Unity-NOPE | 설명 |
|---|---|---|---|
| 기본 변환 | |||
Map | ✅ | ✅ | 성공 시 값 변환 (T → U) |
Bind (Then) | ✅ Then | ✅ Bind | Result를 반환하는 함수 체이닝 (T → Result<U>) |
Filter | ✅ | ⚠️ Ensure | 조건부 검증 (조건 실패 시 Failure) |
| 에러 처리 | |||
MapError | ⚠️ ErrorCode만 | ✅ | 에러 타입 변환 |
Recover | ✅ | ⚠️ OrElse | 실패를 기본값으로 복구 |
IfFailed | ✅ | ⚠️ Or | 실패 시 대체 Result 제공 |
Catch | ✅ | ❌ | 특정 에러를 가로채서 복구 |
| 부수 효과 | |||
Do | ✅ | ⚠️ Tap | 성공 시 부수 효과 실행 (값 변경 없음) |
DoStrict | ✅ | ❌ | 부수 효과 실패 시 파이프라인 중단 |
IfFailed(Action) | ✅ | ❌ | 실패 시만 부수 효과 실행 |
| 조건부 실행 | |||
ThenIf | ✅ | ❌ | 조건부 Then |
MapIf | ✅ | ❌ | 조건부 Map |
Where | ⚠️ Option에만 | ✅ Maybe에만 | 조건 필터링 |
| 결과 검사 | |||
Match | ✅ | ✅ | 성공/실패에 따라 다른 함수 실행 |
Finally | ⚠️ Match 유사 | ✅ | 체인 종료 및 최종 처리 |
Assert | ✅ | ⚠️ Ensure 유사 | 조건 검증 (디버그용) |
| 비동기 (UniTask/Awaitable) | |||
ThenAsync | ✅ | ⚠️ Bind 오버로드 | 비동기 Result 체이닝 |
MapAsync | ✅ | ⚠️ Map 오버로드 | 비동기 값 변환 |
FilterAsync | ✅ | ❌ | 비동기 조건 검증 |
DoAsync | ✅ | ❌ | 비동기 부수 효과 |
TryAsync | ✅ | ⚠️ Of | 예외를 Result로 변환 (비동기) |
| 복원력 | |||
Retry | ✅ | ❌ | 실패 시 재시도 |
RetryAsync | ✅ | ❌ | 비동기 재시도 |
RetryWithBackoff | ✅ | ❌ | 지수 백오프 재시도 |
Repeat | ✅ | ❌ | N번 연속 성공 필요 |
| 결과 결합 | |||
Combine | ✅ | ✅ | 여러 Result 결합 |
Zip | ✅ | ⚠️ CombineValues | 여러 Result를 튜플로 결합 |
CombineAll | ✅ | ❌ | List<Result> → Result<List> |
Partition | ✅ | ❌ | 성공/실패 분리 |
| 컬렉션 확장 | |||
SelectResults | ✅ | ❌ | 컬렉션 → List<Result>, 실패 시 중단 |
FilterResults | ✅ | ❌ | 조건부 필터링 + Result 반환 |
Fold | ✅ | ❌ | 컬렉션 집계 (Result 반환) |
AggregateResults | ✅ | ❌ | 복잡한 집계 로직 |
| 생성 헬퍼 | |||
Success | ✅ | ✅ | 성공 Result 생성 |
Failure | ✅ | ✅ | 실패 Result 생성 |
FromValue | ✅ | ⚠️ implicit | 값에서 Result 생성 |
SuccessIf | ⚠️ Filter 유사 | ✅ | 조건부 성공/실패 생성 |
FailureIf | ⚠️ Filter 반대 | ✅ | 조건부 실패/성공 생성 |
Of | ⚠️ Try | ✅ | 예외 → Result 변환 |
| 안전 연산 | |||
BindSafe | ❌ | ✅ | 예외 처리가 포함된 Bind |
MapSafe | ❌ | ✅ | 예외 처리가 포함된 Map |
TapSafe | ❌ | ✅ | 예외 처리가 포함된 Tap |
| 디버깅 | |||
Trace | ✅ | ❌ | 파이프라인 단계 추적 |
TraceWith | ✅ | ❌ | 커스텀 메시지 추적 |
TraceOnFailure | ✅ | ❌ | 실패 시만 추적 |
Breakpoint | ✅ | ❌ | 디버거 중단점 설정 |
범례:
- ✅ 완전 지원
- ⚠️ 부분 지원 또는 다른 이름으로 제공
- ❌ 미지원
에러 타입화: 99%의 경우 불필요합니다
Unity-NOPE는 Result<T,E>로 에러를 타입화할 수 있지만, Unity 게임 개발에서는 대부분 오버엔지니어링입니다:
왜 타입화된 에러가 불필요한가?
- Unity 게임 로직은 주로 "성공했나? 실패했나?"만 중요
- 에러의 종류보다 메시지가 더 유용 (디버깅/로깅 시)
- 타입화된 에러는 제네릭 파라미터 증가 → 코드 복잡도 상승
- 대부분의 실패는 "리소스 로드 실패", "유효성 검사 실패" 등 단순한 범주
UniFP의 접근: ErrorCode 구조체
// UniFP: 효율적이고 명확한 에러 분류
var result = LoadAsset()
.Filter(x => x != null, ErrorCode.NotFound)
.Then(ValidateAsset); // ErrorCode.ValidationFailed 반환 가능
if (result.IsFailure)
{
Debug.LogError($"[{result.ErrorCode.Category}] {result.Error}");
// [Resource] Asset not found: player_model.prefab
}
1%의 경우: 타입 안전한 에러가 필요할 때
복잡한 도메인 로직에서 정말로 타입화된 에러가 필요하다면?
// 방법 1: 커스텀 ErrorCode 사용
public static class PaymentErrors
{
public static readonly ErrorCode InsufficientFunds = ErrorCode.Custom(1001, "Payment");
public static readonly ErrorCode InvalidCard = ErrorCode.Custom(1002, "Payment");
public static readonly ErrorCode NetworkTimeout = ErrorCode.Custom(1003, "Payment");
}
var paymentResult = ProcessPayment()
.Recover(code => code == PaymentErrors.NetworkTimeout
? RetryPayment()
: RefundUser());
// 방법 2: 판별된 유니온 패턴 (C# 9.0+)
public record PaymentError
{
public record InsufficientFunds(decimal Required, decimal Available) : PaymentError;
public record InvalidCard(string CardNumber) : PaymentError;
public record NetworkTimeout(int Attempts) : PaymentError;
}
// Result의 Error 메시지에 직렬화하여 저장
var result = payment switch
{
PaymentError.InsufficientFunds e =>
Result<Payment>.Failure(ErrorCode.Custom(1001, "Payment"),
$"부족: {e.Required - e.Available}원"),
// ...
};
UniFP vs language-ext
왜 language-ext를 Unity에 바로 쓰지 않는가?
language-ext는 .NET 생태계 최고의 함수형 라이브러리이지만, Unity에는 적합하지 않습니다
1. Unity 런타임 최적화 부재
- language-ext는 범용 .NET을 위해 설계됨
- 많은 타입이 클래스 기반 → GC 압박 증가
- Unity의 IL2CPP AOT 컴파일과 호환성 이슈 가능
2. 압도적인 기능 복잡도
- 100개 이상의 모나드와 트랜스포머
- Higher-kinded types 시뮬레이션 (복잡한 제네릭 패턴)
- 게임 개발에 불필요한 Parsec, Lenses, Free monads 등
3. 학습 곡선
- Haskell 스타일 명명 규칙 (
camelCase정적 함수) - Trait 시스템의 복잡한 추상화
- Unity 개발자에게 낯선 함수형 개념 과다
4. 성능 오버헤드
- 고도의 추상화로 인한 간접 호출
- Unity 프로파일러에서 핫패스 식별 어려움