|
| 1 | +# UniFP AI 코딩 가이드 |
| 2 | + |
| 3 | +이 문서는 AI 코딩 에이전트가 UniFP 프로젝트를 효과적으로 작업할 수 있도록 가이드를 제공합니다. |
| 4 | + |
| 5 | +## 프로젝트 개요 |
| 6 | + |
| 7 | +**UniFP**는 Unity를 위한 함수형 프로그래밍 라이브러리입니다. Result 모나드와 파이프라인 패턴을 통해 안전하고 명시적인 에러 처리를 제공합니다. |
| 8 | + |
| 9 | +### 핵심 컨셉트 |
| 10 | +- **Result 모나드**: 성공(Success) 또는 실패(Failure)를 타입으로 표현 |
| 11 | +- **파이프라인 체이닝**: Bind, Map을 통한 함수형 조합 |
| 12 | +- **명시적 에러 처리**: 예외 대신 Result로 에러를 전파 |
| 13 | +- **Zero Allocation**: 구조체 기반 설계로 GC 부담 최소화 |
| 14 | + |
| 15 | +## 프로젝트 구조 |
| 16 | + |
| 17 | +``` |
| 18 | +. |
| 19 | +├── src/ |
| 20 | +│ └── UniFP/ |
| 21 | +│ └── Assets/ |
| 22 | +│ └── Plugins/ |
| 23 | +│ └── UniFP/ # 핵심 라이브러리 코드 |
| 24 | +│ ├── Result.cs # Result<T> 모나드 구현 |
| 25 | +│ ├── Pipe.cs # 파이프라인 유틸리티 |
| 26 | +│ ├── package.json # UPM 패키지 정의 |
| 27 | +│ └── UniFP.asmdef # Assembly Definition |
| 28 | +├── Assets/ # Unity 테스트 프로젝트 |
| 29 | +│ ├── Test.cs # 예제 및 테스트 코드 |
| 30 | +│ └── ... |
| 31 | +├── docs/ # 문서 |
| 32 | +│ ├── getting-started.md |
| 33 | +│ ├── api-reference.md |
| 34 | +│ ├── best-practices.md |
| 35 | +│ └── examples.md |
| 36 | +├── README.md |
| 37 | +├── CHANGELOG.md |
| 38 | +└── LICENSE |
| 39 | +``` |
| 40 | + |
| 41 | +## 아키텍처 원칙 |
| 42 | + |
| 43 | +### 1. 구조체 기반 설계 |
| 44 | +Result<T>는 구조체로 설계되어 스택에 할당됩니다. 이는 GC 부담을 최소화하기 위함입니다. |
| 45 | + |
| 46 | +```csharp |
| 47 | +public readonly struct Result<T> // struct, not class! |
| 48 | +``` |
| 49 | + |
| 50 | +### 2. 불변성 (Immutability) |
| 51 | +모든 연산은 새로운 Result를 반환하며, 기존 Result를 변경하지 않습니다. |
| 52 | + |
| 53 | +```csharp |
| 54 | +// 각 연산은 새로운 Result를 반환 |
| 55 | +var result1 = Result<int>.Success(10); |
| 56 | +var result2 = result1.Map(x => x * 2); // result1은 변경되지 않음 |
| 57 | +``` |
| 58 | + |
| 59 | +### 3. 널 체크 최소화 |
| 60 | +가독성과 성능을 위해 불필요한 널 체크는 하지 않습니다. 대신 Filter를 사용합니다. |
| 61 | + |
| 62 | +```csharp |
| 63 | +// ❌ 피할 것 |
| 64 | +if (input == null) return Failure("null"); |
| 65 | +if (input.Length == 0) return Failure("empty"); |
| 66 | + |
| 67 | +// ✅ 권장 |
| 68 | +Pipe.Start(input) |
| 69 | + .Filter(x => x?.Length > 0, "입력이 유효하지 않습니다") |
| 70 | +``` |
| 71 | + |
| 72 | +## 코드 작성 규칙 |
| 73 | + |
| 74 | +### 1. 함수 시그니처 |
| 75 | +- 에러가 발생할 수 있는 함수는 `Result<T>` 반환 |
| 76 | +- 순수 변환 함수는 `T` 반환 (Map에서 사용) |
| 77 | + |
| 78 | +```csharp |
| 79 | +// Result를 반환 (Bind용) |
| 80 | +Result<int> ParseInt(string s) |
| 81 | +{ |
| 82 | + return int.TryParse(s, out var value) |
| 83 | + ? Result<int>.Success(value) |
| 84 | + : Result<int>.Failure("파싱 실패"); |
| 85 | +} |
| 86 | + |
| 87 | +// 값을 반환 (Map용) |
| 88 | +string FormatAge(int age) => $"{age}세"; |
| 89 | +``` |
| 90 | + |
| 91 | +### 2. 에러 메시지 |
| 92 | +명확하고 구체적인 에러 메시지를 작성합니다. |
| 93 | + |
| 94 | +```csharp |
| 95 | +// ❌ 피할 것 |
| 96 | +Result<T>.Failure("Error") |
| 97 | +Result<T>.Failure("Invalid") |
| 98 | + |
| 99 | +// ✅ 권장 |
| 100 | +Result<T>.Failure("파일을 찾을 수 없습니다: {path}") |
| 101 | +Result<T>.Failure("나이는 0보다 커야 합니다") |
| 102 | +``` |
| 103 | + |
| 104 | +### 3. 주석 작성 |
| 105 | +모든 public 메서드에는 XML 문서 주석을 작성합니다. |
| 106 | + |
| 107 | +```csharp |
| 108 | +/// <summary> |
| 109 | +/// 문자열을 정수로 파싱합니다. |
| 110 | +/// </summary> |
| 111 | +/// <param name="s">파싱할 문자열</param> |
| 112 | +/// <returns> |
| 113 | +/// Success: 파싱된 정수 |
| 114 | +/// Failure: 파싱 실패 메시지 |
| 115 | +/// </returns> |
| 116 | +public Result<int> ParseInt(string s) |
| 117 | +``` |
| 118 | + |
| 119 | +### 4. Region 사용 |
| 120 | +생성자는 `#region Construction`으로 그룹화합니다. |
| 121 | + |
| 122 | +```csharp |
| 123 | +#region Construction |
| 124 | +private Result(bool isSuccess, T value, string error) |
| 125 | +{ |
| 126 | + _isSuccess = isSuccess; |
| 127 | + _value = value; |
| 128 | + _error = error; |
| 129 | +} |
| 130 | +#endregion |
| 131 | +``` |
| 132 | + |
| 133 | +## 일반적인 패턴 |
| 134 | + |
| 135 | +### 파이프라인 구성 |
| 136 | + |
| 137 | +```csharp |
| 138 | +// 기본 패턴 |
| 139 | +var result = Pipe.Start(initialValue) |
| 140 | + .Bind(Step1) // Result<T> 반환 |
| 141 | + .Map(Step2) // T 반환 |
| 142 | + .Bind(Step3); |
| 143 | + |
| 144 | +// 디버깅 패턴 |
| 145 | +var result = Pipe.Start(initialValue) |
| 146 | + .Do(x => Debug.Log($"단계 1: {x}")) |
| 147 | + .Bind(Process) |
| 148 | + .Do(x => Debug.Log($"단계 2: {x}")); |
| 149 | + |
| 150 | +// 검증 패턴 |
| 151 | +var result = Pipe.Start(input) |
| 152 | + .Filter(x => x > 0, "양수여야 합니다") |
| 153 | + .Filter(x => x < 100, "100보다 작아야 합니다"); |
| 154 | + |
| 155 | +// 복구 패턴 |
| 156 | +var result = Pipe.Start(LoadConfig()) |
| 157 | + .Recover(error => defaultConfig); |
| 158 | +``` |
| 159 | + |
| 160 | +### 예외 처리 |
| 161 | + |
| 162 | +```csharp |
| 163 | +// Try로 예외를 Result로 변환 |
| 164 | +var result = Pipe.Try(() => |
| 165 | +{ |
| 166 | + return JsonUtility.FromJson<Data>(json); |
| 167 | +}); |
| 168 | + |
| 169 | +// 파이프라인에서 사용 |
| 170 | +var result = Pipe.Try(() => LoadFile(path)) |
| 171 | + .Bind(ValidateData) |
| 172 | + .Bind(ProcessData); |
| 173 | +``` |
| 174 | + |
| 175 | +## DI 통합 (VContainer) |
| 176 | + |
| 177 | +### 일반 클래스 |
| 178 | +생성자 주입을 사용하며, 생성자는 Region으로 감쌉니다. |
| 179 | + |
| 180 | +```csharp |
| 181 | +public class UserService |
| 182 | +{ |
| 183 | + private readonly IUserRepository _repository; |
| 184 | + |
| 185 | + #region Construction |
| 186 | + public UserService(IUserRepository repository) |
| 187 | + { |
| 188 | + _repository = repository; |
| 189 | + } |
| 190 | + #endregion |
| 191 | + |
| 192 | + public Result<User> GetUser(int id) |
| 193 | + { |
| 194 | + return Pipe.Start(id) |
| 195 | + .Filter(x => x > 0, "ID는 양수여야 합니다") |
| 196 | + .Bind(_repository.FindById); |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +### MonoBehaviour |
| 202 | +메서드 주입을 사용합니다. |
| 203 | + |
| 204 | +```csharp |
| 205 | +public class UserController : MonoBehaviour |
| 206 | +{ |
| 207 | + private UserService _userService; |
| 208 | + |
| 209 | + [Inject] |
| 210 | + public void Construct(UserService userService) |
| 211 | + { |
| 212 | + _userService = userService; |
| 213 | + } |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +## 테스트 작성 |
| 218 | + |
| 219 | +### 단위 테스트 패턴 |
| 220 | + |
| 221 | +```csharp |
| 222 | +[Test] |
| 223 | +public void MethodName_Condition_ExpectedResult() |
| 224 | +{ |
| 225 | + // Arrange |
| 226 | + var input = "test"; |
| 227 | + |
| 228 | + // Act |
| 229 | + var result = MethodUnderTest(input); |
| 230 | + |
| 231 | + // Assert |
| 232 | + Assert.IsTrue(result.IsSuccess); |
| 233 | + Assert.AreEqual(expectedValue, result.Value); |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +## 피해야 할 안티패턴 |
| 238 | + |
| 239 | +### 1. Result 내부에서 예외 던지기 |
| 240 | +```csharp |
| 241 | +// ❌ 잘못됨 |
| 242 | +public Result<int> Parse(string s) |
| 243 | +{ |
| 244 | + if (string.IsNullOrEmpty(s)) |
| 245 | + throw new ArgumentException(); // Result를 사용하는 의미가 없음! |
| 246 | + // ... |
| 247 | +} |
| 248 | + |
| 249 | +// ✅ 올바름 |
| 250 | +public Result<int> Parse(string s) |
| 251 | +{ |
| 252 | + if (string.IsNullOrEmpty(s)) |
| 253 | + return Result<int>.Failure("입력이 비어있습니다"); |
| 254 | + // ... |
| 255 | +} |
| 256 | +``` |
| 257 | + |
| 258 | +### 2. Result 무시하고 Value 직접 접근 |
| 259 | +```csharp |
| 260 | +// ❌ 잘못됨 |
| 261 | +var result = Process(input); |
| 262 | +var value = result.Value; // 실패 시 예외 발생! |
| 263 | +
|
| 264 | +// ✅ 올바름 |
| 265 | +var result = Process(input); |
| 266 | +if (result.IsSuccess) |
| 267 | + var value = result.Value; |
| 268 | +``` |
| 269 | + |
| 270 | +### 3. 불필요한 중첩 |
| 271 | +```csharp |
| 272 | +// ❌ 잘못됨 |
| 273 | +var result = Pipe.Start(input) |
| 274 | + .Bind(x => |
| 275 | + { |
| 276 | + var temp = Process1(x); |
| 277 | + if (temp.IsSuccess) |
| 278 | + return Process2(temp.Value); |
| 279 | + return Result<int>.Failure(temp.Error); |
| 280 | + }); |
| 281 | + |
| 282 | +// ✅ 올바름 |
| 283 | +var result = Pipe.Start(input) |
| 284 | + .Bind(Process1) |
| 285 | + .Bind(Process2); |
| 286 | +``` |
| 287 | + |
| 288 | +## 빌드 및 배포 |
| 289 | + |
| 290 | +### Unity Package Manager (UPM) |
| 291 | +이 프로젝트는 UPM 패키지로 배포됩니다. 핵심 코드는 `src/UniFP/Assets/Plugins/UniFP`에 있습니다. |
| 292 | + |
| 293 | +### 버전 관리 |
| 294 | +- `package.json`의 version 필드를 업데이트 |
| 295 | +- `CHANGELOG.md`에 변경 사항 기록 |
| 296 | +- Git 태그 생성: `v1.0.0` |
| 297 | + |
| 298 | +## 참고 자료 |
| 299 | + |
| 300 | +- **UniTask**: https://github.com/Cysharp/UniTask - 비동기 처리 패턴 참고 |
| 301 | +- **UniRx**: https://github.com/neuecc/UniRx - Reactive 패턴 참고 |
| 302 | +- **R3**: https://github.com/Cysharp/R3 - 최신 Reactive Extensions |
| 303 | + |
| 304 | +## 자주 묻는 질문 |
| 305 | + |
| 306 | +### Q: 언제 Bind를 사용하고 언제 Map을 사용하나요? |
| 307 | +A: Result를 반환하는 함수는 Bind, 일반 값을 반환하는 함수는 Map을 사용합니다. |
| 308 | + |
| 309 | +### Q: 예외 처리는 어떻게 하나요? |
| 310 | +A: `Pipe.Try()`를 사용하여 예외를 Result로 변환합니다. |
| 311 | + |
| 312 | +### Q: 여러 Result를 합치려면? |
| 313 | +A: 현재는 중첩된 Bind를 사용하거나, 각 Result를 개별적으로 확인한 후 조합합니다. |
| 314 | + |
| 315 | +--- |
| 316 | + |
| 317 | +이 가이드를 따라 일관되고 유지보수하기 쉬운 UniFP 코드를 작성하세요! |
0 commit comments