From af8573d631bd72bb7c132905d029ff34a731fa25 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:06:09 +0900 Subject: [PATCH 01/12] =?UTF-8?q?chore:=20Draw.io=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + Nexa.drawio | 216 ---------------------------------------------------- 2 files changed, 2 insertions(+), 216 deletions(-) delete mode 100644 Nexa.drawio diff --git a/.gitignore b/.gitignore index 1808d7e..ec0fc8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +*.drawio +*.drawio.bkp .build/ /Packages xcuserdata/ diff --git a/Nexa.drawio b/Nexa.drawio deleted file mode 100644 index a8050ab..0000000 --- a/Nexa.drawio +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From fe48d80c8d56005a91357e5a7ed980ed0e14dc21 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:38:44 +0900 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20=ED=95=9C=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20README=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/rules/general.md | 2 +- .agents/rules/project-workflows.md | 2 +- .agents/workflows.md | 2 +- README.en.md | 489 +++++++++++++++++++++++++++++ README.ko.md | 489 ----------------------------- README.md | 272 ++++++++-------- 6 files changed, 628 insertions(+), 628 deletions(-) create mode 100644 README.en.md delete mode 100644 README.ko.md diff --git a/.agents/rules/general.md b/.agents/rules/general.md index aa09779..cab69e1 100644 --- a/.agents/rules/general.md +++ b/.agents/rules/general.md @@ -11,7 +11,7 @@ - Explicit type annotation only when required. - New Swift file header author as `opfic`. - Clear expression available case prefers `<` or `<=` over `>` or `>=`. -- Public API change requires DocC documentation, public test coverage, and `README.md` or `README.ko.md` review. +- Public API change requires DocC documentation, public test coverage, and `README.md` or `README.en.md` review. - `Sendable`, actor isolation, cancellation, and error mapping as explicit review points for concurrency changes. ## Documentation and response diff --git a/.agents/rules/project-workflows.md b/.agents/rules/project-workflows.md index c2a3348..5fb66ab 100644 --- a/.agents/rules/project-workflows.md +++ b/.agents/rules/project-workflows.md @@ -5,7 +5,7 @@ | Change | Required checks | | --- | --- | | Swift source or test | `swift build`, `swift test`, changed-file SwiftLint when available | -| Public API or DocC | `swift build`, `swift test`, `README.md` and `README.ko.md` review | +| Public API or DocC | `swift build`, `swift test`, `README.md` and `README.en.md` review | | Package manifest | `swift build`, `swift test`, package dependency diff review | | AI workflow documents or agent TOML | Baseline diff, staged diff, untracked Markdown/TOML format checks, role-model-fallback checks | | CI workflow | YAML review and affected workflow check when available | diff --git a/.agents/workflows.md b/.agents/workflows.md index aba62f9..6e34062 100644 --- a/.agents/workflows.md +++ b/.agents/workflows.md @@ -92,7 +92,7 @@ Non-trivial design and implementation work sequence: - Documentation Writer checks actual diff before PR or release text. - User text-only request as direct text output without file creation. -- `README.md` and `README.ko.md` parity review for public API documentation. +- `README.md` and `README.en.md` parity review for public API documentation. ## AI workflow maintenance diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..e0f7f73 --- /dev/null +++ b/README.en.md @@ -0,0 +1,489 @@ +# Nexa + +[![Build](https://github.com/opficdev/Nexa/actions/workflows/build.yml/badge.svg)](https://github.com/opficdev/Nexa/actions/workflows/build.yml) +[![Swift](https://img.shields.io/badge/Swift-6.1-orange?style=flat-square)](https://www.swift.org) +[![Platforms](https://img.shields.io/badge/Platforms-iOS_15%2B_macOS_12%2B-blue?style=flat-square)](https://github.com/opficdev/Nexa) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen?style=flat-square)](https://swift.org/package-manager/) + +[English](README.en.md) | [한국어](README.md) + +Nexa is a SwiftUI-inspired declarative networking library built on `URLSession`. + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [Public API](#public-api) +- [Quick Start](#quick-start) +- [Endpoint API](#endpoint-api) +- [Request Migration](#request-migration) +- [Configuration](#configuration) +- [Transport Metrics](#transport-metrics) +- [Authentication Refresh](#authentication-refresh) +- [Response Cache](#response-cache) +- [Retry Policy](#retry-policy) +- [Development](#development) +- [Testing](#testing) + +## Features + +- [x] Declarative request builders for `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` +- [x] Typed response decoding with Swift Concurrency +- [x] Value-semantic request composition +- [x] Query, header, timeout, body, and JSON encoding support +- [x] Endpoint-based API with compile-time response typing +- [x] Request-level and global interceptor chains +- [x] Built-in authentication and token refresh flow through `NXAuthTokenProvider` +- [x] One shared in-flight token refresh for concurrent `401` responses from the same client +- [x] Retry policies with fixed and exponential backoff +- [x] Response validation and server error decoding +- [x] Memory response cache with optional validator revalidation for successful `GET` responses and in-flight identical requests +- [x] Logger hooks and transport abstraction for testing +- [x] `Sendable` URLSession task metrics observer + +## Requirements + +| Platform | Swift | Installation | +| --- | --- | --- | +| iOS 15.0+ / macOS 12.0+ | Swift 6.1 | [Swift Package Manager](#swift-package-manager) | + +## Installation + +### Swift Package Manager + +Add Nexa to your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/opficdev/Nexa.git", branch: "main") +] +``` + +Then add `Nexa` to your target dependencies: + +```swift +.target( + name: "AppModule", + dependencies: [ + .product(name: "Nexa", package: "Nexa") + ] +) +``` + +## Public API + +Most code starts from `NXAPIClient`, then moves into one `NXRequestBuilder` request path. Use `send()` for `NXRawResponse`, `send(as:)` for an explicit decoded type, or contextual `send()` when the assignment supplies the decoded type. + +The rest of the public surface is made of extension points for auth, logging, testing, retry, validation, and custom error mapping. + +| API | When to use it | Example | +| --- | --- | --- | +| `NXAPIClient` | Main entry point for requests that share one `baseURL` and one configuration | `client.get("/users").send(as: User.self)` | +| `NXRequestBuilder` | When you need a prepared `URLRequest`, `NXRawResponse`, or a decoded `Decodable` response | `try await client.get("/users").send()` | +| `NXTypedRequestBuilder` | Endpoint configuration compatibility boundary | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | +| `NXEndpoint` | When an endpoint definition should be reusable and carry its response type with it | `try await client.send(UserEndpoint(identifier: 1))` | +| `NXClientConfiguration` | When shared headers, transport, logger, auth, encoder, decoder, or interceptors should be configured once | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | +| `NXCache` | When successful unauthenticated `GET` responses should be reused for a TTL or revalidated with validators | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | +| `NXRetryBackoff` | When retry delays need fixed or exponential behavior | `.retry(maxAttempts: 3, backoff: .fixed(0))` | +| `NXRetryJitter` | When local retry delay randomization is needed | `.retry(maxAttempts: 3, jitter: .full)` | +| `NXValidationPolicy` | When the accepted status codes differ from the default `200..<300` | `.validate(.statusCodes([200, 201, 204]))` | +| `NXHTTPTransport` | When you need stubs in tests or want to replace the transport implementation | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | +| `NXHTTPInterceptor` | When you need cross-cutting request behavior such as tracing or header injection | `.intercept(yourInterceptor)` | +| `NXAuthTokenProvider` | When `.authorized()` requests need token lookup and refresh support | `authTokenProvider: yourAuthTokenProvider` | +| `NXServerErrorDecoder` | When failed responses should decode into your own domain error | `serverErrorDecoder: yourServerErrorDecoder` | +| `NXLogger` | When you want structured request lifecycle logging | `logger: yourLogger` | +| `NXNetworkMetricsObserver` | When you want URLSession task timing snapshots without changing logger events | `NXURLSessionTransport(metricsObserver: yourObserver)` | +| `NXRawResponse` | When you need both `Data` and `HTTPURLResponse` directly | `let response = try await client.get("/users").send()` | +| `NXError` | When handling Nexa-specific failures in calling code | `catch let error as NXError` | +| `NXHTTPMethod` | When defining an `NXEndpoint` method | `var method: NXHTTPMethod { .post }` | + +### Which one should I start with? + +Assume `client` below is an `NXAPIClient` that has already been configured. + +Use `NXAPIClient` + `NXRequestBuilder` for most application code: + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +let user = try await client + .get("/users/42") + .send(as: User.self) +``` + +When the destination type supplies the decoding context, `send()` can remain concise: + +```swift +let user: User = try await client + .get("/users/42") + .send() +``` + +Use `NXRequestBuilder` when you want to inspect the request or handle the raw response yourself: + +```swift +import Foundation +import Nexa + +let request = try await client + .post("/users") + .header("X-Trace-Id", UUID().uuidString) + .preparedURLRequest() +``` + +```swift +let response = try await client + .get("/users") + .send() +``` + +Use `NXEndpoint` when the same endpoint shape is reused in several places: + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +struct UserEndpoint: NXEndpoint { + let identifier: Int + + var method: NXHTTPMethod { .get } + var path: String { "/users/\(identifier)" } + + func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { + builder.query("include", "profile") + } +} +``` + +Use the lower-level protocols only when the default behavior is not enough: + +- `NXHTTPTransport`: stubs, mocks, custom network backends +- `NXHTTPInterceptor`: tracing, request mutation, custom flow control +- `NXAuthTokenProvider`: bearer token injection and refresh +- `NXServerErrorDecoder`: server payload to domain error mapping +- `NXLogger`: request lifecycle logging and observability +- `NXNetworkMetricsObserver`: URLSession task timing snapshots + +## Request Flow + +```mermaid +flowchart TB + application[Application] --> client[NXAPIClient] + + subgraph publicAPI[Public API] + client + builder[NXRequestBuilder
NXTypedRequestBuilder] + endpoint[NXEndpoint] + client --> builder + client --> endpoint + endpoint --> builder + end + + subgraph core[Core Model] + configuration[NXClientConfiguration] + requestSpec[RequestSpec] + extensionPoints[Extension Points
transport, interceptor, auth, logger, error decoder] + end + + client --> configuration + builder --> requestSpec + + subgraph runtime[Execution Layer] + assembler[NXRequestAssembler] + executor[NXRequestExecutor] + interceptors[NXInterceptorChain
retry, auth, logging, cache] + transport[NXHTTPTransport] + pipeline[NXResponsePipeline] + end + + builder --> assembler --> executor + configuration --> executor + requestSpec --> executor + executor --> interceptors --> transport --> pipeline + pipeline --> rawResponse[Raw Response
NXRawResponse] + pipeline --> decodedResponse[Decoded Response
Response] + + extensionPoints -.-> interceptors + extensionPoints -.-> transport + extensionPoints -.-> pipeline +``` + +## Quick Start + +Nexa keeps request code compact while still exposing auth, retry, validation, and decoding in one flow. + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +let client = NXAPIClient( + configuration: NXClientConfiguration( + baseURL: URL(string: "https://api.example.com")! + ) +) + +let user = try await client + .get("/users/me") + .query("include", "profile") + .accept("application/json") + .send(as: User.self) +``` + +Add `.authorized()` only when the client has an `authTokenProvider`. + +You can also build requests step by step: + +```swift +import Foundation +import Nexa + +struct CreateUserPayload: Encodable { + let name: String +} + +struct User: Decodable { + let id: Int + let name: String +} + +let createdUser = try await client + .post("/users") + .header("X-Trace-Id", UUID().uuidString) + .json(CreateUserPayload(name: "opfic")) + .send(as: User.self) +``` + +## Endpoint API + +If you prefer a Moya-style endpoint abstraction, define an `NXEndpoint` and let Nexa keep the response type attached to the endpoint itself. Its `NXTypedRequestBuilder` configuration contract remains available for source compatibility. + +```swift +import Foundation +import Nexa + +struct User: Decodable { + let id: Int + let name: String +} + +struct UserEndpoint: NXEndpoint { + let identifier: Int + + var method: NXHTTPMethod { .get } + var path: String { "/users/\(identifier)" } + + func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { + builder + .query("include", "profile") + .accept("application/json") + } +} + +let user = try await client.send(UserEndpoint(identifier: 42)) +``` + +## Request Migration + +Nexa 1.3 keeps the existing request APIs available while establishing `NXRequestBuilder` as the one-way method-based request path. + +| Removed call | Replacement | +| --- | --- | +| `client.get("/users").raw()` | `client.get("/users").send()` | +| `client.get("/users", as: User.self).raw()` | `client.get("/users").send()` | +| `client.get("/users").as(User.self).send()` | `client.get("/users").send(as: User.self)` | +| `client.get("/users", as: User.self).send()` | `client.get("/users").send(as: User.self)` | + +Nexa 1.3 removes `NXRequestBuilder.raw()` and `NXTypedRequestBuilder.raw()`. The existing `NXEndpoint.configure(_:)` and `client.send(endpoint)` contracts remain unchanged, but Endpoint requests do not provide a raw-response execution API. Construct a direct `NXRequestBuilder` request when raw response handling is required. This migration does not change empty-body or `204` response behavior. + +## Configuration + +`NXClientConfiguration` centralizes the pieces that usually spread across a custom API layer. + +```swift +import Foundation +import Nexa + +let configuration = NXClientConfiguration( + baseURL: URL(string: "https://api.example.com")!, + headers: [ + "Accept": "application/json" + ], + transport: NXURLSessionTransport(), + logger: NXNoopLogger(), + interceptors: [], + cache: .memory(ttl: 0.3), + serverErrorDecoder: NXDefaultServerErrorDecoder(), + authTokenProvider: nil +) + +let client = NXAPIClient(configuration: configuration) +``` + +Replace the defaults with your own conforming types only when you need custom behavior: + +- `NXLogger` for structured logging +- `NXHTTPInterceptor` for request tracing or mutation +- `NXServerErrorDecoder` for mapping failed responses to domain errors +- `NXAuthTokenProvider` for bearer token injection and refresh + +Nexa currently supports: + +- Global headers and per-request headers +- Raw body and JSON body encoding +- Request-level validation policies +- In-memory reuse of successful unauthenticated `GET` responses within a TTL +- In-flight identical `GET` request reuse while the first request is still running +- Automatic auth header injection for `authorized()` requests +- Token refresh and retry handling +- Custom transports for stubbing and isolated tests + +## Transport Metrics + +`NXURLSessionTransport` can forward one `NXNetworkMetrics` snapshot to an `NXNetworkMetricsObserver` for each `URLSession` task. The snapshot contains task duration, redirect count, transaction count, and ordered `NXNetworkTransactionMetrics` values. + +Each transaction reports DNS, connection, TLS, and request-to-first-byte durations only when both Foundation timestamps exist. A missing timestamp remains `nil`. A reused connection is represented by `isConnectionReused` and can have `nil` DNS or connection durations. + +```swift +import Foundation +import Nexa + +actor MetricsObserver: NXNetworkMetricsObserver { + func record(_ metrics: NXNetworkMetrics) async { + print(metrics.taskDuration ?? 0) + } +} + +let transport = NXURLSessionTransport(metricsObserver: MetricsObserver()) +``` + +Only `NXURLSessionTransport` collects these snapshots. A custom `NXHTTPTransport` and a cache hit do not synthesize metrics. Observer delivery does not delay request completion, and its order relative to `NXLogger` events is not guaranteed. + +## Authentication Refresh + +Concurrent `401` responses from `.authorized()` requests created by one `NXAPIClient` share one in-progress token refresh. Copies of that client and builders derived from it share the same refresh. A separately initialized `NXAPIClient` has an independent refresh lifetime. + +`NXAuthTokenProvider` keeps the same `currentAccessToken()` and `refreshAccessToken()` requirements. Each request retries at most once after a non-`nil` refresh result. A `nil` refresh result returns the request's original `401` response, and a refresh error follows Nexa's existing error mapping. + +Cancelling one caller does not cancel the shared refresh or the other waiting requests. `NXAuthRefreshLog` is emitted once per actual refresh; its `requestIdentifier` is the identifier of the request that started that refresh. + +## Response Cache + +`NXCache.memory(ttl:)` reuses successful unauthenticated `GET` responses for the given TTL and combines identical requests while the first request is in progress. It does not add conditional headers after the TTL expires. + +`NXCache.revalidatingMemory(ttl:)` keeps the same cache behavior and additionally revalidates an expired cached `200` response when it has an `ETag` or `Last-Modified` header. Nexa adds `If-None-Match` and `If-Modified-Since` after creating the cache key. A bodyless `304 Not Modified` with the matching validator returns the cached body as a `200` response. A different validator or a body-bearing `304` discards the stale response and follows normal response validation. A changed `200` replaces the cached body and validator. Cached `201`, `204`, and other successful non-`200` responses keep the normal TTL-expiry reload path. + +```swift +let client = NXAPIClient( + configuration: NXClientConfiguration( + baseURL: URL(string: "https://api.example.com")!, + cache: .revalidatingMemory(ttl: 300) + ) +) +``` + +The cache and in-flight request store belong to an `NXAPIClient` instance. Keep a client in a service or dependency container when requests should share cache state. Creating a new `NXAPIClient(configuration:)` creates an independent store. A copy of an existing client value shares the original store. + +```swift +struct UserService { + private let client: NXAPIClient + + init(client: NXAPIClient) { + self.client = client + } +} +``` + +Nexa does not implement `Vary`, disk cache, full `Cache-Control` interpretation, or stale-if-error behavior. Adding `.revalidatingMemory(ttl:)` adds an `NXCache` enum case, so update exhaustive `switch` statements over `NXCache` when recompiling. + +## Retry Policy + +`.retry(...)` retries `GET`, `HEAD`, `PUT`, `DELETE`, and `OPTIONS` by default when a configured retryable status code or transport error occurs. It uses three attempts when `maxAttempts` is omitted. `POST` and `PATCH` remain single-attempt requests unless you explicitly add them through `allowing` for an endpoint that safely accepts repeated requests. + +For retryable `429` and `503` responses, Nexa accepts `Retry-After` delay seconds and HTTP-date values. A valid server value replaces local backoff, is capped by `maximumServerDelay` (60 seconds by default), and is recorded through `NXRetryLog`. `NXRetryJitter.full` changes only local backoff delays and never shortens a server-provided delay. + +```swift +let user = try await client + .post("/users") + .retry( + maxAttempts: 3, + backoff: .fixed(0), + allowing: [.post], + maximumServerDelay: 30, + jitter: .none + ) + .send(as: User.self) +``` + +## Nexa 1.3 Migration + +The public `NXRetryPolicy` constructor, `NXRetryPolicy.Backoff`, `NXRetryPolicy.Jitter`, and `.retry(_:)` are removed in Nexa 1.3. Nexa keeps `NXRetryPolicy` as an internal implementation detail. Use `.retry(maxAttempts:backoff:retryableStatusCodes:allowing:maximumServerDelay:jitter:)` with `NXRetryBackoff` and `NXRetryJitter` instead; `maxAttempts` defaults to `3`. + +## Interceptor Method Contract + +`NXHTTPInterceptor.replacingRequest(_:)` can change a request URL, headers, and body, but the request method must remain equal to the configured method. A different method ends the chain with `NXError.invalidRequest` before later interceptors, logging, caching, or transport. + +## Development + +Nexa keeps SwiftLint out of the distributable package graph so package consumers do not inherit maintainer lint rules. + +For local library development, use `Examples/NexaClient/NexaClient.xcodeproj`. + +- Build the `NexaClient` target in Xcode to validate the local package integration path +- The app target runs `swiftlint` against the repository root during build +- If `swiftlint` is missing locally, the script phase fails with an installation hint + +This project is a maintainer-only integration host. It is not required for apps that depend on Nexa through Swift Package Manager. + +## Testing + +Nexa was designed to keep request execution testable. `NXHTTPTransport` lets you replace live networking with a custom transport and validate the outgoing request and decoded response. + +```swift +import Foundation +import Nexa + +struct User: Codable, Equatable { + let id: Int + let name: String +} + +struct StubTransport: NXHTTPTransport { + func send(_ request: URLRequest) async throws -> NXRawResponse { + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + + return NXRawResponse( + data: Data(#"{"id":1,"name":"opfic"}"#.utf8), + response: response + ) + } +} + +let client = NXAPIClient( + configuration: NXClientConfiguration( + baseURL: URL(string: "https://example.com")!, + transport: StubTransport() + ) +) + +let user = try await client.get("/users/1").send(as: User.self) +``` diff --git a/README.ko.md b/README.ko.md deleted file mode 100644 index 9406bcb..0000000 --- a/README.ko.md +++ /dev/null @@ -1,489 +0,0 @@ -# Nexa - -[![Build](https://github.com/opficdev/Nexa/actions/workflows/build.yml/badge.svg)](https://github.com/opficdev/Nexa/actions/workflows/build.yml) -[![Swift](https://img.shields.io/badge/Swift-6.1-orange?style=flat-square)](https://www.swift.org) -[![Platforms](https://img.shields.io/badge/Platforms-iOS_15%2B_macOS_12%2B-blue?style=flat-square)](https://github.com/opficdev/Nexa) -[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen?style=flat-square)](https://swift.org/package-manager/) - -[English](README.md) | [한국어](README.ko.md) - -Nexa는 `URLSession` 기반의 SwiftUI 스타일 선언형 네트워킹 라이브러리입니다. - -- [기능](#기능) -- [요구 사항](#요구-사항) -- [설치](#설치) -- [공개 API](#공개-api) -- [빠른 시작](#빠른-시작) -- [Endpoint API](#endpoint-api) -- [요청 전환](#요청-전환) -- [설정](#설정) -- [전송 metrics](#전송-metrics) -- [인증 토큰 갱신](#인증-토큰-갱신) -- [응답 cache](#응답-cache) -- [재시도 정책](#재시도-정책) -- [개발](#개발) -- [테스트](#테스트) - -## 기능 - -- [x] `GET`, `POST`, `PUT`, `PATCH`, `DELETE`를 위한 선언형 요청 빌더 -- [x] Swift Concurrency를 활용한 타입 안전 응답 디코딩 -- [x] 값 타입 기반 요청 조합 -- [x] 쿼리, 헤더, 타임아웃, 바디, JSON 인코딩 지원 -- [x] 컴파일 타임 응답 타입을 갖는 Endpoint 기반 API -- [x] 요청 단위 및 전역 인터셉터 체인 -- [x] `NXAuthTokenProvider`를 통한 인증 및 토큰 갱신 흐름 내장 -- [x] 같은 client의 동시 `401` 응답을 위한 진행 중 토큰 갱신 공유 -- [x] Fixed backoff 및 exponential backoff 기반 재시도 정책 -- [x] 응답 유효성 검사 및 서버 에러 디코딩 -- [x] 성공한 `GET` 응답과 진행 중인 동일 요청을 위한 validator 재검증 선택형 memory response cache -- [x] 로거 훅 및 테스트를 위한 transport 추상화 -- [x] `Sendable` URLSession task metrics observer - -## 요구 사항 - -| 플랫폼 | Swift | 설치 | -| --- | --- | --- | -| iOS 15.0+ / macOS 12.0+ | Swift 6.1 | [Swift Package Manager](#swift-package-manager) | - -## 설치 - -### Swift Package Manager - -`Package.swift`에 Nexa를 추가하세요: - -```swift -dependencies: [ - .package(url: "https://github.com/opficdev/Nexa.git", branch: "main") -] -``` - -그런 다음 타겟 의존성에 `Nexa`를 추가하세요: - -```swift -.target( - name: "AppModule", - dependencies: [ - .product(name: "Nexa", package: "Nexa") - ] -) -``` - -## 공개 API - -대부분의 코드는 `NXAPIClient`에서 시작하여 하나의 `NXRequestBuilder` 요청 경로로 이어집니다. `NXRawResponse`에는 `send()`, 명시적 디코딩 타입에는 `send(as:)`, 대입문이 타입을 제공할 때는 문맥 기반 `send()`를 사용합니다. - -나머지 공개 인터페이스는 인증, 로깅, 테스트, 재시도, 유효성 검사, 커스텀 에러 매핑을 위한 확장 포인트로 구성됩니다. - -| API | 사용 시점 | 예시 | -| --- | --- | --- | -| `NXAPIClient` | 동일한 `baseURL`과 설정을 공유하는 요청의 주 진입점 | `client.get("/users").send(as: User.self)` | -| `NXRequestBuilder` | `URLRequest`, `NXRawResponse`, 또는 디코딩된 `Decodable` 응답이 필요할 때 | `try await client.get("/users").send()` | -| `NXTypedRequestBuilder` | Endpoint 설정 호환성 경계 | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | -| `NXEndpoint` | 엔드포인트 정의를 재사용하고 응답 타입을 함께 관리할 때 | `try await client.send(UserEndpoint(identifier: 1))` | -| `NXClientConfiguration` | 공통 헤더, transport, 로거, 인증, 인코더, 디코더, 인터셉터를 한 번에 설정할 때 | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | -| `NXCache` | 인증이 필요 없는 성공한 `GET` 응답을 TTL 동안 재사용하거나 validator로 재검증할 때 | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | -| `NXRetryBackoff` | 고정 또는 지수 재시도 지연이 필요할 때 | `.retry(maxAttempts: 3, backoff: .fixed(0))` | -| `NXRetryJitter` | local 재시도 지연의 무작위 처리가 필요할 때 | `.retry(maxAttempts: 3, jitter: .full)` | -| `NXValidationPolicy` | 허용할 상태 코드가 기본값(`200..<300`)과 다를 때 | `.validate(.statusCodes([200, 201, 204]))` | -| `NXHTTPTransport` | 테스트용 스텁이 필요하거나 transport 구현을 교체할 때 | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | -| `NXHTTPInterceptor` | 트레이싱이나 헤더 주입처럼 요청 전반에 적용되는 처리가 필요할 때 | `.intercept(yourInterceptor)` | -| `NXAuthTokenProvider` | `.authorized()` 요청에 토큰 조회 및 갱신 기능이 필요할 때 | `authTokenProvider: yourAuthTokenProvider` | -| `NXServerErrorDecoder` | 실패 응답을 도메인 에러로 디코딩할 때 | `serverErrorDecoder: yourServerErrorDecoder` | -| `NXLogger` | 구조화된 요청 생명주기 로깅이 필요할 때 | `logger: yourLogger` | -| `NXNetworkMetricsObserver` | logger event 변경 없이 URLSession task 시간 snapshot이 필요할 때 | `NXURLSessionTransport(metricsObserver: yourObserver)` | -| `NXRawResponse` | `Data`와 `HTTPURLResponse`를 직접 다뤄야 할 때 | `let response = try await client.get("/users").send()` | -| `NXError` | 호출 코드에서 Nexa 고유 오류를 처리할 때 | `catch let error as NXError` | -| `NXHTTPMethod` | `NXEndpoint`의 메서드를 정의할 때 | `var method: NXHTTPMethod { .post }` | - -### 어디서 시작할까요? - -아래에서 `client`는 이미 설정된 `NXAPIClient`라고 가정합니다. - -대부분의 앱 코드에서는 `NXAPIClient`와 `NXRequestBuilder` 조합을 사용할 수 있습니다. - -```swift -import Foundation -import Nexa - -struct User: Decodable { - let id: Int - let name: String -} - -let user = try await client - .get("/users/42") - .send(as: User.self) -``` - -대입 대상 타입이 디코딩 문맥을 제공하면 `send()`를 간결하게 유지할 수 있습니다. - -```swift -let user: User = try await client - .get("/users/42") - .send() -``` - -요청을 직접 확인하거나 원시 응답을 직접 처리할 때는 `NXRequestBuilder`를 사용하면 됩니다. - -```swift -import Foundation -import Nexa - -let request = try await client - .post("/users") - .header("X-Trace-Id", UUID().uuidString) - .preparedURLRequest() -``` - -```swift -let response = try await client - .get("/users") - .send() -``` - -동일한 엔드포인트 형태가 여러 곳에서 재사용될 때는 `NXEndpoint`를 사용하면 됩니다. - -```swift -import Foundation -import Nexa - -struct User: Decodable { - let id: Int - let name: String -} - -struct UserEndpoint: NXEndpoint { - let identifier: Int - - var method: NXHTTPMethod { .get } - var path: String { "/users/\(identifier)" } - - func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { - builder.query("include", "profile") - } -} -``` - -기본 동작으로 충분하지 않을 때만 하위 레벨 프로토콜을 사용하면 됩니다. - -- `NXHTTPTransport`: 스텁, 목, 커스텀 네트워크 백엔드 -- `NXHTTPInterceptor`: 트레이싱, 요청 변환, 커스텀 흐름 제어 -- `NXAuthTokenProvider`: bearer token 주입 및 갱신 -- `NXServerErrorDecoder`: 서버 payload를 도메인 에러로 매핑 -- `NXLogger`: 요청 생명주기 로깅 및 관측성 -- `NXNetworkMetricsObserver`: URLSession task 시간 snapshot - -## 요청 흐름 - -```mermaid -flowchart TB - application[소비자 앱] --> client[NXAPIClient] - - subgraph publicAPI[공개 API] - client - builder[NXRequestBuilder
NXTypedRequestBuilder] - endpoint[NXEndpoint] - client --> builder - client --> endpoint - endpoint --> builder - end - - subgraph core[핵심 모델] - configuration[NXClientConfiguration] - requestSpec[RequestSpec] - extensionPoints[확장 지점
전송, 인터셉터, 인증, 로깅, 오류 디코더] - end - - client --> configuration - builder --> requestSpec - - subgraph runtime[실행 계층] - assembler[NXRequestAssembler] - executor[NXRequestExecutor] - interceptors[NXInterceptorChain
재시도, 인증, 로깅, 응답 캐시] - transport[NXHTTPTransport] - pipeline[NXResponsePipeline] - end - - builder --> assembler --> executor - configuration --> executor - requestSpec --> executor - executor --> interceptors --> transport --> pipeline - pipeline --> rawResponse[원시 응답
NXRawResponse] - pipeline --> decodedResponse[디코딩된 응답
Response] - - extensionPoints -.-> interceptors - extensionPoints -.-> transport - extensionPoints -.-> pipeline -``` - -## 빠른 시작 - -Nexa는 인증, 재시도, 유효성 검사, 디코딩을 하나의 흐름으로 간결하게 표현합니다. - -```swift -import Foundation -import Nexa - -struct User: Decodable { - let id: Int - let name: String -} - -let client = NXAPIClient( - configuration: NXClientConfiguration( - baseURL: URL(string: "https://api.example.com")! - ) -) - -let user = try await client - .get("/users/me") - .query("include", "profile") - .accept("application/json") - .send(as: User.self) -``` - -`.authorized()`는 클라이언트에 `authTokenProvider`가 설정된 경우에만 추가하세요. - -단계별로 요청을 구성할 수도 있습니다: - -```swift -import Foundation -import Nexa - -struct CreateUserPayload: Encodable { - let name: String -} - -struct User: Decodable { - let id: Int - let name: String -} - -let createdUser = try await client - .post("/users") - .header("X-Trace-Id", UUID().uuidString) - .json(CreateUserPayload(name: "opfic")) - .send(as: User.self) -``` - -## Endpoint API - -Moya 스타일의 엔드포인트 추상화를 선호한다면, `NXEndpoint`를 정의하여 응답 타입을 엔드포인트에 직접 연결할 수 있습니다. `NXTypedRequestBuilder` 설정 계약은 소스 호환성을 위해 유지됩니다. - -```swift -import Foundation -import Nexa - -struct User: Decodable { - let id: Int - let name: String -} - -struct UserEndpoint: NXEndpoint { - let identifier: Int - - var method: NXHTTPMethod { .get } - var path: String { "/users/\(identifier)" } - - func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { - builder - .query("include", "profile") - .accept("application/json") - } -} - -let user = try await client.send(UserEndpoint(identifier: 42)) -``` - -## 요청 전환 - -Nexa 1.3은 기존 요청 API를 유지하면서 `NXRequestBuilder`를 단방향 method 기반 요청 경로로 구성합니다. - -| 제거된 호출 | 대체 호출 | -| --- | --- | -| `client.get("/users").raw()` | `client.get("/users").send()` | -| `client.get("/users", as: User.self).raw()` | `client.get("/users").send()` | -| `client.get("/users").as(User.self).send()` | `client.get("/users").send(as: User.self)` | -| `client.get("/users", as: User.self).send()` | `client.get("/users").send(as: User.self)` | - -Nexa 1.3은 `NXRequestBuilder.raw()`와 `NXTypedRequestBuilder.raw()`를 제거합니다. 기존 `NXEndpoint.configure(_:)`와 `client.send(endpoint)` 계약은 유지하지만 Endpoint 요청에는 원시 응답 실행 API가 없습니다. 원시 응답 처리가 필요하면 `NXRequestBuilder`로 요청을 직접 구성해야 합니다. 이 전환은 빈 바디와 `204` 응답 동작을 변경하지 않습니다. - -## 설정 - -`NXClientConfiguration`은 커스텀 API 레이어에 흩어지기 쉬운 설정을 한 곳에 집중시킵니다. - -```swift -import Foundation -import Nexa - -let configuration = NXClientConfiguration( - baseURL: URL(string: "https://api.example.com")!, - headers: [ - "Accept": "application/json" - ], - transport: NXURLSessionTransport(), - logger: NXNoopLogger(), - interceptors: [], - cache: .memory(ttl: 0.3), - serverErrorDecoder: NXDefaultServerErrorDecoder(), - authTokenProvider: nil -) - -let client = NXAPIClient(configuration: configuration) -``` - -커스텀 동작이 필요할 때만 기본값을 직접 구현한 타입으로 교체하세요: - -- `NXLogger`: 구조화된 로깅 -- `NXHTTPInterceptor`: 요청 트레이싱 또는 변환 -- `NXServerErrorDecoder`: 실패 응답을 도메인 에러로 매핑 -- `NXAuthTokenProvider`: bearer token 주입 및 갱신 - -현재 Nexa가 지원하는 기능: - -- 전역 헤더 및 요청별 헤더 -- 원시 바디 및 JSON 바디 인코딩 -- 요청 단위 유효성 검사 정책 -- 성공한 비인증 `GET` 응답의 TTL 내 memory 재사용 -- 첫 요청이 실행 중일 때 진행 중인 동일 `GET` 요청 재사용 -- `.authorized()` 요청에 대한 자동 인증 헤더 주입 -- 토큰 갱신 및 재시도 처리 -- 스터빙 및 격리 테스트를 위한 커스텀 transport - -## 전송 metrics - -`NXURLSessionTransport`는 `URLSession` task마다 `NXNetworkMetrics` snapshot 하나를 `NXNetworkMetricsObserver`에 전달할 수 있습니다. snapshot에는 task duration, redirect 수, transaction 수, 수집 순서를 보존한 `NXNetworkTransactionMetrics` 값이 포함됩니다. - -각 transaction은 Foundation timestamp의 시작과 끝이 모두 있을 때만 DNS, connection, TLS, request 시작부터 first byte까지의 duration을 나타냅니다. timestamp가 없으면 해당 값은 `nil`입니다. 재사용 connection은 `isConnectionReused`로 나타내며 DNS 또는 connection duration이 `nil`일 수 있습니다. - -```swift -import Foundation -import Nexa - -actor MetricsObserver: NXNetworkMetricsObserver { - func record(_ metrics: NXNetworkMetrics) async { - print(metrics.taskDuration ?? 0) - } -} - -let transport = NXURLSessionTransport(metricsObserver: MetricsObserver()) -``` - -snapshot은 `NXURLSessionTransport`만 수집합니다. 커스텀 `NXHTTPTransport`와 cache hit는 metrics를 합성하지 않습니다. observer 전달은 요청 완료를 지연하지 않으며 `NXLogger` event와의 순서는 보장하지 않습니다. - -## 인증 토큰 갱신 - -하나의 `NXAPIClient`에서 생성한 `.authorized()` 요청이 동시에 `401` 응답을 받으면 진행 중인 토큰 갱신 결과 하나를 공유합니다. 해당 client의 값 복사본과 여기서 파생한 builder도 같은 갱신을 공유합니다. 별도로 생성한 `NXAPIClient`는 독립된 갱신 수명을 가집니다. - -`NXAuthTokenProvider`의 `currentAccessToken()`, `refreshAccessToken()` 요구 사항은 바뀌지 않습니다. 각 요청은 `nil`이 아닌 갱신 결과 뒤 최대 한 번만 재전송합니다. `nil` 갱신 결과는 해당 요청의 원래 `401` 응답을 반환하고, 갱신 오류는 Nexa의 기존 오류 매핑을 따릅니다. - -한 호출자의 취소는 공유 갱신이나 다른 대기 요청을 취소하지 않습니다. `NXAuthRefreshLog`는 실제 갱신 한 번당 한 번 기록되고, `requestIdentifier`는 해당 갱신을 시작한 요청의 식별자입니다. - -## 응답 cache - -`NXCache.memory(ttl:)`는 인증이 필요 없는 성공한 `GET` 응답을 지정한 TTL 동안 재사용하고, 첫 요청이 진행 중인 동일 요청을 하나로 합칩니다. TTL 만료 뒤에는 조건부 header를 추가하지 않습니다. - -`NXCache.revalidatingMemory(ttl:)`는 같은 cache 동작에 더해 `ETag` 또는 `Last-Modified` header가 있는 만료 `200` 응답을 재검증합니다. Nexa는 cache key 생성 뒤 `If-None-Match`, `If-Modified-Since`를 추가합니다. validator가 일치하고 body가 없는 `304 Not Modified`는 cached body를 `200` 응답으로 반환합니다. validator가 다르거나 body가 있는 `304`는 만료 응답을 제거하고 일반 응답 유효성 검사로 처리합니다. 변경된 `200`은 cached body와 validator를 교체합니다. cached `201`, `204`와 다른 성공 non-`200` 응답은 기존 TTL 만료 뒤 무조건 요청 경로를 유지합니다. - -```swift -let client = NXAPIClient( - configuration: NXClientConfiguration( - baseURL: URL(string: "https://api.example.com")!, - cache: .revalidatingMemory(ttl: 300) - ) -) -``` - -cache와 진행 중인 요청 store는 `NXAPIClient` 인스턴스에 속합니다. 요청 간 cache 상태를 공유하려면 service 또는 의존성 주입 계층에 client를 저장해 재사용해야 합니다. `NXAPIClient(configuration:)`를 새로 만들면 독립 store가 생성됩니다. 기존 client 값의 복사본은 원래 store를 공유합니다. - -```swift -struct UserService { - private let client: NXAPIClient - - init(client: NXAPIClient) { - self.client = client - } -} -``` - -Nexa는 `Vary`, disk cache, 전체 `Cache-Control` 해석, stale-if-error를 구현하지 않습니다. `.revalidatingMemory(ttl:)`는 `NXCache` enum case를 추가하므로 `NXCache`의 모든 case를 나열한 `switch`는 재컴파일 시 새 case를 처리해야 합니다. - -## 재시도 정책 - -`.retry(...)`는 설정한 재시도 상태 코드 또는 전송 오류가 발생하면 기본으로 `GET`, `HEAD`, `PUT`, `DELETE`, `OPTIONS`를 재시도합니다. `maxAttempts`를 생략하면 세 번 시도합니다. 같은 요청을 반복해도 안전하게 처리하는 엔드포인트일 때만 `POST`, `PATCH`를 `allowing`에 명시적으로 추가할 수 있습니다. - -재시도 가능한 `429`, `503` 응답에서는 `Retry-After`의 초 단위와 HTTP-date 값을 처리합니다. 유효한 서버 값은 local backoff를 대체하고 기본 60초인 `maximumServerDelay`로 제한되며 `NXRetryLog`에 기록됩니다. `NXRetryJitter.full`은 local backoff에만 적용되고 서버가 지정한 지연을 줄이지 않습니다. - -```swift -let user = try await client - .post("/users") - .retry( - maxAttempts: 3, - backoff: .fixed(0), - allowing: [.post], - maximumServerDelay: 30, - jitter: .none - ) - .send(as: User.self) -``` - -## Nexa 1.3 전환 - -Nexa 1.3에서는 공개 `NXRetryPolicy` 생성자, `NXRetryPolicy.Backoff`, `NXRetryPolicy.Jitter`, `.retry(_:)`를 제거합니다. `NXRetryPolicy`는 internal 구현 타입으로 유지합니다. `NXRetryBackoff`, `NXRetryJitter`와 `.retry(maxAttempts:backoff:retryableStatusCodes:allowing:maximumServerDelay:jitter:)`를 사용하며 `maxAttempts`의 기본값은 `3`입니다. - -## Interceptor method 계약 - -`NXHTTPInterceptor.replacingRequest(_:)`는 request URL, header, body를 바꿀 수 있지만 method는 설정한 method와 같아야 합니다. 다른 method는 이후 interceptor, logger, cache, transport 실행 전에 `NXError.invalidRequest`로 종료됩니다. - -## 개발 - -Nexa는 배포되는 package graph에서 SwiftLint를 분리하여 패키지 소비자가 maintainer용 lint 규칙을 함께 받지 않도록 구성합니다. - -로컬 라이브러리 개발 시에는 `Examples/NexaClient/NexaClient.xcodeproj`를 사용하면 됩니다. - -- Xcode에서 `NexaClient` 타깃을 build하여 local package integration 경로를 확인 -- app target build 시 repo 루트 기준으로 `swiftlint` 실행 -- 로컬에 `swiftlint`가 없으면 script phase가 설치 안내와 함께 실패 - -이 project는 maintainer 전용 integration host이며 Swift Package Manager로 Nexa를 사용하는 앱에는 필요하지 않습니다. - -## 테스트 - -Nexa는 요청 실행을 테스트하기 쉽도록 설계되었습니다. `NXHTTPTransport`를 통해 실제 네트워킹을 커스텀 transport로 교체하고 발신 요청과 디코딩된 응답을 검증할 수 있습니다. - -```swift -import Foundation -import Nexa - -struct User: Codable, Equatable { - let id: Int - let name: String -} - -struct StubTransport: NXHTTPTransport { - func send(_ request: URLRequest) async throws -> NXRawResponse { - let response = HTTPURLResponse( - url: request.url!, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! - - return NXRawResponse( - data: Data(#"{"id":1,"name":"opfic"}"#.utf8), - response: response - ) - } -} - -let client = NXAPIClient( - configuration: NXClientConfiguration( - baseURL: URL(string: "https://example.com")!, - transport: StubTransport() - ) -) - -let user = try await client.get("/users/1").send(as: User.self) -``` diff --git a/README.md b/README.md index 407ce05..4cffc63 100644 --- a/README.md +++ b/README.md @@ -5,52 +5,52 @@ [![Platforms](https://img.shields.io/badge/Platforms-iOS_15%2B_macOS_12%2B-blue?style=flat-square)](https://github.com/opficdev/Nexa) [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen?style=flat-square)](https://swift.org/package-manager/) -[English](README.md) | [한국어](README.ko.md) +[English](README.en.md) | [한국어](README.md) -Nexa is a SwiftUI-inspired declarative networking library built on `URLSession`. +Nexa는 `URLSession` 기반의 SwiftUI 스타일 선언형 네트워킹 라이브러리입니다. -- [Features](#features) -- [Requirements](#requirements) -- [Installation](#installation) -- [Public API](#public-api) -- [Quick Start](#quick-start) +- [기능](#기능) +- [요구 사항](#요구-사항) +- [설치](#설치) +- [공개 API](#공개-api) +- [빠른 시작](#빠른-시작) - [Endpoint API](#endpoint-api) -- [Request Migration](#request-migration) -- [Configuration](#configuration) -- [Transport Metrics](#transport-metrics) -- [Authentication Refresh](#authentication-refresh) -- [Response Cache](#response-cache) -- [Retry Policy](#retry-policy) -- [Development](#development) -- [Testing](#testing) - -## Features - -- [x] Declarative request builders for `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` -- [x] Typed response decoding with Swift Concurrency -- [x] Value-semantic request composition -- [x] Query, header, timeout, body, and JSON encoding support -- [x] Endpoint-based API with compile-time response typing -- [x] Request-level and global interceptor chains -- [x] Built-in authentication and token refresh flow through `NXAuthTokenProvider` -- [x] One shared in-flight token refresh for concurrent `401` responses from the same client -- [x] Retry policies with fixed and exponential backoff -- [x] Response validation and server error decoding -- [x] Memory response cache with optional validator revalidation for successful `GET` responses and in-flight identical requests -- [x] Logger hooks and transport abstraction for testing +- [요청 전환](#요청-전환) +- [설정](#설정) +- [전송 측정값](#전송-측정값) +- [인증 토큰 갱신](#인증-토큰-갱신) +- [응답 캐시](#응답-캐시) +- [재시도 정책](#재시도-정책) +- [개발](#개발) +- [테스트](#테스트) + +## 기능 + +- [x] `GET`, `POST`, `PUT`, `PATCH`, `DELETE`를 위한 선언형 요청 빌더 +- [x] Swift Concurrency를 활용한 타입 안전 응답 디코딩 +- [x] 값 타입 기반 요청 조합 +- [x] 쿼리, 헤더, 타임아웃, 바디, JSON 인코딩 지원 +- [x] 컴파일 타임 응답 타입을 갖는 Endpoint 기반 API +- [x] 요청 단위 및 전역 인터셉터 체인 +- [x] `NXAuthTokenProvider`를 통한 인증 및 토큰 갱신 흐름 내장 +- [x] 같은 client의 동시 `401` 응답을 위한 진행 중 토큰 갱신 공유 +- [x] Fixed backoff 및 exponential backoff 기반 재시도 정책 +- [x] 응답 유효성 검사 및 서버 에러 디코딩 +- [x] 성공한 `GET` 응답과 진행 중인 동일 요청을 위한 validator 재검증을 지원하는 메모리 응답 캐시 +- [x] 로거 훅 및 테스트를 위한 transport 추상화 - [x] `Sendable` URLSession task metrics observer -## Requirements +## 요구 사항 -| Platform | Swift | Installation | +| 플랫폼 | Swift | 설치 | | --- | --- | --- | | iOS 15.0+ / macOS 12.0+ | Swift 6.1 | [Swift Package Manager](#swift-package-manager) | -## Installation +## 설치 ### Swift Package Manager -Add Nexa to your `Package.swift`: +`Package.swift`에 Nexa를 추가하세요: ```swift dependencies: [ @@ -58,7 +58,7 @@ dependencies: [ ] ``` -Then add `Nexa` to your target dependencies: +그런 다음 타겟 의존성에 `Nexa`를 추가하세요: ```swift .target( @@ -69,38 +69,38 @@ Then add `Nexa` to your target dependencies: ) ``` -## Public API +## 공개 API -Most code starts from `NXAPIClient`, then moves into one `NXRequestBuilder` request path. Use `send()` for `NXRawResponse`, `send(as:)` for an explicit decoded type, or contextual `send()` when the assignment supplies the decoded type. +대부분의 코드는 `NXAPIClient`에서 시작하여 하나의 `NXRequestBuilder` 요청 경로로 이어집니다. `NXRawResponse`에는 `send()`, 명시적 디코딩 타입에는 `send(as:)`, 대입문이 타입을 제공할 때는 문맥 기반 `send()`를 사용합니다. -The rest of the public surface is made of extension points for auth, logging, testing, retry, validation, and custom error mapping. +나머지 공개 인터페이스는 인증, 로깅, 테스트, 재시도, 유효성 검사, 커스텀 에러 매핑을 위한 확장 포인트로 구성됩니다. -| API | When to use it | Example | +| API | 사용 시점 | 예시 | | --- | --- | --- | -| `NXAPIClient` | Main entry point for requests that share one `baseURL` and one configuration | `client.get("/users").send(as: User.self)` | -| `NXRequestBuilder` | When you need a prepared `URLRequest`, `NXRawResponse`, or a decoded `Decodable` response | `try await client.get("/users").send()` | -| `NXTypedRequestBuilder` | Endpoint configuration compatibility boundary | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | -| `NXEndpoint` | When an endpoint definition should be reusable and carry its response type with it | `try await client.send(UserEndpoint(identifier: 1))` | -| `NXClientConfiguration` | When shared headers, transport, logger, auth, encoder, decoder, or interceptors should be configured once | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | -| `NXCache` | When successful unauthenticated `GET` responses should be reused for a TTL or revalidated with validators | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | -| `NXRetryBackoff` | When retry delays need fixed or exponential behavior | `.retry(maxAttempts: 3, backoff: .fixed(0))` | -| `NXRetryJitter` | When local retry delay randomization is needed | `.retry(maxAttempts: 3, jitter: .full)` | -| `NXValidationPolicy` | When the accepted status codes differ from the default `200..<300` | `.validate(.statusCodes([200, 201, 204]))` | -| `NXHTTPTransport` | When you need stubs in tests or want to replace the transport implementation | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | -| `NXHTTPInterceptor` | When you need cross-cutting request behavior such as tracing or header injection | `.intercept(yourInterceptor)` | -| `NXAuthTokenProvider` | When `.authorized()` requests need token lookup and refresh support | `authTokenProvider: yourAuthTokenProvider` | -| `NXServerErrorDecoder` | When failed responses should decode into your own domain error | `serverErrorDecoder: yourServerErrorDecoder` | -| `NXLogger` | When you want structured request lifecycle logging | `logger: yourLogger` | -| `NXNetworkMetricsObserver` | When you want URLSession task timing snapshots without changing logger events | `NXURLSessionTransport(metricsObserver: yourObserver)` | -| `NXRawResponse` | When you need both `Data` and `HTTPURLResponse` directly | `let response = try await client.get("/users").send()` | -| `NXError` | When handling Nexa-specific failures in calling code | `catch let error as NXError` | -| `NXHTTPMethod` | When defining an `NXEndpoint` method | `var method: NXHTTPMethod { .post }` | - -### Which one should I start with? - -Assume `client` below is an `NXAPIClient` that has already been configured. - -Use `NXAPIClient` + `NXRequestBuilder` for most application code: +| `NXAPIClient` | 동일한 `baseURL`과 설정을 공유하는 요청의 주 진입점 | `client.get("/users").send(as: User.self)` | +| `NXRequestBuilder` | `URLRequest`, `NXRawResponse`, 또는 디코딩된 `Decodable` 응답이 필요할 때 | `try await client.get("/users").send()` | +| `NXTypedRequestBuilder` | Endpoint 설정 호환성 경계 | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | +| `NXEndpoint` | 엔드포인트 정의를 재사용하고 응답 타입을 함께 관리할 때 | `try await client.send(UserEndpoint(identifier: 1))` | +| `NXClientConfiguration` | 공통 헤더, transport, 로거, 인증, 인코더, 디코더, 인터셉터를 한 번에 설정할 때 | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | +| `NXCache` | 인증이 필요 없는 성공한 `GET` 응답을 TTL 동안 재사용하거나 validator로 재검증할 때 | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | +| `NXRetryBackoff` | 고정 또는 지수 재시도 지연이 필요할 때 | `.retry(maxAttempts: 3, backoff: .fixed(0))` | +| `NXRetryJitter` | local 재시도 지연의 무작위 처리가 필요할 때 | `.retry(maxAttempts: 3, jitter: .full)` | +| `NXValidationPolicy` | 허용할 상태 코드가 기본값(`200..<300`)과 다를 때 | `.validate(.statusCodes([200, 201, 204]))` | +| `NXHTTPTransport` | 테스트용 스텁이 필요하거나 transport 구현을 교체할 때 | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | +| `NXHTTPInterceptor` | 트레이싱이나 헤더 주입처럼 요청 전반에 적용되는 처리가 필요할 때 | `.intercept(yourInterceptor)` | +| `NXAuthTokenProvider` | `.authorized()` 요청에 토큰 조회 및 갱신 기능이 필요할 때 | `authTokenProvider: yourAuthTokenProvider` | +| `NXServerErrorDecoder` | 실패 응답을 도메인 에러로 디코딩할 때 | `serverErrorDecoder: yourServerErrorDecoder` | +| `NXLogger` | 구조화된 요청 생명주기 로깅이 필요할 때 | `logger: yourLogger` | +| `NXNetworkMetricsObserver` | logger event 변경 없이 URLSession task 시간 snapshot이 필요할 때 | `NXURLSessionTransport(metricsObserver: yourObserver)` | +| `NXRawResponse` | `Data`와 `HTTPURLResponse`를 직접 다뤄야 할 때 | `let response = try await client.get("/users").send()` | +| `NXError` | 호출 코드에서 Nexa 고유 오류를 처리할 때 | `catch let error as NXError` | +| `NXHTTPMethod` | `NXEndpoint`의 메서드를 정의할 때 | `var method: NXHTTPMethod { .post }` | + +### 어디서 시작할까요? + +아래에서 `client`는 이미 설정된 `NXAPIClient`라고 가정합니다. + +대부분의 앱 코드에서는 `NXAPIClient`와 `NXRequestBuilder` 조합을 사용할 수 있습니다. ```swift import Foundation @@ -116,7 +116,7 @@ let user = try await client .send(as: User.self) ``` -When the destination type supplies the decoding context, `send()` can remain concise: +대입 대상 타입이 디코딩 문맥을 제공하면 `send()`를 간결하게 유지할 수 있습니다. ```swift let user: User = try await client @@ -124,7 +124,7 @@ let user: User = try await client .send() ``` -Use `NXRequestBuilder` when you want to inspect the request or handle the raw response yourself: +요청을 직접 확인하거나 원시 응답을 직접 처리할 때는 `NXRequestBuilder`를 사용하면 됩니다. ```swift import Foundation @@ -142,7 +142,7 @@ let response = try await client .send() ``` -Use `NXEndpoint` when the same endpoint shape is reused in several places: +동일한 엔드포인트 형태가 여러 곳에서 재사용될 때는 `NXEndpoint`를 사용하면 됩니다. ```swift import Foundation @@ -165,22 +165,22 @@ struct UserEndpoint: NXEndpoint { } ``` -Use the lower-level protocols only when the default behavior is not enough: +기본 동작으로 충분하지 않을 때만 하위 레벨 프로토콜을 사용하면 됩니다. -- `NXHTTPTransport`: stubs, mocks, custom network backends -- `NXHTTPInterceptor`: tracing, request mutation, custom flow control -- `NXAuthTokenProvider`: bearer token injection and refresh -- `NXServerErrorDecoder`: server payload to domain error mapping -- `NXLogger`: request lifecycle logging and observability -- `NXNetworkMetricsObserver`: URLSession task timing snapshots +- `NXHTTPTransport`: 스텁, 목, 커스텀 네트워크 백엔드 +- `NXHTTPInterceptor`: 트레이싱, 요청 변환, 커스텀 흐름 제어 +- `NXAuthTokenProvider`: bearer token 주입 및 갱신 +- `NXServerErrorDecoder`: 서버 payload를 도메인 에러로 매핑 +- `NXLogger`: 요청 생명주기 로깅 및 관측성 +- `NXNetworkMetricsObserver`: URLSession task 시간 snapshot -## Request Flow +## 요청 흐름 ```mermaid flowchart TB - application[Application] --> client[NXAPIClient] + application[소비자 앱] --> client[NXAPIClient] - subgraph publicAPI[Public API] + subgraph publicAPI[공개 API] client builder[NXRequestBuilder
NXTypedRequestBuilder] endpoint[NXEndpoint] @@ -189,19 +189,19 @@ flowchart TB endpoint --> builder end - subgraph core[Core Model] + subgraph core[핵심 모델] configuration[NXClientConfiguration] requestSpec[RequestSpec] - extensionPoints[Extension Points
transport, interceptor, auth, logger, error decoder] + extensionPoints[확장 지점
전송, 인터셉터, 인증, 로깅, 오류 디코더] end client --> configuration builder --> requestSpec - subgraph runtime[Execution Layer] + subgraph runtime[실행 계층] assembler[NXRequestAssembler] executor[NXRequestExecutor] - interceptors[NXInterceptorChain
retry, auth, logging, cache] + interceptors[NXInterceptorChain
재시도, 인증, 로깅, 응답 캐시] transport[NXHTTPTransport] pipeline[NXResponsePipeline] end @@ -210,17 +210,17 @@ flowchart TB configuration --> executor requestSpec --> executor executor --> interceptors --> transport --> pipeline - pipeline --> rawResponse[Raw Response
NXRawResponse] - pipeline --> decodedResponse[Decoded Response
Response] + pipeline --> rawResponse[원시 응답
NXRawResponse] + pipeline --> decodedResponse[디코딩된 응답
Response] extensionPoints -.-> interceptors extensionPoints -.-> transport extensionPoints -.-> pipeline ``` -## Quick Start +## 빠른 시작 -Nexa keeps request code compact while still exposing auth, retry, validation, and decoding in one flow. +Nexa는 인증, 재시도, 유효성 검사, 디코딩을 하나의 흐름으로 간결하게 표현합니다. ```swift import Foundation @@ -244,9 +244,9 @@ let user = try await client .send(as: User.self) ``` -Add `.authorized()` only when the client has an `authTokenProvider`. +`.authorized()`는 클라이언트에 `authTokenProvider`가 설정된 경우에만 추가하세요. -You can also build requests step by step: +단계별로 요청을 구성할 수도 있습니다: ```swift import Foundation @@ -270,7 +270,7 @@ let createdUser = try await client ## Endpoint API -If you prefer a Moya-style endpoint abstraction, define an `NXEndpoint` and let Nexa keep the response type attached to the endpoint itself. Its `NXTypedRequestBuilder` configuration contract remains available for source compatibility. +Moya 스타일의 엔드포인트 추상화를 선호한다면, `NXEndpoint`를 정의하여 응답 타입을 엔드포인트에 직접 연결할 수 있습니다. `NXTypedRequestBuilder` 설정 계약은 소스 호환성을 위해 유지됩니다. ```swift import Foundation @@ -297,22 +297,22 @@ struct UserEndpoint: NXEndpoint { let user = try await client.send(UserEndpoint(identifier: 42)) ``` -## Request Migration +## 요청 전환 -Nexa 1.3 keeps the existing request APIs available while establishing `NXRequestBuilder` as the one-way method-based request path. +Nexa 1.3은 기존 요청 API를 유지하면서 `NXRequestBuilder`를 단방향 method 기반 요청 경로로 구성합니다. -| Removed call | Replacement | +| 제거된 호출 | 대체 호출 | | --- | --- | | `client.get("/users").raw()` | `client.get("/users").send()` | | `client.get("/users", as: User.self).raw()` | `client.get("/users").send()` | | `client.get("/users").as(User.self).send()` | `client.get("/users").send(as: User.self)` | | `client.get("/users", as: User.self).send()` | `client.get("/users").send(as: User.self)` | -Nexa 1.3 removes `NXRequestBuilder.raw()` and `NXTypedRequestBuilder.raw()`. The existing `NXEndpoint.configure(_:)` and `client.send(endpoint)` contracts remain unchanged, but Endpoint requests do not provide a raw-response execution API. Construct a direct `NXRequestBuilder` request when raw response handling is required. This migration does not change empty-body or `204` response behavior. +Nexa 1.3은 `NXRequestBuilder.raw()`와 `NXTypedRequestBuilder.raw()`를 제거합니다. 기존 `NXEndpoint.configure(_:)`와 `client.send(endpoint)` 계약은 유지하지만 Endpoint 요청에는 원시 응답 실행 API가 없습니다. 원시 응답 처리가 필요하면 `NXRequestBuilder`로 요청을 직접 구성해야 합니다. 이 전환은 빈 바디와 `204` 응답 동작을 변경하지 않습니다. -## Configuration +## 설정 -`NXClientConfiguration` centralizes the pieces that usually spread across a custom API layer. +`NXClientConfiguration`은 커스텀 API 레이어에 흩어지기 쉬운 설정을 한 곳에 집중시킵니다. ```swift import Foundation @@ -334,29 +334,29 @@ let configuration = NXClientConfiguration( let client = NXAPIClient(configuration: configuration) ``` -Replace the defaults with your own conforming types only when you need custom behavior: +커스텀 동작이 필요할 때만 기본값을 직접 구현한 타입으로 교체하세요: -- `NXLogger` for structured logging -- `NXHTTPInterceptor` for request tracing or mutation -- `NXServerErrorDecoder` for mapping failed responses to domain errors -- `NXAuthTokenProvider` for bearer token injection and refresh +- `NXLogger`: 구조화된 로깅 +- `NXHTTPInterceptor`: 요청 트레이싱 또는 변환 +- `NXServerErrorDecoder`: 실패 응답을 도메인 에러로 매핑 +- `NXAuthTokenProvider`: bearer token 주입 및 갱신 -Nexa currently supports: +현재 Nexa가 지원하는 기능: -- Global headers and per-request headers -- Raw body and JSON body encoding -- Request-level validation policies -- In-memory reuse of successful unauthenticated `GET` responses within a TTL -- In-flight identical `GET` request reuse while the first request is still running -- Automatic auth header injection for `authorized()` requests -- Token refresh and retry handling -- Custom transports for stubbing and isolated tests +- 전역 헤더 및 요청별 헤더 +- 원시 바디 및 JSON 바디 인코딩 +- 요청 단위 유효성 검사 정책 +- 성공한 비인증 `GET` 응답의 TTL 내 memory 재사용 +- 첫 요청이 실행 중일 때 진행 중인 동일 `GET` 요청 재사용 +- `.authorized()` 요청에 대한 자동 인증 헤더 주입 +- 토큰 갱신 및 재시도 처리 +- 스터빙 및 격리 테스트를 위한 커스텀 transport -## Transport Metrics +## 전송 측정값 -`NXURLSessionTransport` can forward one `NXNetworkMetrics` snapshot to an `NXNetworkMetricsObserver` for each `URLSession` task. The snapshot contains task duration, redirect count, transaction count, and ordered `NXNetworkTransactionMetrics` values. +`NXURLSessionTransport`는 `URLSession` task마다 `NXNetworkMetrics` snapshot 하나를 `NXNetworkMetricsObserver`에 전달할 수 있습니다. snapshot에는 task duration, redirect 수, transaction 수, 수집 순서를 보존한 `NXNetworkTransactionMetrics` 값이 포함됩니다. -Each transaction reports DNS, connection, TLS, and request-to-first-byte durations only when both Foundation timestamps exist. A missing timestamp remains `nil`. A reused connection is represented by `isConnectionReused` and can have `nil` DNS or connection durations. +각 transaction은 Foundation timestamp의 시작과 끝이 모두 있을 때만 DNS, connection, TLS, request 시작부터 first byte까지의 duration을 나타냅니다. timestamp가 없으면 해당 값은 `nil`입니다. 재사용 connection은 `isConnectionReused`로 나타내며 DNS 또는 connection duration이 `nil`일 수 있습니다. ```swift import Foundation @@ -371,21 +371,21 @@ actor MetricsObserver: NXNetworkMetricsObserver { let transport = NXURLSessionTransport(metricsObserver: MetricsObserver()) ``` -Only `NXURLSessionTransport` collects these snapshots. A custom `NXHTTPTransport` and a cache hit do not synthesize metrics. Observer delivery does not delay request completion, and its order relative to `NXLogger` events is not guaranteed. +snapshot은 `NXURLSessionTransport`만 수집합니다. 커스텀 `NXHTTPTransport`와 cache hit는 metrics를 합성하지 않습니다. observer 전달은 요청 완료를 지연하지 않으며 `NXLogger` event와의 순서는 보장하지 않습니다. -## Authentication Refresh +## 인증 토큰 갱신 -Concurrent `401` responses from `.authorized()` requests created by one `NXAPIClient` share one in-progress token refresh. Copies of that client and builders derived from it share the same refresh. A separately initialized `NXAPIClient` has an independent refresh lifetime. +하나의 `NXAPIClient`에서 생성한 `.authorized()` 요청이 동시에 `401` 응답을 받으면 진행 중인 토큰 갱신 결과 하나를 공유합니다. 해당 client의 값 복사본과 여기서 파생한 builder도 같은 갱신을 공유합니다. 별도로 생성한 `NXAPIClient`는 독립된 갱신 수명을 가집니다. -`NXAuthTokenProvider` keeps the same `currentAccessToken()` and `refreshAccessToken()` requirements. Each request retries at most once after a non-`nil` refresh result. A `nil` refresh result returns the request's original `401` response, and a refresh error follows Nexa's existing error mapping. +`NXAuthTokenProvider`의 `currentAccessToken()`, `refreshAccessToken()` 요구 사항은 바뀌지 않습니다. 각 요청은 `nil`이 아닌 갱신 결과 뒤 최대 한 번만 재전송합니다. `nil` 갱신 결과는 해당 요청의 원래 `401` 응답을 반환하고, 갱신 오류는 Nexa의 기존 오류 매핑을 따릅니다. -Cancelling one caller does not cancel the shared refresh or the other waiting requests. `NXAuthRefreshLog` is emitted once per actual refresh; its `requestIdentifier` is the identifier of the request that started that refresh. +한 호출자의 취소는 공유 갱신이나 다른 대기 요청을 취소하지 않습니다. `NXAuthRefreshLog`는 실제 갱신 한 번당 한 번 기록되고, `requestIdentifier`는 해당 갱신을 시작한 요청의 식별자입니다. -## Response Cache +## 응답 캐시 -`NXCache.memory(ttl:)` reuses successful unauthenticated `GET` responses for the given TTL and combines identical requests while the first request is in progress. It does not add conditional headers after the TTL expires. +`NXCache.memory(ttl:)`는 인증이 필요 없는 성공한 `GET` 응답을 지정한 TTL 동안 재사용하고, 첫 요청이 진행 중인 동일 요청을 하나로 합칩니다. TTL 만료 뒤에는 조건부 header를 추가하지 않습니다. -`NXCache.revalidatingMemory(ttl:)` keeps the same cache behavior and additionally revalidates an expired cached `200` response when it has an `ETag` or `Last-Modified` header. Nexa adds `If-None-Match` and `If-Modified-Since` after creating the cache key. A bodyless `304 Not Modified` with the matching validator returns the cached body as a `200` response. A different validator or a body-bearing `304` discards the stale response and follows normal response validation. A changed `200` replaces the cached body and validator. Cached `201`, `204`, and other successful non-`200` responses keep the normal TTL-expiry reload path. +`NXCache.revalidatingMemory(ttl:)`는 같은 cache 동작에 더해 `ETag` 또는 `Last-Modified` header가 있는 만료 `200` 응답을 재검증합니다. Nexa는 cache key 생성 뒤 `If-None-Match`, `If-Modified-Since`를 추가합니다. validator가 일치하고 body가 없는 `304 Not Modified`는 cached body를 `200` 응답으로 반환합니다. validator가 다르거나 body가 있는 `304`는 만료 응답을 제거하고 일반 응답 유효성 검사로 처리합니다. 변경된 `200`은 cached body와 validator를 교체합니다. cached `201`, `204`와 다른 성공 non-`200` 응답은 기존 TTL 만료 뒤 무조건 요청 경로를 유지합니다. ```swift let client = NXAPIClient( @@ -396,7 +396,7 @@ let client = NXAPIClient( ) ``` -The cache and in-flight request store belong to an `NXAPIClient` instance. Keep a client in a service or dependency container when requests should share cache state. Creating a new `NXAPIClient(configuration:)` creates an independent store. A copy of an existing client value shares the original store. +cache와 진행 중인 요청 store는 `NXAPIClient` 인스턴스에 속합니다. 요청 간 cache 상태를 공유하려면 service 또는 의존성 주입 계층에 client를 저장해 재사용해야 합니다. `NXAPIClient(configuration:)`를 새로 만들면 독립 store가 생성됩니다. 기존 client 값의 복사본은 원래 store를 공유합니다. ```swift struct UserService { @@ -408,13 +408,13 @@ struct UserService { } ``` -Nexa does not implement `Vary`, disk cache, full `Cache-Control` interpretation, or stale-if-error behavior. Adding `.revalidatingMemory(ttl:)` adds an `NXCache` enum case, so update exhaustive `switch` statements over `NXCache` when recompiling. +Nexa는 `Vary`, disk cache, 전체 `Cache-Control` 해석, stale-if-error를 구현하지 않습니다. `.revalidatingMemory(ttl:)`는 `NXCache` enum case를 추가하므로 `NXCache`의 모든 case를 나열한 `switch`는 재컴파일 시 새 case를 처리해야 합니다. -## Retry Policy +## 재시도 정책 -`.retry(...)` retries `GET`, `HEAD`, `PUT`, `DELETE`, and `OPTIONS` by default when a configured retryable status code or transport error occurs. It uses three attempts when `maxAttempts` is omitted. `POST` and `PATCH` remain single-attempt requests unless you explicitly add them through `allowing` for an endpoint that safely accepts repeated requests. +`.retry(...)`는 설정한 재시도 상태 코드 또는 전송 오류가 발생하면 기본으로 `GET`, `HEAD`, `PUT`, `DELETE`, `OPTIONS`를 재시도합니다. `maxAttempts`를 생략하면 세 번 시도합니다. 같은 요청을 반복해도 안전하게 처리하는 엔드포인트일 때만 `POST`, `PATCH`를 `allowing`에 명시적으로 추가할 수 있습니다. -For retryable `429` and `503` responses, Nexa accepts `Retry-After` delay seconds and HTTP-date values. A valid server value replaces local backoff, is capped by `maximumServerDelay` (60 seconds by default), and is recorded through `NXRetryLog`. `NXRetryJitter.full` changes only local backoff delays and never shortens a server-provided delay. +재시도 가능한 `429`, `503` 응답에서는 `Retry-After`의 초 단위와 HTTP-date 값을 처리합니다. 유효한 서버 값은 local backoff를 대체하고 기본 60초인 `maximumServerDelay`로 제한되며 `NXRetryLog`에 기록됩니다. `NXRetryJitter.full`은 local backoff에만 적용되고 서버가 지정한 지연을 줄이지 않습니다. ```swift let user = try await client @@ -429,29 +429,29 @@ let user = try await client .send(as: User.self) ``` -## Nexa 1.3 Migration +## Nexa 1.3 전환 -The public `NXRetryPolicy` constructor, `NXRetryPolicy.Backoff`, `NXRetryPolicy.Jitter`, and `.retry(_:)` are removed in Nexa 1.3. Nexa keeps `NXRetryPolicy` as an internal implementation detail. Use `.retry(maxAttempts:backoff:retryableStatusCodes:allowing:maximumServerDelay:jitter:)` with `NXRetryBackoff` and `NXRetryJitter` instead; `maxAttempts` defaults to `3`. +Nexa 1.3에서는 공개 `NXRetryPolicy` 생성자, `NXRetryPolicy.Backoff`, `NXRetryPolicy.Jitter`, `.retry(_:)`를 제거합니다. `NXRetryPolicy`는 internal 구현 타입으로 유지합니다. `NXRetryBackoff`, `NXRetryJitter`와 `.retry(maxAttempts:backoff:retryableStatusCodes:allowing:maximumServerDelay:jitter:)`를 사용하며 `maxAttempts`의 기본값은 `3`입니다. -## Interceptor Method Contract +## Interceptor 메서드 계약 -`NXHTTPInterceptor.replacingRequest(_:)` can change a request URL, headers, and body, but the request method must remain equal to the configured method. A different method ends the chain with `NXError.invalidRequest` before later interceptors, logging, caching, or transport. +`NXHTTPInterceptor.replacingRequest(_:)`는 request URL, header, body를 바꿀 수 있지만 method는 설정한 method와 같아야 합니다. 다른 method는 이후 interceptor, logger, cache, transport 실행 전에 `NXError.invalidRequest`로 종료됩니다. -## Development +## 개발 -Nexa keeps SwiftLint out of the distributable package graph so package consumers do not inherit maintainer lint rules. +Nexa는 배포되는 package graph에서 SwiftLint를 분리하여 패키지 소비자가 maintainer용 lint 규칙을 함께 받지 않도록 구성합니다. -For local library development, use `Examples/NexaClient/NexaClient.xcodeproj`. +로컬 라이브러리 개발 시에는 `Examples/NexaClient/NexaClient.xcodeproj`를 사용하면 됩니다. -- Build the `NexaClient` target in Xcode to validate the local package integration path -- The app target runs `swiftlint` against the repository root during build -- If `swiftlint` is missing locally, the script phase fails with an installation hint +- Xcode에서 `NexaClient` 타깃을 build하여 local package integration 경로를 확인 +- app target build 시 repo 루트 기준으로 `swiftlint` 실행 +- 로컬에 `swiftlint`가 없으면 script phase가 설치 안내와 함께 실패 -This project is a maintainer-only integration host. It is not required for apps that depend on Nexa through Swift Package Manager. +이 project는 라이브러리 개발 전용 통합 프로젝트이며 Swift Package Manager로 Nexa를 사용하는 앱에는 필요하지 않습니다. -## Testing +## 테스트 -Nexa was designed to keep request execution testable. `NXHTTPTransport` lets you replace live networking with a custom transport and validate the outgoing request and decoded response. +Nexa는 요청 실행을 테스트하기 쉽도록 설계되었습니다. `NXHTTPTransport`를 통해 실제 네트워킹을 커스텀 transport로 교체하고 발신 요청과 디코딩된 응답을 검증할 수 있습니다. ```swift import Foundation From 44655eb45a9e463652e0010d87a4061cf16f6840 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:51:29 +0900 Subject: [PATCH 03/12] =?UTF-8?q?docs:=20=EC=84=A4=EA=B3=84=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=95=88=EB=82=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.en.md b/README.en.md index e0f7f73..07da742 100644 --- a/README.en.md +++ b/README.en.md @@ -9,6 +9,8 @@ Nexa is a SwiftUI-inspired declarative networking library built on `URLSession`. +Nexa preserves HTTP semantics by defining explicit boundaries for sharing cached responses and in-flight work, deciding when failed requests may be retried, and observing `URLSession` transport. The alternatives, reasoning, and verification for each decision are documented in the [design stories (Korean)](https://github.com/opficdev/Nexa/wiki/설계-스토리). + - [Features](#features) - [Requirements](#requirements) - [Installation](#installation) diff --git a/README.md b/README.md index 4cffc63..96d2d84 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Nexa는 `URLSession` 기반의 SwiftUI 스타일 선언형 네트워킹 라이브러리입니다. +Nexa는 HTTP 의미를 보존하기 위해 캐시 응답과 진행 중 작업의 공유 범위, 실패한 요청의 재시도 조건, `URLSession` 전송 관측의 경계를 명시합니다. 각 선택의 대안과 근거, 검증 결과는 [설계 스토리](https://github.com/opficdev/Nexa/wiki/설계-스토리)에 정리했습니다. + - [기능](#기능) - [요구 사항](#요구-사항) - [설치](#설치) From fbe2fa1d4ee9f62d0ac7d618699df9e02574d0ee Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:55:33 +0900 Subject: [PATCH 04/12] =?UTF-8?q?docs:=20=EC=BD=94=EB=93=9C=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=95=88=EB=82=B4=20=EB=AC=B8=EC=9E=A5=20=EB=B6=80?= =?UTF-8?q?=ED=98=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 14 +++++++------- README.md | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.en.md b/README.en.md index 07da742..8ffd636 100644 --- a/README.en.md +++ b/README.en.md @@ -52,7 +52,7 @@ Nexa preserves HTTP semantics by defining explicit boundaries for sharing cached ### Swift Package Manager -Add Nexa to your `Package.swift`: +Add Nexa to your `Package.swift` ```swift dependencies: [ @@ -60,7 +60,7 @@ dependencies: [ ] ``` -Then add `Nexa` to your target dependencies: +Then add `Nexa` to your target dependencies ```swift .target( @@ -102,7 +102,7 @@ The rest of the public surface is made of extension points for auth, logging, te Assume `client` below is an `NXAPIClient` that has already been configured. -Use `NXAPIClient` + `NXRequestBuilder` for most application code: +Use `NXAPIClient` + `NXRequestBuilder` for most application code ```swift import Foundation @@ -118,7 +118,7 @@ let user = try await client .send(as: User.self) ``` -When the destination type supplies the decoding context, `send()` can remain concise: +When the destination type supplies the decoding context, `send()` can remain concise ```swift let user: User = try await client @@ -126,7 +126,7 @@ let user: User = try await client .send() ``` -Use `NXRequestBuilder` when you want to inspect the request or handle the raw response yourself: +Use `NXRequestBuilder` when you want to inspect the request or handle the raw response yourself ```swift import Foundation @@ -144,7 +144,7 @@ let response = try await client .send() ``` -Use `NXEndpoint` when the same endpoint shape is reused in several places: +Use `NXEndpoint` when the same endpoint shape is reused in several places ```swift import Foundation @@ -248,7 +248,7 @@ let user = try await client Add `.authorized()` only when the client has an `authTokenProvider`. -You can also build requests step by step: +You can also build requests step by step ```swift import Foundation diff --git a/README.md b/README.md index 96d2d84..18dd1e7 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Nexa는 HTTP 의미를 보존하기 위해 캐시 응답과 진행 중 작업의 ### Swift Package Manager -`Package.swift`에 Nexa를 추가하세요: +`Package.swift`에 Nexa를 추가하세요 ```swift dependencies: [ @@ -60,7 +60,7 @@ dependencies: [ ] ``` -그런 다음 타겟 의존성에 `Nexa`를 추가하세요: +그런 다음 타겟 의존성에 `Nexa`를 추가하세요 ```swift .target( @@ -248,7 +248,7 @@ let user = try await client `.authorized()`는 클라이언트에 `authTokenProvider`가 설정된 경우에만 추가하세요. -단계별로 요청을 구성할 수도 있습니다: +단계별로 요청을 구성할 수도 있습니다 ```swift import Foundation From cf509f792b0cc9bf4e79bb3db3f023bdd9b3922d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:45:54 +0900 Subject: [PATCH 05/12] =?UTF-8?q?docs:=20=EC=9A=94=EC=B2=AD=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EC=99=80=20=EC=A0=88=EB=8C=80=20URL=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 9 +++++++++ README.md | 9 +++++++++ Sources/Nexa.docc/Nexa.md | 8 +++++++- Sources/Public/NXAPIClient.swift | 26 +++++++++++++++----------- Sources/Public/NXEndpoint.swift | 2 +- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/README.en.md b/README.en.md index 8ffd636..ba83409 100644 --- a/README.en.md +++ b/README.en.md @@ -270,6 +270,15 @@ let createdUser = try await client .send(as: User.self) ``` +### Request Paths + +A relative path is appended to the existing path in `baseURL`, and builder query items are appended to its existing query. Calling `client.get()` without a path requests the path already contained in `baseURL`. + +A string containing an absolute URL with a scheme replaces `baseURL`, and builder query items are appended after the URL's existing query. + +> [!WARNING] +> Client-wide and request headers and global and request interceptors still apply to an absolute URL. An `.authorized()` request also applies its Bearer token. Use absolute URLs only for trusted hosts, and do not send a cross-host absolute URL through a client configured with authentication or sensitive headers. + ## Endpoint API If you prefer a Moya-style endpoint abstraction, define an `NXEndpoint` and let Nexa keep the response type attached to the endpoint itself. Its `NXTypedRequestBuilder` configuration contract remains available for source compatibility. diff --git a/README.md b/README.md index 18dd1e7..e9582a1 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,15 @@ let createdUser = try await client .send(as: User.self) ``` +### 요청 경로 + +상대 경로는 `baseURL`의 기존 path 뒤에 결합되고 builder query는 기존 query 뒤에 추가됩니다. `client.get()`처럼 경로를 생략하면 `baseURL`에 포함된 path를 그대로 요청합니다. + +scheme을 포함한 절대 URL 문자열은 `baseURL` 대신 사용되며 해당 URL의 기존 query 뒤에 builder query가 추가됩니다. + +> [!WARNING] +> 절대 URL에도 client 공통 header와 요청 header, 전역 및 요청 interceptor가 그대로 적용됩니다. `.authorized()` 요청은 Bearer token도 적용하므로 신뢰하는 host에만 절대 URL을 사용해야 합니다. 인증이나 민감 header가 설정된 client에서 다른 host의 절대 URL을 요청하지 마세요. + ## Endpoint API Moya 스타일의 엔드포인트 추상화를 선호한다면, `NXEndpoint`를 정의하여 응답 타입을 엔드포인트에 직접 연결할 수 있습니다. `NXTypedRequestBuilder` 설정 계약은 소스 호환성을 위해 유지됩니다. diff --git a/Sources/Nexa.docc/Nexa.md b/Sources/Nexa.docc/Nexa.md index 77070e5..15e9de7 100644 --- a/Sources/Nexa.docc/Nexa.md +++ b/Sources/Nexa.docc/Nexa.md @@ -6,7 +6,7 @@ Declarative networking built around value-semantic request builders, typed decod Start with ``NXClientConfiguration`` to define your shared transport, auth, logging, and serialization behavior. -Use ``NXAPIClient`` to create an ``NXRequestBuilder`` from a relative path. Call ``NXRequestBuilder/send()`` for an ``NXRawResponse``, or ``NXRequestBuilder/send(as:)`` to decode a response in the same request path. +Use ``NXAPIClient`` to create an ``NXRequestBuilder`` from a relative path or an absolute URL string. Call ``NXRequestBuilder/send()`` for an ``NXRawResponse``, or ``NXRequestBuilder/send(as:)`` to decode a response in the same request path. Adopt ``NXEndpoint`` when a request shape should be reusable and carry its response type with it. Its existing ``NXTypedRequestBuilder`` configuration contract remains available for endpoint compatibility. @@ -36,6 +36,12 @@ let user = try await client .send(as: User.self) ``` +## Request Paths + +A relative path is appended to the path in ``NXClientConfiguration/baseURL``, while `get()` without a path uses the base URL path unchanged. Builder query items follow any query already present in the selected URL. + +A string containing an absolute URL with a scheme replaces the base URL. Client-wide and request headers, global and request interceptors, and `.authorized()` Bearer tokens still apply. Use absolute URLs only for trusted hosts, and do not send cross-host absolute URLs through a client configured with authentication or sensitive headers. + ## Raw Response ```swift diff --git a/Sources/Public/NXAPIClient.swift b/Sources/Public/NXAPIClient.swift index 14f078b..7cf6eb7 100644 --- a/Sources/Public/NXAPIClient.swift +++ b/Sources/Public/NXAPIClient.swift @@ -11,7 +11,10 @@ import Foundation /// /// ## 개요 /// -/// client 단일 생성 후 상대 경로 기반 요청 시작 +/// client 단일 생성 후 상대 경로 또는 scheme 포함 절대 URL 문자열 기반 요청 시작 +/// +/// 절대 URL 문자열은 기본 URL을 대체하지만 client 공통 header, interceptor, 인증 정책은 유지 +/// 인증이나 민감 header가 설정된 client에서는 신뢰하는 host에만 절대 URL 사용 /// /// cache 사용 시 각 `NXAPIClient` 초기화마다 독립 메모리 cache와 진행 중 request store 생성. cache 응답 공유는 서비스 또는 DI 계층에서 client 단일 재사용으로 보장. 동일 client 값 복사본은 동일 store 유지 /// @@ -72,7 +75,8 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 미지정 `GET` request builder 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 + /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 + /// 빈 값은 기본 URL path 유지 /// - Returns: 전송 전 추가 설정 가능한 request builder public func get(_ path: String = "") -> NXRequestBuilder { request(method: .get, path: path) @@ -81,7 +85,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 지정 `GET` request builder 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 + /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열. 빈 값은 기본 URL path 유지 /// - type: 성공 응답 decoding 대상 type /// - Returns: `Response` decoding type 지정 request builder @available(*, deprecated, message: "Use get(_:) followed by send(as:).") @@ -94,7 +98,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 미지정 `POST` request builder 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 + /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - Returns: 전송 전 추가 설정 가능한 request builder public func post(_ path: String) -> NXRequestBuilder { request(method: .post, path: path) @@ -103,7 +107,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 지정 `POST` request builder 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 + /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - type: 성공 응답 decoding 대상 type /// - Returns: `Response` decoding type 지정 request builder @available(*, deprecated, message: "Use post(_:) followed by send(as:).") @@ -113,7 +117,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 미지정 `PUT` request builder 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 + /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - Returns: 전송 전 추가 설정 가능한 request builder public func put(_ path: String) -> NXRequestBuilder { request(method: .put, path: path) @@ -122,7 +126,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 지정 `PUT` request builder 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 + /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - type: 성공 응답 decoding 대상 type /// - Returns: `Response` decoding type 지정 request builder @available(*, deprecated, message: "Use put(_:) followed by send(as:).") @@ -132,7 +136,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 미지정 `PATCH` request builder 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 + /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - Returns: 전송 전 추가 설정 가능한 request builder public func patch(_ path: String) -> NXRequestBuilder { request(method: .patch, path: path) @@ -141,7 +145,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 지정 `PATCH` request builder 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 + /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - type: 성공 응답 decoding 대상 type /// - Returns: `Response` decoding type 지정 request builder @available(*, deprecated, message: "Use patch(_:) followed by send(as:).") @@ -151,7 +155,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 미지정 `DELETE` request builder 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 + /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - Returns: 전송 전 추가 설정 가능한 request builder public func delete(_ path: String) -> NXRequestBuilder { request(method: .delete, path: path) @@ -160,7 +164,7 @@ public struct NXAPIClient: Sendable { /// 지정 경로 기준 type 지정 `DELETE` request builder 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 + /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 /// - type: 성공 응답 decoding 대상 type /// - Returns: `Response` decoding type 지정 request builder @available(*, deprecated, message: "Use delete(_:) followed by send(as:).") diff --git a/Sources/Public/NXEndpoint.swift b/Sources/Public/NXEndpoint.swift index c2d1527..d8d4f07 100644 --- a/Sources/Public/NXEndpoint.swift +++ b/Sources/Public/NXEndpoint.swift @@ -40,7 +40,7 @@ public protocol NXEndpoint { associatedtype Response: Decodable /// 요청에 사용되는 HTTP 메서드 var method: NXHTTPMethod { get } - /// client 기본 URL 기준 상대 경로 + /// client 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 var path: String { get } /// 전송 전 endpoint별 customization을 builder에 적용 /// From d2c0b7017ae0d1f88b0554792cd4fe370f3379d2 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:46:51 +0900 Subject: [PATCH 06/12] =?UTF-8?q?docs:=20=EA=B5=AC=EC=A1=B0=ED=99=94=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=EA=B3=BC=20=EC=98=A4=EB=A5=98=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 39 ++++++++++++++++++++++++++++++++++++++- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index ba83409..e65751b 100644 --- a/README.en.md +++ b/README.en.md @@ -19,6 +19,8 @@ Nexa preserves HTTP semantics by defining explicit boundaries for sharing cached - [Endpoint API](#endpoint-api) - [Request Migration](#request-migration) - [Configuration](#configuration) +- [Structured Logging](#structured-logging) +- [Error Handling](#error-handling) - [Transport Metrics](#transport-metrics) - [Authentication Refresh](#authentication-refresh) - [Response Cache](#response-cache) @@ -363,6 +365,41 @@ Nexa currently supports: - Token refresh and retry handling - Custom transports for stubbing and isolated tests +## Structured Logging + +`NXLogger` connects one logical request through `requestIdentifier` and reports the retry-policy attempt through `attemptNumber`. Attempt numbers start at 1 and do not increase when a request is replayed after a Bearer token refresh. + +| Event | When emitted | Main values | +| --- | --- | --- | +| `requestStart` | Before execution below the logger | Request identifier, attempt number, method, URL, headers | +| `requestEnd` | When execution below the logger returns a response | Request identifier, attempt number, status code, elapsed time, payload size | +| `requestFailure` | When a lower interceptor or transport throws | Request identifier, attempt number, elapsed time, error description | +| `retry` | When the next retry attempt is scheduled | Request identifier, next attempt number, delay | +| `authRefresh` | When an actual Bearer token refresh completes | Identifier of the request that started the refresh, success state | + +Only `Authorization` and `Cookie` values in `requestStart` headers are replaced with ``, regardless of header-name casing. URL queries, error descriptions, and other custom sensitive headers are not redacted automatically and require protection before they reach the logger. + +Request execution awaits `NXLogger.log(_:)`. A slow logger can therefore delay request execution, retry scheduling, or authentication refresh. Response validation and decoding occur after the logger, so `requestFailure` does not represent every final `NXError`. + +## Error Handling + +Public Nexa requests classify errors from request assembly, authentication, transport, response validation, and decoding as `NXError`. + +| Case | When it occurs | +| --- | --- | +| `invalidRequest` | URL assembly fails or an interceptor changes the HTTP method | +| `authenticationRequired` | An authenticated request cannot obtain a current Bearer token | +| `authProviderUnavailable` | `.authorized()` is used without an `NXAuthTokenProvider` | +| `timeout` | `URLError.timedOut` occurs | +| `cancelled` | `URLError.cancelled` or Swift Task cancellation occurs | +| `transport` | A `URLError` other than timeout or cancellation occurs | +| `invalidStatus` | The response status is rejected by validation and is not converted into a custom server error | +| `server` | `NXServerErrorDecoder` converts a response rejected by the validation policy into a custom error | +| `decoding` | A successful response cannot be decoded into the requested `Decodable` type | +| `unknown` | An error does not match any category above | + +When the response validation policy rejects a status code, `NXServerErrorDecoder` runs first. The result maps to `server` when the decoder returns an error and to `invalidStatus` otherwise. + ## Transport Metrics `NXURLSessionTransport` can forward one `NXNetworkMetrics` snapshot to an `NXNetworkMetricsObserver` for each `URLSession` task. The snapshot contains task duration, redirect count, transaction count, and ordered `NXNetworkTransactionMetrics` values. @@ -388,7 +425,7 @@ Only `NXURLSessionTransport` collects these snapshots. A custom `NXHTTPTransport Concurrent `401` responses from `.authorized()` requests created by one `NXAPIClient` share one in-progress token refresh. Copies of that client and builders derived from it share the same refresh. A separately initialized `NXAPIClient` has an independent refresh lifetime. -`NXAuthTokenProvider` keeps the same `currentAccessToken()` and `refreshAccessToken()` requirements. Each request retries at most once after a non-`nil` refresh result. A `nil` refresh result returns the request's original `401` response, and a refresh error follows Nexa's existing error mapping. +`NXAuthTokenProvider` keeps the same `currentAccessToken()` and `refreshAccessToken()` requirements. Each request retries at most once after a non-`nil` refresh result. For a `nil` refresh result, the authentication interceptor preserves the original `401` response, but a public `send()` using default response validation can map it to `NXError.invalidStatus`. A refresh error follows Nexa's existing error mapping. Cancelling one caller does not cancel the shared refresh or the other waiting requests. `NXAuthRefreshLog` is emitted once per actual refresh; its `requestIdentifier` is the identifier of the request that started that refresh. diff --git a/README.md b/README.md index e9582a1..823eb37 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Nexa는 HTTP 의미를 보존하기 위해 캐시 응답과 진행 중 작업의 - [Endpoint API](#endpoint-api) - [요청 전환](#요청-전환) - [설정](#설정) +- [구조화된 로깅](#구조화된-로깅) +- [오류 처리](#오류-처리) - [전송 측정값](#전송-측정값) - [인증 토큰 갱신](#인증-토큰-갱신) - [응답 캐시](#응답-캐시) @@ -363,6 +365,41 @@ let client = NXAPIClient(configuration: configuration) - 토큰 갱신 및 재시도 처리 - 스터빙 및 격리 테스트를 위한 커스텀 transport +## 구조화된 로깅 + +`NXLogger`는 하나의 논리 요청을 `requestIdentifier`로 연결하고 재시도 정책의 시도 번호를 `attemptNumber`로 전달합니다. `attemptNumber`는 1부터 시작하며 Bearer token 갱신 뒤의 요청 재전송에서는 증가하지 않습니다. + +| 이벤트 | 발생 시점 | 주요 값 | +| --- | --- | --- | +| `requestStart` | 로거 이후의 요청 실행 전 | 요청 식별자, 시도 번호, method, URL, header | +| `requestEnd` | 로거 이후 실행에서 응답 반환 | 요청 식별자, 시도 번호, 상태 코드, 경과 시간, 응답 데이터 크기 | +| `requestFailure` | 로거 이후의 interceptor 또는 transport에서 오류 발생 | 요청 식별자, 시도 번호, 경과 시간, 오류 설명 | +| `retry` | 다음 retry 시도 예약 | 요청 식별자, 다음 시도 번호, 대기 시간 | +| `authRefresh` | 실제 Bearer token 갱신 완료 | 갱신을 시작한 요청 식별자, 성공 여부 | + +`requestStart`의 header에서는 `Authorization`과 `Cookie` 값만 대소문자와 관계없이 ``로 바뀝니다. URL query, 오류 설명, 다른 사용자 정의 민감 header는 자동으로 가려지지 않으므로 logger로 전달하기 전에 별도 보호가 필요합니다. + +`NXLogger.log(_:)` 호출은 요청 실행 경로에서 기다립니다. 로거의 처리 시간이 길면 요청 실행과 retry 또는 인증 갱신 흐름도 지연될 수 있습니다. 응답 검증과 decoding은 logger 이후에 실행되므로 `requestFailure`가 모든 최종 `NXError`를 나타내지는 않습니다. + +## 오류 처리 + +Nexa의 공개 요청은 조립, 인증, 전송, 응답 검증, decoding 단계의 오류를 `NXError`로 구분합니다. + +| 오류 | 발생 조건 | +| --- | --- | +| `invalidRequest` | 유효한 URL 조립 실패 또는 interceptor의 HTTP method 변경 | +| `authenticationRequired` | 인증 요청에서 현재 Bearer token을 제공하지 못함 | +| `authProviderUnavailable` | `.authorized()` 요청에 `NXAuthTokenProvider`가 설정되지 않음 | +| `timeout` | `URLError.timedOut` 발생 | +| `cancelled` | `URLError.cancelled` 또는 Swift Task 취소 발생 | +| `transport` | timeout과 cancellation 외 `URLError` 발생 | +| `invalidStatus` | 응답 상태가 validation policy에 포함되지 않고 사용자 정의 서버 오류로 변환되지 않음 | +| `server` | 응답 검증 정책이 거부한 응답을 `NXServerErrorDecoder`가 사용자 정의 오류로 변환함 | +| `decoding` | 성공 응답을 요청한 `Decodable` type으로 변환하지 못함 | +| `unknown` | 위 범주에 포함되지 않은 오류 발생 | + +응답 검증 정책이 상태 코드를 거부하면 `NXServerErrorDecoder`가 먼저 실행됩니다. decoder가 오류를 반환하면 `server`, 반환하지 않으면 `invalidStatus`로 매핑됩니다. + ## 전송 측정값 `NXURLSessionTransport`는 `URLSession` task마다 `NXNetworkMetrics` snapshot 하나를 `NXNetworkMetricsObserver`에 전달할 수 있습니다. snapshot에는 task duration, redirect 수, transaction 수, 수집 순서를 보존한 `NXNetworkTransactionMetrics` 값이 포함됩니다. @@ -388,7 +425,7 @@ snapshot은 `NXURLSessionTransport`만 수집합니다. 커스텀 `NXHTTPTranspo 하나의 `NXAPIClient`에서 생성한 `.authorized()` 요청이 동시에 `401` 응답을 받으면 진행 중인 토큰 갱신 결과 하나를 공유합니다. 해당 client의 값 복사본과 여기서 파생한 builder도 같은 갱신을 공유합니다. 별도로 생성한 `NXAPIClient`는 독립된 갱신 수명을 가집니다. -`NXAuthTokenProvider`의 `currentAccessToken()`, `refreshAccessToken()` 요구 사항은 바뀌지 않습니다. 각 요청은 `nil`이 아닌 갱신 결과 뒤 최대 한 번만 재전송합니다. `nil` 갱신 결과는 해당 요청의 원래 `401` 응답을 반환하고, 갱신 오류는 Nexa의 기존 오류 매핑을 따릅니다. +`NXAuthTokenProvider`의 `currentAccessToken()`, `refreshAccessToken()` 요구 사항은 바뀌지 않습니다. 각 요청은 `nil`이 아닌 갱신 결과 뒤 최대 한 번만 재전송합니다. `nil` 갱신 결과에서는 인증 interceptor가 원래 `401` 응답을 유지하지만, 기본 응답 검증을 사용하는 공개 `send()`에서는 `NXError.invalidStatus`로 매핑될 수 있습니다. 갱신 오류는 Nexa의 기존 오류 매핑을 따릅니다. 한 호출자의 취소는 공유 갱신이나 다른 대기 요청을 취소하지 않습니다. `NXAuthRefreshLog`는 실제 갱신 한 번당 한 번 기록되고, `requestIdentifier`는 해당 갱신을 시작한 요청의 식별자입니다. From 04ebb7933311ce6db5a5aaf4ce592f504a609fdd Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:47:08 +0900 Subject: [PATCH 07/12] =?UTF-8?q?docs:=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=8B=A4=ED=96=89=20=EC=A0=95=EB=B3=B4=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 2 ++ README.md | 2 ++ Sources/Public/NXHTTPInterceptor.swift | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index e65751b..f6b3564 100644 --- a/README.en.md +++ b/README.en.md @@ -485,6 +485,8 @@ The public `NXRetryPolicy` constructor, `NXRetryPolicy.Backoff`, `NXRetryPolicy. `NXHTTPInterceptor.replacingRequest(_:)` can change a request URL, headers, and body, but the request method must remain equal to the configured method. A different method ends the chain with `NXError.invalidRequest` before later interceptors, logging, caching, or transport. +`NXRequestExecutionContext.requestIdentifier` remains stable for one logical request and its retry attempts. `attemptNumber` starts at 1 and represents the retry-policy attempt; it does not increase when a request is replayed after a Bearer token refresh. + ## Development Nexa keeps SwiftLint out of the distributable package graph so package consumers do not inherit maintainer lint rules. diff --git a/README.md b/README.md index 823eb37..2e4c414 100644 --- a/README.md +++ b/README.md @@ -485,6 +485,8 @@ Nexa 1.3에서는 공개 `NXRetryPolicy` 생성자, `NXRetryPolicy.Backoff`, `NX `NXHTTPInterceptor.replacingRequest(_:)`는 request URL, header, body를 바꿀 수 있지만 method는 설정한 method와 같아야 합니다. 다른 method는 이후 interceptor, logger, cache, transport 실행 전에 `NXError.invalidRequest`로 종료됩니다. +`NXRequestExecutionContext.requestIdentifier`는 하나의 논리 요청과 해당 retry 시도에서 유지됩니다. `attemptNumber`는 1부터 시작하는 retry policy의 시도 번호이며 Bearer token 갱신 뒤의 재전송에서는 증가하지 않습니다. + ## 개발 Nexa는 배포되는 package graph에서 SwiftLint를 분리하여 패키지 소비자가 maintainer용 lint 규칙을 함께 받지 않도록 구성합니다. diff --git a/Sources/Public/NXHTTPInterceptor.swift b/Sources/Public/NXHTTPInterceptor.swift index 1852c7a..6327945 100644 --- a/Sources/Public/NXHTTPInterceptor.swift +++ b/Sources/Public/NXHTTPInterceptor.swift @@ -43,7 +43,8 @@ public protocol NXHTTPInterceptor: Sendable { /// interceptor 노출용 현재 요청 실행 상태 snapshot /// -/// `NXRequestExecutionContext`의 준비된 요청, 요청 식별자, retry 시도 번호, 사용자 정의 metadata를 interceptor에서 조회 가능 +/// `NXRequestExecutionContext`의 준비된 요청, 요청 식별자, retry 시도 번호를 interceptor에서 조회 가능 +/// `userInfo`는 현재 공개 request builder에서 설정되지 않아 빈 dictionary로 전달 public struct NXRequestExecutionContext: Sendable { /// 현재 실행 중인 요청 public let request: URLRequest @@ -51,7 +52,7 @@ public struct NXRequestExecutionContext: Sendable { public let requestIdentifier: UUID /// 현재 시도 번호(`1`부터 시작) public let attemptNumber: Int - /// 요청 바인딩용 사용자 정의 문자열 metadata 값 + /// 현재 공개 request builder에서 설정되지 않아 빈 dictionary로 전달되는 metadata 값 public let userInfo: [String: String] let specification: RequestSpec From c0abcd71debf3dac44c727a44ea2ca93095f7ac8 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:04:53 +0900 Subject: [PATCH 08/12] =?UTF-8?q?docs:=20=ED=95=9C=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=20=EB=AC=B8=EC=84=9C=20=EB=AC=B8=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 128 ++++++++++---------- Sources/Core/NXCache.swift | 10 +- Sources/Core/NXClientConfiguration.swift | 42 +++---- Sources/Core/NXError.swift | 16 +-- Sources/Core/NXHTTPMethod.swift | 2 +- Sources/Core/NXLogging.swift | 38 +++--- Sources/Core/NXNetworkMetrics.swift | 28 ++--- Sources/Core/NXNetworkMetricsObserver.swift | 6 +- Sources/Core/NXProtocols.swift | 18 +-- Sources/Core/NXRawResponse.swift | 8 +- Sources/Core/NXRequestBody.swift | 2 +- Sources/Core/NXRetryBackoff.swift | 6 +- Sources/Core/NXRetryJitter.swift | 6 +- Sources/Core/NXValidationPolicy.swift | 4 +- Sources/Public/NXAPIClient.swift | 100 +++++++-------- Sources/Public/NXEndpoint.swift | 20 +-- Sources/Public/NXHTTPInterceptor.swift | 22 ++-- Sources/Public/NXRequestBuilder.swift | 92 +++++++------- Sources/Public/NXTypedRequestBuilder.swift | 80 ++++++------ Sources/Runtime/NXURLSessionTransport.swift | 12 +- 20 files changed, 320 insertions(+), 320 deletions(-) diff --git a/README.md b/README.md index 2e4c414..616c874 100644 --- a/README.md +++ b/README.md @@ -34,15 +34,15 @@ Nexa는 HTTP 의미를 보존하기 위해 캐시 응답과 진행 중 작업의 - [x] Swift Concurrency를 활용한 타입 안전 응답 디코딩 - [x] 값 타입 기반 요청 조합 - [x] 쿼리, 헤더, 타임아웃, 바디, JSON 인코딩 지원 -- [x] 컴파일 타임 응답 타입을 갖는 Endpoint 기반 API +- [x] 컴파일 타임 응답 타입을 갖는 엔드포인트 기반 API - [x] 요청 단위 및 전역 인터셉터 체인 - [x] `NXAuthTokenProvider`를 통한 인증 및 토큰 갱신 흐름 내장 -- [x] 같은 client의 동시 `401` 응답을 위한 진행 중 토큰 갱신 공유 -- [x] Fixed backoff 및 exponential backoff 기반 재시도 정책 -- [x] 응답 유효성 검사 및 서버 에러 디코딩 -- [x] 성공한 `GET` 응답과 진행 중인 동일 요청을 위한 validator 재검증을 지원하는 메모리 응답 캐시 -- [x] 로거 훅 및 테스트를 위한 transport 추상화 -- [x] `Sendable` URLSession task metrics observer +- [x] 같은 클라이언트의 동시 `401` 응답을 위한 진행 중 토큰 갱신 공유 +- [x] 고정 backoff 및 지수 backoff 기반 재시도 정책 +- [x] 응답 유효성 검사 및 서버 오류 디코딩 +- [x] 성공한 `GET` 응답과 진행 중인 동일 요청을 위한 식별값을 사용한 재검증을 지원하는 메모리 응답 캐시 +- [x] 로거 훅 및 테스트를 위한 전송 추상화 +- [x] `Sendable` URLSession 작업 측정값 관측자 ## 요구 사항 @@ -77,25 +77,25 @@ dependencies: [ 대부분의 코드는 `NXAPIClient`에서 시작하여 하나의 `NXRequestBuilder` 요청 경로로 이어집니다. `NXRawResponse`에는 `send()`, 명시적 디코딩 타입에는 `send(as:)`, 대입문이 타입을 제공할 때는 문맥 기반 `send()`를 사용합니다. -나머지 공개 인터페이스는 인증, 로깅, 테스트, 재시도, 유효성 검사, 커스텀 에러 매핑을 위한 확장 포인트로 구성됩니다. +나머지 공개 인터페이스는 인증, 로깅, 테스트, 재시도, 유효성 검사, 사용자 정의 오류 매핑을 위한 확장 포인트로 구성됩니다. | API | 사용 시점 | 예시 | | --- | --- | --- | | `NXAPIClient` | 동일한 `baseURL`과 설정을 공유하는 요청의 주 진입점 | `client.get("/users").send(as: User.self)` | | `NXRequestBuilder` | `URLRequest`, `NXRawResponse`, 또는 디코딩된 `Decodable` 응답이 필요할 때 | `try await client.get("/users").send()` | -| `NXTypedRequestBuilder` | Endpoint 설정 호환성 경계 | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | +| `NXTypedRequestBuilder` | 엔드포인트 설정 호환성 경계 | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | | `NXEndpoint` | 엔드포인트 정의를 재사용하고 응답 타입을 함께 관리할 때 | `try await client.send(UserEndpoint(identifier: 1))` | -| `NXClientConfiguration` | 공통 헤더, transport, 로거, 인증, 인코더, 디코더, 인터셉터를 한 번에 설정할 때 | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | -| `NXCache` | 인증이 필요 없는 성공한 `GET` 응답을 TTL 동안 재사용하거나 validator로 재검증할 때 | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | +| `NXClientConfiguration` | 공통 헤더, 전송, 로거, 인증, 인코더, 디코더, 인터셉터를 한 번에 설정할 때 | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | +| `NXCache` | 인증이 필요 없는 성공한 `GET` 응답을 TTL 동안 재사용하거나 식별값을 사용해 재검증할 때 | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | | `NXRetryBackoff` | 고정 또는 지수 재시도 지연이 필요할 때 | `.retry(maxAttempts: 3, backoff: .fixed(0))` | -| `NXRetryJitter` | local 재시도 지연의 무작위 처리가 필요할 때 | `.retry(maxAttempts: 3, jitter: .full)` | +| `NXRetryJitter` | 클라이언트에서 계산한 재시도 지연의 무작위 처리가 필요할 때 | `.retry(maxAttempts: 3, jitter: .full)` | | `NXValidationPolicy` | 허용할 상태 코드가 기본값(`200..<300`)과 다를 때 | `.validate(.statusCodes([200, 201, 204]))` | -| `NXHTTPTransport` | 테스트용 스텁이 필요하거나 transport 구현을 교체할 때 | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | +| `NXHTTPTransport` | 테스트용 스텁이 필요하거나 전송 구현을 교체할 때 | `NXClientConfiguration(baseURL: url, transport: yourStubTransport)` | | `NXHTTPInterceptor` | 트레이싱이나 헤더 주입처럼 요청 전반에 적용되는 처리가 필요할 때 | `.intercept(yourInterceptor)` | | `NXAuthTokenProvider` | `.authorized()` 요청에 토큰 조회 및 갱신 기능이 필요할 때 | `authTokenProvider: yourAuthTokenProvider` | -| `NXServerErrorDecoder` | 실패 응답을 도메인 에러로 디코딩할 때 | `serverErrorDecoder: yourServerErrorDecoder` | +| `NXServerErrorDecoder` | 실패 응답을 도메인 오류로 디코딩할 때 | `serverErrorDecoder: yourServerErrorDecoder` | | `NXLogger` | 구조화된 요청 생명주기 로깅이 필요할 때 | `logger: yourLogger` | -| `NXNetworkMetricsObserver` | logger event 변경 없이 URLSession task 시간 snapshot이 필요할 때 | `NXURLSessionTransport(metricsObserver: yourObserver)` | +| `NXNetworkMetricsObserver` | 로거 이벤트 변경 없이 URLSession 작업 시간 스냅샷이 필요할 때 | `NXURLSessionTransport(metricsObserver: yourObserver)` | | `NXRawResponse` | `Data`와 `HTTPURLResponse`를 직접 다뤄야 할 때 | `let response = try await client.get("/users").send()` | | `NXError` | 호출 코드에서 Nexa 고유 오류를 처리할 때 | `catch let error as NXError` | | `NXHTTPMethod` | `NXEndpoint`의 메서드를 정의할 때 | `var method: NXHTTPMethod { .post }` | @@ -169,14 +169,14 @@ struct UserEndpoint: NXEndpoint { } ``` -기본 동작으로 충분하지 않을 때만 하위 레벨 프로토콜을 사용하면 됩니다. +기본 동작으로 충분하지 않을 때만 하위 수준 프로토콜을 사용하면 됩니다. -- `NXHTTPTransport`: 스텁, 목, 커스텀 네트워크 백엔드 -- `NXHTTPInterceptor`: 트레이싱, 요청 변환, 커스텀 흐름 제어 -- `NXAuthTokenProvider`: bearer token 주입 및 갱신 -- `NXServerErrorDecoder`: 서버 payload를 도메인 에러로 매핑 +- `NXHTTPTransport`: 스텁, 목, 사용자 정의 네트워크 백엔드 +- `NXHTTPInterceptor`: 트레이싱, 요청 변환, 사용자 정의 흐름 제어 +- `NXAuthTokenProvider`: Bearer 토큰 주입 및 갱신 +- `NXServerErrorDecoder`: 서버 페이로드를 도메인 오류로 매핑 - `NXLogger`: 요청 생명주기 로깅 및 관측성 -- `NXNetworkMetricsObserver`: URLSession task 시간 snapshot +- `NXNetworkMetricsObserver`: URLSession 작업 시간 스냅샷 ## 요청 흐름 @@ -274,12 +274,12 @@ let createdUser = try await client ### 요청 경로 -상대 경로는 `baseURL`의 기존 path 뒤에 결합되고 builder query는 기존 query 뒤에 추가됩니다. `client.get()`처럼 경로를 생략하면 `baseURL`에 포함된 path를 그대로 요청합니다. +상대 경로는 `baseURL`의 기존 경로 뒤에 결합되고 빌더 쿼리는 기존 쿼리 뒤에 추가됩니다. `client.get()`처럼 경로를 생략하면 `baseURL`에 포함된 경로를 그대로 요청합니다. -scheme을 포함한 절대 URL 문자열은 `baseURL` 대신 사용되며 해당 URL의 기존 query 뒤에 builder query가 추가됩니다. +스킴을 포함한 절대 URL 문자열은 `baseURL` 대신 사용되며 해당 URL의 기존 쿼리 뒤에 빌더 쿼리가 추가됩니다. > [!WARNING] -> 절대 URL에도 client 공통 header와 요청 header, 전역 및 요청 interceptor가 그대로 적용됩니다. `.authorized()` 요청은 Bearer token도 적용하므로 신뢰하는 host에만 절대 URL을 사용해야 합니다. 인증이나 민감 header가 설정된 client에서 다른 host의 절대 URL을 요청하지 마세요. +> 절대 URL에도 클라이언트 공통 헤더와 요청 헤더, 전역 및 요청 인터셉터가 그대로 적용됩니다. `.authorized()` 요청은 Bearer 토큰도 적용하므로 신뢰하는 호스트에만 절대 URL을 사용해야 합니다. 인증이나 민감한 헤더가 설정된 클라이언트에서 다른 호스트의 절대 URL을 요청하지 마세요. ## Endpoint API @@ -312,7 +312,7 @@ let user = try await client.send(UserEndpoint(identifier: 42)) ## 요청 전환 -Nexa 1.3은 기존 요청 API를 유지하면서 `NXRequestBuilder`를 단방향 method 기반 요청 경로로 구성합니다. +Nexa 1.3은 기존 요청 API를 유지하면서 `NXRequestBuilder`를 단방향 메서드 기반 요청 경로로 구성합니다. | 제거된 호출 | 대체 호출 | | --- | --- | @@ -321,11 +321,11 @@ Nexa 1.3은 기존 요청 API를 유지하면서 `NXRequestBuilder`를 단방향 | `client.get("/users").as(User.self).send()` | `client.get("/users").send(as: User.self)` | | `client.get("/users", as: User.self).send()` | `client.get("/users").send(as: User.self)` | -Nexa 1.3은 `NXRequestBuilder.raw()`와 `NXTypedRequestBuilder.raw()`를 제거합니다. 기존 `NXEndpoint.configure(_:)`와 `client.send(endpoint)` 계약은 유지하지만 Endpoint 요청에는 원시 응답 실행 API가 없습니다. 원시 응답 처리가 필요하면 `NXRequestBuilder`로 요청을 직접 구성해야 합니다. 이 전환은 빈 바디와 `204` 응답 동작을 변경하지 않습니다. +Nexa 1.3은 `NXRequestBuilder.raw()`와 `NXTypedRequestBuilder.raw()`를 제거합니다. 기존 `NXEndpoint.configure(_:)`와 `client.send(endpoint)` 계약은 유지하지만 엔드포인트 요청에는 원시 응답 실행 API가 없습니다. 원시 응답 처리가 필요하면 `NXRequestBuilder`로 요청을 직접 구성해야 합니다. 이 전환은 빈 바디와 `204` 응답 동작을 변경하지 않습니다. ## 설정 -`NXClientConfiguration`은 커스텀 API 레이어에 흩어지기 쉬운 설정을 한 곳에 집중시킵니다. +`NXClientConfiguration`은 사용자 정의 API 계층에 흩어지기 쉬운 설정을 한 곳에 집중시킵니다. ```swift import Foundation @@ -347,11 +347,11 @@ let configuration = NXClientConfiguration( let client = NXAPIClient(configuration: configuration) ``` -커스텀 동작이 필요할 때만 기본값을 직접 구현한 타입으로 교체하세요: +사용자 정의 동작이 필요할 때만 기본값을 직접 구현한 타입으로 교체하세요: - `NXLogger`: 구조화된 로깅 - `NXHTTPInterceptor`: 요청 트레이싱 또는 변환 -- `NXServerErrorDecoder`: 실패 응답을 도메인 에러로 매핑 +- `NXServerErrorDecoder`: 실패 응답을 도메인 오류로 매핑 - `NXAuthTokenProvider`: bearer token 주입 및 갱신 현재 Nexa가 지원하는 기능: @@ -359,52 +359,52 @@ let client = NXAPIClient(configuration: configuration) - 전역 헤더 및 요청별 헤더 - 원시 바디 및 JSON 바디 인코딩 - 요청 단위 유효성 검사 정책 -- 성공한 비인증 `GET` 응답의 TTL 내 memory 재사용 +- 성공한 비인증 `GET` 응답의 TTL 내 메모리 재사용 - 첫 요청이 실행 중일 때 진행 중인 동일 `GET` 요청 재사용 - `.authorized()` 요청에 대한 자동 인증 헤더 주입 - 토큰 갱신 및 재시도 처리 -- 스터빙 및 격리 테스트를 위한 커스텀 transport +- 스터빙 및 격리 테스트를 위한 사용자 정의 전송 ## 구조화된 로깅 -`NXLogger`는 하나의 논리 요청을 `requestIdentifier`로 연결하고 재시도 정책의 시도 번호를 `attemptNumber`로 전달합니다. `attemptNumber`는 1부터 시작하며 Bearer token 갱신 뒤의 요청 재전송에서는 증가하지 않습니다. +`NXLogger`는 하나의 논리 요청을 `requestIdentifier`로 연결하고 재시도 정책의 시도 번호를 `attemptNumber`로 전달합니다. `attemptNumber`는 1부터 시작하며 Bearer 토큰 갱신 뒤의 요청 재전송에서는 증가하지 않습니다. | 이벤트 | 발생 시점 | 주요 값 | | --- | --- | --- | -| `requestStart` | 로거 이후의 요청 실행 전 | 요청 식별자, 시도 번호, method, URL, header | +| `requestStart` | 로거 다음 단계의 요청 실행 전 | 요청 식별자, 시도 번호, 메서드, URL, 헤더 | | `requestEnd` | 로거 이후 실행에서 응답 반환 | 요청 식별자, 시도 번호, 상태 코드, 경과 시간, 응답 데이터 크기 | -| `requestFailure` | 로거 이후의 interceptor 또는 transport에서 오류 발생 | 요청 식별자, 시도 번호, 경과 시간, 오류 설명 | -| `retry` | 다음 retry 시도 예약 | 요청 식별자, 다음 시도 번호, 대기 시간 | -| `authRefresh` | 실제 Bearer token 갱신 완료 | 갱신을 시작한 요청 식별자, 성공 여부 | +| `requestFailure` | 로거 다음 단계의 인터셉터 또는 전송에서 오류 발생 | 요청 식별자, 시도 번호, 경과 시간, 오류 설명 | +| `retry` | 다음 재시도 예약 | 요청 식별자, 다음 시도 번호, 대기 시간 | +| `authRefresh` | 실제 Bearer 토큰 갱신 완료 | 갱신을 시작한 요청 식별자, 성공 여부 | -`requestStart`의 header에서는 `Authorization`과 `Cookie` 값만 대소문자와 관계없이 ``로 바뀝니다. URL query, 오류 설명, 다른 사용자 정의 민감 header는 자동으로 가려지지 않으므로 logger로 전달하기 전에 별도 보호가 필요합니다. +`requestStart`의 헤더에서는 `Authorization`과 `Cookie` 값만 헤더 이름의 대소문자와 관계없이 ``로 바뀝니다. URL 쿼리, 오류 설명, 다른 사용자 정의 민감 헤더는 자동으로 가려지지 않으므로 로거로 전달하기 전에 별도 보호가 필요합니다. -`NXLogger.log(_:)` 호출은 요청 실행 경로에서 기다립니다. 로거의 처리 시간이 길면 요청 실행과 retry 또는 인증 갱신 흐름도 지연될 수 있습니다. 응답 검증과 decoding은 logger 이후에 실행되므로 `requestFailure`가 모든 최종 `NXError`를 나타내지는 않습니다. +`NXLogger.log(_:)` 호출은 요청 실행 경로에서 기다립니다. 로거의 처리 시간이 길면 요청 실행과 재시도 또는 인증 갱신 흐름도 지연될 수 있습니다. 응답 검증과 디코딩은 로거 이후에 실행되므로 `requestFailure`가 모든 최종 `NXError`를 나타내지는 않습니다. ## 오류 처리 -Nexa의 공개 요청은 조립, 인증, 전송, 응답 검증, decoding 단계의 오류를 `NXError`로 구분합니다. +Nexa의 공개 요청은 조립, 인증, 전송, 응답 유효성 검사, 디코딩 단계의 오류를 `NXError`로 구분합니다. | 오류 | 발생 조건 | | --- | --- | -| `invalidRequest` | 유효한 URL 조립 실패 또는 interceptor의 HTTP method 변경 | -| `authenticationRequired` | 인증 요청에서 현재 Bearer token을 제공하지 못함 | +| `invalidRequest` | 유효한 URL 조립 실패 또는 인터셉터의 HTTP 메서드 변경 | +| `authenticationRequired` | 인증 요청에서 현재 Bearer 토큰을 제공하지 못함 | | `authProviderUnavailable` | `.authorized()` 요청에 `NXAuthTokenProvider`가 설정되지 않음 | | `timeout` | `URLError.timedOut` 발생 | | `cancelled` | `URLError.cancelled` 또는 Swift Task 취소 발생 | -| `transport` | timeout과 cancellation 외 `URLError` 발생 | -| `invalidStatus` | 응답 상태가 validation policy에 포함되지 않고 사용자 정의 서버 오류로 변환되지 않음 | +| `transport` | 타임아웃과 취소를 제외한 `URLError` 발생 | +| `invalidStatus` | 응답 상태가 유효성 검사 정책에 포함되지 않고 사용자 정의 서버 오류로 변환되지 않음 | | `server` | 응답 검증 정책이 거부한 응답을 `NXServerErrorDecoder`가 사용자 정의 오류로 변환함 | -| `decoding` | 성공 응답을 요청한 `Decodable` type으로 변환하지 못함 | +| `decoding` | 성공 응답을 요청한 `Decodable` 타입으로 디코딩하지 못함 | | `unknown` | 위 범주에 포함되지 않은 오류 발생 | -응답 검증 정책이 상태 코드를 거부하면 `NXServerErrorDecoder`가 먼저 실행됩니다. decoder가 오류를 반환하면 `server`, 반환하지 않으면 `invalidStatus`로 매핑됩니다. +응답 유효성 검사 정책이 상태 코드를 거부하면 `NXServerErrorDecoder`가 먼저 실행됩니다. 디코더가 오류를 반환하면 `server`로, 반환하지 않으면 `invalidStatus`로 매핑됩니다. ## 전송 측정값 -`NXURLSessionTransport`는 `URLSession` task마다 `NXNetworkMetrics` snapshot 하나를 `NXNetworkMetricsObserver`에 전달할 수 있습니다. snapshot에는 task duration, redirect 수, transaction 수, 수집 순서를 보존한 `NXNetworkTransactionMetrics` 값이 포함됩니다. +`NXURLSessionTransport`는 `URLSession` 작업마다 `NXNetworkMetrics` 스냅샷 하나를 `NXNetworkMetricsObserver`에 전달할 수 있습니다. 스냅샷에는 작업 소요 시간, 리디렉션 수, 트랜잭션 수, 수집 순서를 보존한 `NXNetworkTransactionMetrics` 값이 포함됩니다. -각 transaction은 Foundation timestamp의 시작과 끝이 모두 있을 때만 DNS, connection, TLS, request 시작부터 first byte까지의 duration을 나타냅니다. timestamp가 없으면 해당 값은 `nil`입니다. 재사용 connection은 `isConnectionReused`로 나타내며 DNS 또는 connection duration이 `nil`일 수 있습니다. +각 트랜잭션은 Foundation 타임스탬프의 시작과 끝이 모두 있을 때만 DNS, 연결, TLS, 요청 시작부터 첫 바이트까지의 구간 시간을 나타냅니다. 타임스탬프가 없으면 해당 값은 `nil`입니다. 재사용 연결은 `isConnectionReused`로 나타내며 DNS 또는 연결 구간 시간이 `nil`일 수 있습니다. ```swift import Foundation @@ -419,21 +419,21 @@ actor MetricsObserver: NXNetworkMetricsObserver { let transport = NXURLSessionTransport(metricsObserver: MetricsObserver()) ``` -snapshot은 `NXURLSessionTransport`만 수집합니다. 커스텀 `NXHTTPTransport`와 cache hit는 metrics를 합성하지 않습니다. observer 전달은 요청 완료를 지연하지 않으며 `NXLogger` event와의 순서는 보장하지 않습니다. +스냅샷은 `NXURLSessionTransport`만 수집합니다. 사용자 정의 `NXHTTPTransport`와 캐시 적중은 측정값을 합성하지 않습니다. 관측자 전달은 요청 완료를 지연하지 않으며 `NXLogger` 이벤트와의 순서는 보장하지 않습니다. ## 인증 토큰 갱신 -하나의 `NXAPIClient`에서 생성한 `.authorized()` 요청이 동시에 `401` 응답을 받으면 진행 중인 토큰 갱신 결과 하나를 공유합니다. 해당 client의 값 복사본과 여기서 파생한 builder도 같은 갱신을 공유합니다. 별도로 생성한 `NXAPIClient`는 독립된 갱신 수명을 가집니다. +하나의 `NXAPIClient`에서 생성한 `.authorized()` 요청이 동시에 `401` 응답을 받으면 진행 중인 토큰 갱신 결과 하나를 공유합니다. 해당 클라이언트의 값 복사본과 여기서 파생한 빌더도 같은 갱신을 공유합니다. 별도로 생성한 `NXAPIClient`는 독립된 갱신 수명을 가집니다. -`NXAuthTokenProvider`의 `currentAccessToken()`, `refreshAccessToken()` 요구 사항은 바뀌지 않습니다. 각 요청은 `nil`이 아닌 갱신 결과 뒤 최대 한 번만 재전송합니다. `nil` 갱신 결과에서는 인증 interceptor가 원래 `401` 응답을 유지하지만, 기본 응답 검증을 사용하는 공개 `send()`에서는 `NXError.invalidStatus`로 매핑될 수 있습니다. 갱신 오류는 Nexa의 기존 오류 매핑을 따릅니다. +`NXAuthTokenProvider`의 `currentAccessToken()`, `refreshAccessToken()` 요구 사항은 바뀌지 않습니다. 각 요청은 `nil`이 아닌 갱신 결과 뒤 최대 한 번만 재전송합니다. `nil` 갱신 결과에서는 인증 인터셉터가 원래 `401` 응답을 유지하지만, 기본 응답 검증을 사용하는 공개 `send()`에서는 `NXError.invalidStatus`로 매핑될 수 있습니다. 갱신 오류는 Nexa의 기존 오류 매핑을 따릅니다. 한 호출자의 취소는 공유 갱신이나 다른 대기 요청을 취소하지 않습니다. `NXAuthRefreshLog`는 실제 갱신 한 번당 한 번 기록되고, `requestIdentifier`는 해당 갱신을 시작한 요청의 식별자입니다. ## 응답 캐시 -`NXCache.memory(ttl:)`는 인증이 필요 없는 성공한 `GET` 응답을 지정한 TTL 동안 재사용하고, 첫 요청이 진행 중인 동일 요청을 하나로 합칩니다. TTL 만료 뒤에는 조건부 header를 추가하지 않습니다. +`NXCache.memory(ttl:)`는 인증이 필요 없는 성공한 `GET` 응답을 지정한 TTL 동안 재사용하고, 첫 요청이 진행 중인 동일 요청을 하나로 합칩니다. TTL 만료 뒤에는 조건부 헤더를 추가하지 않습니다. -`NXCache.revalidatingMemory(ttl:)`는 같은 cache 동작에 더해 `ETag` 또는 `Last-Modified` header가 있는 만료 `200` 응답을 재검증합니다. Nexa는 cache key 생성 뒤 `If-None-Match`, `If-Modified-Since`를 추가합니다. validator가 일치하고 body가 없는 `304 Not Modified`는 cached body를 `200` 응답으로 반환합니다. validator가 다르거나 body가 있는 `304`는 만료 응답을 제거하고 일반 응답 유효성 검사로 처리합니다. 변경된 `200`은 cached body와 validator를 교체합니다. cached `201`, `204`와 다른 성공 non-`200` 응답은 기존 TTL 만료 뒤 무조건 요청 경로를 유지합니다. +`NXCache.revalidatingMemory(ttl:)`는 같은 캐시 동작에 더해 `ETag` 또는 `Last-Modified` 헤더가 있는 만료 `200` 응답을 재검증합니다. Nexa는 캐시 키 생성 뒤 `If-None-Match`, `If-Modified-Since`를 추가합니다. 식별값이 일치하고 본문이 없는 `304 Not Modified`는 저장 본문을 `200` 응답으로 반환합니다. 식별값이 다르거나 본문이 있는 `304`는 만료 응답을 제거하고 일반 응답 유효성 검사로 처리합니다. 변경된 `200`은 저장 본문과 식별값을 교체합니다. 저장된 `201`, `204`와 `200` 이외의 다른 성공 응답은 기존 TTL 만료 뒤 무조건 요청 경로를 유지합니다. ```swift let client = NXAPIClient( @@ -444,7 +444,7 @@ let client = NXAPIClient( ) ``` -cache와 진행 중인 요청 store는 `NXAPIClient` 인스턴스에 속합니다. 요청 간 cache 상태를 공유하려면 service 또는 의존성 주입 계층에 client를 저장해 재사용해야 합니다. `NXAPIClient(configuration:)`를 새로 만들면 독립 store가 생성됩니다. 기존 client 값의 복사본은 원래 store를 공유합니다. +캐시와 진행 중인 요청 저장소는 `NXAPIClient` 인스턴스에 속합니다. 요청 간 캐시 상태를 공유하려면 서비스 또는 의존성 주입 계층에 클라이언트를 저장해 재사용해야 합니다. `NXAPIClient(configuration:)`를 새로 만들면 독립 저장소가 생성됩니다. 기존 클라이언트 값의 복사본은 원래 저장소를 공유합니다. ```swift struct UserService { @@ -456,13 +456,13 @@ struct UserService { } ``` -Nexa는 `Vary`, disk cache, 전체 `Cache-Control` 해석, stale-if-error를 구현하지 않습니다. `.revalidatingMemory(ttl:)`는 `NXCache` enum case를 추가하므로 `NXCache`의 모든 case를 나열한 `switch`는 재컴파일 시 새 case를 처리해야 합니다. +Nexa는 `Vary`, 디스크 캐시, 전체 `Cache-Control` 해석, stale-if-error를 구현하지 않습니다. `.revalidatingMemory(ttl:)`는 `NXCache` 열거형 case를 추가하므로 `NXCache`의 모든 case를 나열한 `switch`는 재컴파일 시 새 case를 처리해야 합니다. ## 재시도 정책 `.retry(...)`는 설정한 재시도 상태 코드 또는 전송 오류가 발생하면 기본으로 `GET`, `HEAD`, `PUT`, `DELETE`, `OPTIONS`를 재시도합니다. `maxAttempts`를 생략하면 세 번 시도합니다. 같은 요청을 반복해도 안전하게 처리하는 엔드포인트일 때만 `POST`, `PATCH`를 `allowing`에 명시적으로 추가할 수 있습니다. -재시도 가능한 `429`, `503` 응답에서는 `Retry-After`의 초 단위와 HTTP-date 값을 처리합니다. 유효한 서버 값은 local backoff를 대체하고 기본 60초인 `maximumServerDelay`로 제한되며 `NXRetryLog`에 기록됩니다. `NXRetryJitter.full`은 local backoff에만 적용되고 서버가 지정한 지연을 줄이지 않습니다. +재시도 가능한 `429`, `503` 응답에서는 `Retry-After`의 초 단위와 HTTP-date 값을 처리합니다. 유효한 서버 값은 클라이언트에서 계산한 backoff를 대체하고 기본 60초인 `maximumServerDelay`로 제한되며 `NXRetryLog`에 기록됩니다. `NXRetryJitter.full`은 클라이언트에서 계산한 backoff에만 적용되고 서버가 지정한 지연을 줄이지 않습니다. ```swift let user = try await client @@ -479,29 +479,29 @@ let user = try await client ## Nexa 1.3 전환 -Nexa 1.3에서는 공개 `NXRetryPolicy` 생성자, `NXRetryPolicy.Backoff`, `NXRetryPolicy.Jitter`, `.retry(_:)`를 제거합니다. `NXRetryPolicy`는 internal 구현 타입으로 유지합니다. `NXRetryBackoff`, `NXRetryJitter`와 `.retry(maxAttempts:backoff:retryableStatusCodes:allowing:maximumServerDelay:jitter:)`를 사용하며 `maxAttempts`의 기본값은 `3`입니다. +Nexa 1.3에서는 공개 `NXRetryPolicy` 생성자, `NXRetryPolicy.Backoff`, `NXRetryPolicy.Jitter`, `.retry(_:)`를 제거합니다. `NXRetryPolicy`는 내부 구현 타입으로 유지합니다. `NXRetryBackoff`, `NXRetryJitter`와 `.retry(maxAttempts:backoff:retryableStatusCodes:allowing:maximumServerDelay:jitter:)`를 사용하며 `maxAttempts`의 기본값은 `3`입니다. ## Interceptor 메서드 계약 -`NXHTTPInterceptor.replacingRequest(_:)`는 request URL, header, body를 바꿀 수 있지만 method는 설정한 method와 같아야 합니다. 다른 method는 이후 interceptor, logger, cache, transport 실행 전에 `NXError.invalidRequest`로 종료됩니다. +`NXHTTPInterceptor.replacingRequest(_:)`는 요청 URL, 헤더, 본문을 바꿀 수 있지만 메서드는 설정한 메서드와 같아야 합니다. 다른 메서드는 이후 인터셉터, 로거, 캐시, 전송이 실행되기 전에 `NXError.invalidRequest`로 종료됩니다. -`NXRequestExecutionContext.requestIdentifier`는 하나의 논리 요청과 해당 retry 시도에서 유지됩니다. `attemptNumber`는 1부터 시작하는 retry policy의 시도 번호이며 Bearer token 갱신 뒤의 재전송에서는 증가하지 않습니다. +`NXRequestExecutionContext.requestIdentifier`는 하나의 논리 요청과 해당 재시도에서 유지됩니다. `attemptNumber`는 1부터 시작하는 재시도 정책의 시도 번호이며 Bearer 토큰 갱신 뒤의 재전송에서는 증가하지 않습니다. ## 개발 -Nexa는 배포되는 package graph에서 SwiftLint를 분리하여 패키지 소비자가 maintainer용 lint 규칙을 함께 받지 않도록 구성합니다. +Nexa는 배포되는 패키지 그래프에서 SwiftLint를 분리하여 패키지 소비자가 라이브러리 개발자용 린트 규칙을 함께 받지 않도록 구성합니다. 로컬 라이브러리 개발 시에는 `Examples/NexaClient/NexaClient.xcodeproj`를 사용하면 됩니다. -- Xcode에서 `NexaClient` 타깃을 build하여 local package integration 경로를 확인 -- app target build 시 repo 루트 기준으로 `swiftlint` 실행 -- 로컬에 `swiftlint`가 없으면 script phase가 설치 안내와 함께 실패 +- Xcode에서 `NexaClient` 타깃을 빌드하여 로컬 패키지 통합 경로를 확인 +- 앱 타깃 빌드 시 저장소 루트 기준으로 `swiftlint` 실행 +- 로컬에 `swiftlint`가 없으면 스크립트 단계가 설치 안내와 함께 실패 -이 project는 라이브러리 개발 전용 통합 프로젝트이며 Swift Package Manager로 Nexa를 사용하는 앱에는 필요하지 않습니다. +이 프로젝트는 라이브러리 개발 전용 통합 프로젝트이며 Swift Package Manager로 Nexa를 사용하는 앱에는 필요하지 않습니다. ## 테스트 -Nexa는 요청 실행을 테스트하기 쉽도록 설계되었습니다. `NXHTTPTransport`를 통해 실제 네트워킹을 커스텀 transport로 교체하고 발신 요청과 디코딩된 응답을 검증할 수 있습니다. +Nexa는 요청 실행을 테스트하기 쉽도록 설계되었습니다. `NXHTTPTransport`를 통해 실제 네트워킹을 사용자 정의 전송으로 교체하고 발신 요청과 디코딩된 응답을 검증할 수 있습니다. ```swift import Foundation diff --git a/Sources/Core/NXCache.swift b/Sources/Core/NXCache.swift index 977d320..efed193 100644 --- a/Sources/Core/NXCache.swift +++ b/Sources/Core/NXCache.swift @@ -7,14 +7,14 @@ import Foundation -/// 성공한 GET 응답과 진행 중인 동일 요청의 cache 동작 +/// 성공한 GET 응답과 진행 중인 동일 요청의 캐시 동작 public enum NXCache: Sendable, Equatable { - /// 동일한 요청 cache 미적용, 개별 실행 + /// 동일한 요청에 캐시를 적용하지 않고 개별 실행 case disabled - /// 지정된 TTL 동안 성공한 GET 응답 메모리 저장, validator 재검증 없음, 진행 중 동일 GET 요청 결과 재사용 + /// 지정된 TTL 동안 성공한 GET 응답을 메모리에 저장하고 식별값으로 재검증하지 않으며 진행 중인 동일 GET 요청 결과 재사용 case memory(ttl: TimeInterval) - /// 지정된 TTL 동안 성공한 GET 응답 메모리 저장, 만료된 validator 기반 `200` 응답 재검증 + /// 지정된 TTL 동안 성공한 GET 응답을 메모리에 저장하고 식별값이 있는 만료 `200` 응답 재검증 /// - /// cache 소유권은 정책 적용 `NXAPIClient` instance 단위, client 재생성 시 독립 cache 생성 + /// 캐시 소유권은 정책을 적용한 `NXAPIClient` 인스턴스 단위이며 클라이언트를 새로 만들면 독립된 캐시 생성 case revalidatingMemory(ttl: TimeInterval) } diff --git a/Sources/Core/NXClientConfiguration.swift b/Sources/Core/NXClientConfiguration.swift index 15ea6d0..a43a43c 100644 --- a/Sources/Core/NXClientConfiguration.swift +++ b/Sources/Core/NXClientConfiguration.swift @@ -7,11 +7,11 @@ import Foundation -/// API client가 생성하는 모든 요청의 공통 설정 +/// API 클라이언트가 생성하는 모든 요청의 공통 설정 /// /// ## 개요 /// -/// 공통 네트워크 동작의 단일 정의 지점, `NXAPIClient` 통한 재사용 +/// 공통 네트워크 동작을 한 곳에서 정의하고 `NXAPIClient`를 통해 재사용 /// /// ```swift /// import Foundation @@ -32,38 +32,38 @@ import Foundation public struct NXClientConfiguration: Sendable { /// 상대 경로 요청 해석 기본 URL public let baseURL: URL - /// 모든 요청 공통 header(키 충돌 시 요청 단위 header 우선 적용) + /// 모든 요청의 공통 헤더(키 충돌 시 요청 단위 헤더 우선 적용) public let headers: [String: String] - /// 조립된 `URLRequest` 실행 transport + /// 조립된 `URLRequest`를 실행하는 전송 public let transport: any NXHTTPTransport - /// 요청 lifecycle event 수신 logger + /// 요청 생명주기 이벤트를 받는 로거 public let logger: any NXLogger - /// 요청별 적용 interceptor + /// 요청별로 적용할 인터셉터 public let interceptors: [any NXHTTPInterceptor] - /// 성공한 GET 응답 및 진행 중 동일 요청의 cache 동작 + /// 성공한 GET 응답 및 진행 중 동일 요청의 캐시 동작 public let cache: NXCache - /// type 지정 요청 응답 body decoder + /// 타입을 지정한 요청의 응답 본문 디코더 public let decoder: JSONDecoder - /// `json(_:encoder:)` 미전달 시 사용 JSON 요청 body encoder + /// `json(_:encoder:)`를 전달하지 않았을 때 사용할 JSON 요청 본문 인코더 public let encoder: JSONEncoder - /// 실패한 서버 응답을 도메인 전용 오류로 mapping하는 decoder + /// 실패한 서버 응답을 도메인 전용 오류로 매핑하는 디코더 public let serverErrorDecoder: any NXServerErrorDecoder - /// 인증 요청에서 베어러 토큰 조회 및 갱신 provider + /// 인증 요청에서 Bearer 토큰을 조회하고 갱신하는 제공자 public let authTokenProvider: (any NXAuthTokenProvider)? - /// client 구성 initialization + /// 클라이언트 구성 초기화 /// /// - Parameters: /// - baseURL: 상대 경로 요청 해석 기본 URL - /// - headers: 모든 요청 공통 header(키 충돌 시 요청 단위 header 우선 적용) - /// - transport: 요청 실행 transport - /// - logger: 요청 lifecycle event 수신 logger - /// - interceptors: 요청 적용 interceptor 목록 - /// - cache: 성공한 GET 응답 및 진행 중 동일 요청의 cache 동작 - /// - decoder: type 지정 응답 decoding decoder - /// - encoder: JSON 요청 body encoder - /// - serverErrorDecoder: 실패 응답 사용자 정의 오류 mapping decoder - /// - authTokenProvider: 인증 요청용 token provider + /// - headers: 모든 요청의 공통 헤더(키 충돌 시 요청 단위 헤더 우선 적용) + /// - transport: 요청을 실행하는 전송 + /// - logger: 요청 생명주기 이벤트를 받는 로거 + /// - interceptors: 요청에 적용할 인터셉터 목록 + /// - cache: 성공한 GET 응답 및 진행 중 동일 요청의 캐시 동작 + /// - decoder: 타입을 지정한 응답을 디코딩하는 디코더 + /// - encoder: JSON 요청 본문 인코더 + /// - serverErrorDecoder: 실패 응답을 사용자 정의 오류로 매핑하는 디코더 + /// - authTokenProvider: 인증 요청용 토큰 제공자 public init( baseURL: URL, headers: [String: String] = [:], diff --git a/Sources/Core/NXError.swift b/Sources/Core/NXError.swift index 7801f20..4becae0 100644 --- a/Sources/Core/NXError.swift +++ b/Sources/Core/NXError.swift @@ -7,25 +7,25 @@ import Foundation -/// Nexa 요청 조립·transport·validation·decoding 과정 오류 +/// Nexa 요청 조립, 전송, 유효성 검사, 디코딩 과정에서 발생하는 오류 public enum NXError: Error, Sendable { - /// 입력으로 유효한 HTTP 요청 생성 실패 + /// 입력으로 유효한 HTTP 요청을 생성하지 못함 case invalidRequest(String) - /// 인증 필요 요청에서 사용 가능한 토큰 미보유 + /// 인증이 필요한 요청에서 사용 가능한 토큰이 없음 case authenticationRequired - /// 인증 필요 요청에서 `NXAuthTokenProvider` 미설정 + /// 인증이 필요한 요청에 `NXAuthTokenProvider`가 설정되지 않음 case authProviderUnavailable - /// transport `URLError` 발생 + /// 전송 과정에서 `URLError`가 발생함 case transport(URLError) /// 요청 타임아웃 case timeout /// 요청 취소 case cancelled - /// 수신 상태 코드 응답 validation 실패 + /// 수신한 상태 코드의 유효성 검사 실패 case invalidStatus(statusCode: Int, data: Data?) - /// 실패 상태 코드 응답의 사용자 정의 서버 오류 decoding + /// 실패 상태 코드 응답의 사용자 정의 서버 오류 디코딩 case server(statusCode: Int, data: Data?, underlying: any Error) - /// 성공 요청 응답 decoding 실패 + /// 성공 응답 디코딩 실패 case decoding(any Error, data: Data?) /// 분류되지 않은 오류 case unknown(any Error) diff --git a/Sources/Core/NXHTTPMethod.swift b/Sources/Core/NXHTTPMethod.swift index fbaa158..e72febf 100644 --- a/Sources/Core/NXHTTPMethod.swift +++ b/Sources/Core/NXHTTPMethod.swift @@ -7,7 +7,7 @@ import Foundation -/// Nexa request builder 지원 HTTP method +/// Nexa가 요청 빌더에서 지원하는 HTTP 메서드 public enum NXHTTPMethod: String, Sendable, Hashable { case get = "GET" case post = "POST" diff --git a/Sources/Core/NXLogging.swift b/Sources/Core/NXLogging.swift index b0c5e22..e8c5b76 100644 --- a/Sources/Core/NXLogging.swift +++ b/Sources/Core/NXLogging.swift @@ -7,7 +7,7 @@ import Foundation -/// Nexa logger가 내보내는 요청 lifecycle event +/// Nexa 로거가 내보내는 요청 생명주기 이벤트 public enum NXLogEvent: Sendable { case requestStart(NXRequestStartLog) case requestEnd(NXRequestEndLog) @@ -16,7 +16,7 @@ public enum NXLogEvent: Sendable { case authRefresh(NXAuthRefreshLog) } -/// 요청 시도 시작 시점 구조화 payload +/// 요청 시도 시작 시점의 구조화된 페이로드 public struct NXRequestStartLog: Sendable { /// 동일한 논리 요청 시도 간 공유 안정적 식별자 public let requestIdentifier: UUID @@ -26,10 +26,10 @@ public struct NXRequestStartLog: Sendable { public let method: String /// 완전 해석된 요청 URL 문자열 public let url: String - /// 요청 최종 header + /// 요청의 최종 헤더 public let headers: [String: String] - /// 요청 시작 log payload 생성 + /// 요청 시작 로그 정보 생성 public init( requestIdentifier: UUID, attemptNumber: Int, @@ -45,7 +45,7 @@ public struct NXRequestStartLog: Sendable { } } -/// 요청 성공 종료 시점 구조화 payload +/// 요청 성공 종료 시점의 구조화된 페이로드 public struct NXRequestEndLog: Sendable { /// 동일한 논리 요청 시도 간 공유 안정적 식별자 public let requestIdentifier: UUID @@ -55,10 +55,10 @@ public struct NXRequestEndLog: Sendable { public let statusCode: Int /// 시도 실제 경과 시간 public let elapsedTime: TimeInterval - /// 응답 payload 크기(byte) + /// 응답 페이로드 크기(바이트) public let payloadSize: Int - /// 요청 완료 log payload 생성 + /// 요청 완료 로그 정보 생성 public init( requestIdentifier: UUID, attemptNumber: Int, @@ -74,7 +74,7 @@ public struct NXRequestEndLog: Sendable { } } -/// 요청 실패 시점 구조화 payload +/// 요청 실패 시점의 구조화된 페이로드 public struct NXRequestFailureLog: Sendable { /// 동일한 논리 요청 시도 간 공유 안정적 식별자 public let requestIdentifier: UUID @@ -85,7 +85,7 @@ public struct NXRequestFailureLog: Sendable { /// 사람이 읽기 쉬운 실패 설명 public let errorDescription: String - /// 요청 실패 log payload 생성 + /// 요청 실패 로그 정보 생성 public init( requestIdentifier: UUID, attemptNumber: Int, @@ -99,7 +99,7 @@ public struct NXRequestFailureLog: Sendable { } } -/// Nexa 다음 retry 예약 시 출력 구조화 payload +/// Nexa가 다음 재시도를 예약할 때 출력하는 구조화된 페이로드 public struct NXRetryLog: Sendable { /// 동일한 논리 요청 시도 간 공유 안정적 식별자 public let requestIdentifier: UUID @@ -108,7 +108,7 @@ public struct NXRetryLog: Sendable { /// 다음 시도 시작 전 대기 시간 public let delay: TimeInterval - /// retry log payload 생성 + /// 재시도 로그 정보 생성 public init(requestIdentifier: UUID, nextAttemptNumber: Int, delay: TimeInterval) { self.requestIdentifier = requestIdentifier self.nextAttemptNumber = nextAttemptNumber @@ -116,35 +116,35 @@ public struct NXRetryLog: Sendable { } } -/// 인증 토큰 갱신 시도 종료 후 출력 구조화 payload +/// 인증 토큰 갱신 시도가 끝난 뒤 출력하는 구조화된 페이로드 public struct NXAuthRefreshLog: Sendable { /// 갱신 시작 요청 식별자 public let requestIdentifier: UUID /// 갱신 시도 성공 여부 public let succeeded: Bool - /// 인증 갱신 log payload 생성 + /// 인증 갱신 로그 정보 생성 public init(requestIdentifier: UUID, succeeded: Bool) { self.requestIdentifier = requestIdentifier self.succeeded = succeeded } } -/// Nexa의 구조화된 요청 lifecycle event 수신 +/// Nexa의 구조화된 요청 생명주기 이벤트 수신 /// /// ## 개요 /// -/// 요청 lifecycle event의 자체 logging 또는 analytics pipeline 전달 대상 `NXLogger` 채택 +/// 요청 생명주기 이벤트를 직접 기록하거나 분석 파이프라인으로 전달할 때 `NXLogger` 채택 public protocol NXLogger: Sendable { - /// Nexa 발행 log event 단건 처리 + /// Nexa가 발행한 로그 이벤트 하나 처리 func log(_ event: NXLogEvent) async } -/// 발생 event 전체 무시 logger +/// 발생한 모든 이벤트를 무시하는 로거 public struct NXNoopLogger: NXLogger { - /// 무동작 logger 생성 + /// 아무 동작도 하지 않는 로거 생성 public init() {} - /// 전달 log event 무시 + /// 전달된 로그 이벤트 무시 public func log(_ event: NXLogEvent) async {} } diff --git a/Sources/Core/NXNetworkMetrics.swift b/Sources/Core/NXNetworkMetrics.swift index a54cbb9..474849d 100644 --- a/Sources/Core/NXNetworkMetrics.swift +++ b/Sources/Core/NXNetworkMetrics.swift @@ -7,21 +7,21 @@ import Foundation -/// `URLSession` task 기반 네트워크 활동 값 snapshot +/// `URLSession` 작업 기반 네트워크 활동 측정값 스냅샷 /// -/// `NXURLSessionTransport`는 `NXNetworkMetricsObserver` 전달 전 snapshot 생성 -/// 사용자 정의 `NXHTTPTransport` 구현은 snapshot 미생성 +/// `NXURLSessionTransport`는 `NXNetworkMetricsObserver`에 전달하기 전에 스냅샷 생성 +/// 사용자 정의 `NXHTTPTransport` 구현에서는 스냅샷을 생성하지 않음 public struct NXNetworkMetrics: Sendable, Equatable { - /// URL loading task 총 소요 시간 + /// URL 로딩 작업의 총 소요 시간 public let taskDuration: TimeInterval? - /// URL loading task redirect 횟수 + /// URL 로딩 작업의 리디렉션 횟수 public let redirectCount: Int - /// URL loading task 수집 transaction 개수 + /// URL 로딩 작업에서 수집한 트랜잭션 수 public let transactionCount: Int - /// `URLSession` 보고 순서 보존 transaction snapshot + /// `URLSession`이 보고한 순서를 보존한 트랜잭션 스냅샷 public let transactions: [NXNetworkTransactionMetrics] - /// 네트워크 metrics snapshot 생성 + /// 네트워크 측정값 스냅샷 생성 public init( taskDuration: TimeInterval?, redirectCount: Int, @@ -35,20 +35,20 @@ public struct NXNetworkMetrics: Sendable, Equatable { } } -/// 단일 URL loading transaction 값 snapshot +/// 단일 URL 로딩 트랜잭션의 측정값 스냅샷 public struct NXNetworkTransactionMetrics: Sendable, Equatable { - /// `URLSession` 시작·종료 timestamp 제공 시 DNS 조회 시간 + /// `URLSession`이 시작과 종료 타임스탬프를 모두 제공할 때의 DNS 조회 시간 public let domainLookupDuration: TimeInterval? - /// `URLSession` 시작·종료 timestamp 제공 시 연결 시작부터 종료까지의 시간 + /// `URLSession`이 시작과 종료 타임스탬프를 모두 제공할 때의 연결 시작부터 종료까지의 시간 public let connectionDuration: TimeInterval? - /// `URLSession` 시작·종료 timestamp 제공 시 TLS 협상 시간 + /// `URLSession`이 시작과 종료 타임스탬프를 모두 제공할 때의 TLS 협상 시간 public let secureConnectionDuration: TimeInterval? - /// `URLSession` 시작·종료 timestamp 제공 시 요청 시작부터 첫 응답 byte 수신까지의 시간 + /// `URLSession`이 시작과 종료 타임스탬프를 모두 제공할 때의 요청 시작부터 첫 응답 바이트 수신까지의 시간 public let timeToFirstByte: TimeInterval? /// 기존 연결 재사용 여부 public let isConnectionReused: Bool - /// transaction metrics snapshot 생성 + /// 트랜잭션 측정값 스냅샷 생성 public init( domainLookupDuration: TimeInterval?, connectionDuration: TimeInterval?, diff --git a/Sources/Core/NXNetworkMetricsObserver.swift b/Sources/Core/NXNetworkMetricsObserver.swift index 2f70b13..026d30b 100644 --- a/Sources/Core/NXNetworkMetricsObserver.swift +++ b/Sources/Core/NXNetworkMetricsObserver.swift @@ -5,10 +5,10 @@ // Created by opfic on 8/23/26. // -/// `NXURLSessionTransport` 수집 metrics snapshot 수신 대상 +/// `NXURLSessionTransport`가 수집한 측정값 스냅샷을 받는 대상 /// -/// 요청 완료 및 logger event 순서와 독립적 metrics 전달 +/// 요청 완료 및 로거 이벤트 순서와 관계없이 측정값 전달 public protocol NXNetworkMetricsObserver: Sendable { - /// 네트워크 metrics snapshot 단건 기록 + /// 네트워크 측정값 스냅샷 하나 기록 func record(_ metrics: NXNetworkMetrics) async } diff --git a/Sources/Core/NXProtocols.swift b/Sources/Core/NXProtocols.swift index 915c2d4..894c3d0 100644 --- a/Sources/Core/NXProtocols.swift +++ b/Sources/Core/NXProtocols.swift @@ -7,25 +7,25 @@ import Foundation -/// `URLRequest` 실행 네트워크 transport 추상화 +/// `URLRequest`를 실행하는 네트워크 전송 추상화 /// /// ## 개요 /// -/// 테스트에서 네트워크 응답 stub 처리 또는 기본 `URLSession` transport 교체 시 `NXHTTPTransport` 채택 +/// 테스트에서 네트워크 응답을 스텁으로 처리하거나 기본 `URLSession` 전송을 교체할 때 `NXHTTPTransport` 채택 public protocol NXHTTPTransport: Sendable { - /// 준비된 요청 transport 및 `NXRawResponse` 반환 + /// 준비된 요청을 전송하고 `NXRawResponse` 반환 func send(_ request: URLRequest) async throws -> NXRawResponse } -/// 실패한 서버 응답의 도메인별 오류 decoding +/// 실패한 서버 응답의 도메인별 오류 디코딩 public protocol NXServerErrorDecoder: Sendable { - /// 실패한 HTTP 응답 기반 사용자 정의 오류 decoding 시도 + /// 실패한 HTTP 응답을 기반으로 사용자 정의 오류 디코딩 시도 func decodeServerError(data: Data, response: HTTPURLResponse, decoder: JSONDecoder) -> (any Error)? } -/// 사용자 정의 오류 미생성 기본 서버 오류 decoder +/// 사용자 정의 오류를 생성하지 않는 기본 서버 오류 디코더 public struct NXDefaultServerErrorDecoder: NXServerErrorDecoder { - /// 기본 서버 오류 decoder initialization + /// 기본 서버 오류 디코더 초기화 public init() {} /// 항상 `nil` 반환 @@ -34,11 +34,11 @@ public struct NXDefaultServerErrorDecoder: NXServerErrorDecoder { } } -/// 인증 요청·토큰 갱신용 베어러 token provider +/// 인증 요청과 토큰 갱신을 위한 Bearer 토큰 제공자 /// /// ## 개요 /// -/// `.authorized()` 사용 시 `NXAuthTokenProvider` 구성 point +/// `.authorized()` 사용 시 `NXAuthTokenProvider` 구성 지점 /// /// ```swift /// import Nexa diff --git a/Sources/Core/NXRawResponse.swift b/Sources/Core/NXRawResponse.swift index 7b35758..d70cd0d 100644 --- a/Sources/Core/NXRawResponse.swift +++ b/Sources/Core/NXRawResponse.swift @@ -7,14 +7,14 @@ import Foundation -/// decoding 전 반환용 HTTP 응답 container +/// 디코딩 전에 반환하는 HTTP 응답 컨테이너 public struct NXRawResponse: Sendable { - /// 응답 body data + /// 응답 본문 데이터 public var data: Data - /// body 연계 HTTP 응답 + /// 본문과 연결된 HTTP 응답 public var response: HTTPURLResponse - /// HTTP 응답 container 생성 + /// HTTP 응답 컨테이너 생성 public init(data: Data, response: HTTPURLResponse) { self.data = data self.response = response diff --git a/Sources/Core/NXRequestBody.swift b/Sources/Core/NXRequestBody.swift index b15e9f9..7bdd09a 100644 --- a/Sources/Core/NXRequestBody.swift +++ b/Sources/Core/NXRequestBody.swift @@ -7,7 +7,7 @@ import Foundation -/// Nexa 요청 body 표현 방식 +/// Nexa 요청 본문 표현 방식 public enum NXRequestBody: Sendable { case data(Data) diff --git a/Sources/Core/NXRetryBackoff.swift b/Sources/Core/NXRetryBackoff.swift index 2c9332d..702075b 100644 --- a/Sources/Core/NXRetryBackoff.swift +++ b/Sources/Core/NXRetryBackoff.swift @@ -7,11 +7,11 @@ import Foundation -/// retry 간격 delay 전략 +/// 재시도 간격 지연 전략 public enum NXRetryBackoff: Sendable { - /// retry 시 고정 delay 사용 + /// 재시도 시 고정 지연 사용 case fixed(TimeInterval) - /// 최대 delay 도달 시까지 시도별 두 배 delay 증가 + /// 최대 지연에 도달할 때까지 시도마다 지연을 두 배로 늘림 case exponential(base: TimeInterval, maxDelay: TimeInterval) func delay(forAttempt attemptNumber: Int) -> TimeInterval { diff --git a/Sources/Core/NXRetryJitter.swift b/Sources/Core/NXRetryJitter.swift index ad4f504..4ec861f 100644 --- a/Sources/Core/NXRetryJitter.swift +++ b/Sources/Core/NXRetryJitter.swift @@ -5,10 +5,10 @@ // Created by opfic on 8/23/26. // -/// 로컬 retry backoff delay 무작위화 +/// 로컬 재시도 backoff 지연 무작위화 public enum NXRetryJitter: Sendable, Equatable { - /// 로컬 backoff delay 비변경 + /// 로컬 backoff 지연 변경 없음 case none - /// 로컬 backoff delay 범위 임의값 사용 + /// 로컬 backoff 지연 범위의 임의값 사용 case full } diff --git a/Sources/Core/NXValidationPolicy.swift b/Sources/Core/NXValidationPolicy.swift index f7759d4..ed824a4 100644 --- a/Sources/Core/NXValidationPolicy.swift +++ b/Sources/Core/NXValidationPolicy.swift @@ -7,9 +7,9 @@ import Foundation -/// 응답 상태 코드 성공 validation 규칙 +/// 응답 상태 코드 유효성 검사 규칙 public enum NXValidationPolicy: Sendable { - /// 상태 코드 validation 비활성화 + /// 상태 코드 유효성 검사 비활성화 case none /// `200..<300` 범위 상태 코드만 허용 case successStatusCode diff --git a/Sources/Public/NXAPIClient.swift b/Sources/Public/NXAPIClient.swift index 7cf6eb7..df99046 100644 --- a/Sources/Public/NXAPIClient.swift +++ b/Sources/Public/NXAPIClient.swift @@ -7,16 +7,16 @@ import Foundation -/// 단일 client 설정 기반 HTTP 요청 구성·전송 공용 진입점 +/// 단일 클라이언트 설정을 기반으로 HTTP 요청을 구성하고 전송하는 공용 진입점 /// /// ## 개요 /// -/// client 단일 생성 후 상대 경로 또는 scheme 포함 절대 URL 문자열 기반 요청 시작 +/// 클라이언트를 한 번 생성한 뒤 상대 경로나 스킴을 포함한 절대 URL 문자열로 요청 시작 /// -/// 절대 URL 문자열은 기본 URL을 대체하지만 client 공통 header, interceptor, 인증 정책은 유지 -/// 인증이나 민감 header가 설정된 client에서는 신뢰하는 host에만 절대 URL 사용 +/// 절대 URL 문자열은 기본 URL을 대체하지만 클라이언트 공통 헤더, 인터셉터, 인증 정책은 유지 +/// 인증이나 민감한 헤더가 설정된 클라이언트에서는 신뢰하는 호스트에만 절대 URL 사용 /// -/// cache 사용 시 각 `NXAPIClient` 초기화마다 독립 메모리 cache와 진행 중 request store 생성. cache 응답 공유는 서비스 또는 DI 계층에서 client 단일 재사용으로 보장. 동일 client 값 복사본은 동일 store 유지 +/// 캐시 사용 시 각 `NXAPIClient` 초기화마다 독립된 메모리 캐시와 진행 중 요청 저장소 생성. 캐시 응답 공유는 서비스 또는 의존성 주입 계층에서 클라이언트를 재사용해 보장. 동일한 클라이언트 값의 복사본은 같은 저장소 유지 /// /// ```swift /// import Foundation @@ -38,15 +38,15 @@ import Foundation /// .send(as: User.self) /// ``` /// -/// 직접 요청은 method 기반 builder 경로, `NXEndpoint`는 type 지정 builder 구성 contract의 source 호환성 유지 +/// 직접 요청은 메서드 기반 빌더 경로를 사용하고 `NXEndpoint`는 타입 지정 빌더 구성 계약의 소스 호환성 유지 public struct NXAPIClient: Sendable { private let configuration: NXClientConfiguration private let responseCacheStore: NXResponseCacheStore? private let authRefreshCoordinator: NXAuthRefreshCoordinator - /// 모든 요청 공통 설정 반영 client 생성 + /// 모든 요청의 공통 설정을 반영한 클라이언트 생성 /// - /// - Parameter configuration: 기본 URL, transport, logger, `NXAuthTokenProvider` 같은 공용 설정 + /// - Parameter configuration: 기본 URL, 전송, 로거, `NXAuthTokenProvider` 같은 공용 설정 public init(configuration: NXClientConfiguration) { self.init( configuration: configuration, @@ -73,21 +73,21 @@ public struct NXAPIClient: Sendable { } } - /// 지정 경로 기준 type 미지정 `GET` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정하지 않은 `GET` 요청 빌더 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// 빈 값은 기본 URL path 유지 - /// - Returns: 전송 전 추가 설정 가능한 request builder + /// - Parameter path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// 빈 값은 기본 URL 경로 유지 + /// - Returns: 전송 전에 추가로 설정할 수 있는 요청 빌더 public func get(_ path: String = "") -> NXRequestBuilder { request(method: .get, path: path) } - /// 지정 경로 기준 type 지정 `GET` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정한 `GET` 요청 빌더 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열. 빈 값은 기본 URL path 유지 - /// - type: 성공 응답 decoding 대상 type - /// - Returns: `Response` decoding type 지정 request builder + /// - path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열. 빈 값은 기본 URL 경로 유지 + /// - type: 성공 응답 디코딩 대상 타입 + /// - Returns: `Response` 디코딩 타입을 지정한 요청 빌더 @available(*, deprecated, message: "Use get(_:) followed by send(as:).") public func get( _ path: String = "", @@ -96,94 +96,94 @@ public struct NXAPIClient: Sendable { typedRequest(method: .get, path: path) } - /// 지정 경로 기준 type 미지정 `POST` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정하지 않은 `POST` 요청 빌더 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - Returns: 전송 전 추가 설정 가능한 request builder + /// - Parameter path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - Returns: 전송 전에 추가로 설정할 수 있는 요청 빌더 public func post(_ path: String) -> NXRequestBuilder { request(method: .post, path: path) } - /// 지정 경로 기준 type 지정 `POST` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정한 `POST` 요청 빌더 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - type: 성공 응답 decoding 대상 type - /// - Returns: `Response` decoding type 지정 request builder + /// - path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - type: 성공 응답 디코딩 대상 타입 + /// - Returns: `Response` 디코딩 타입을 지정한 요청 빌더 @available(*, deprecated, message: "Use post(_:) followed by send(as:).") public func post(_ path: String, as type: Response.Type) -> NXTypedRequestBuilder { typedRequest(method: .post, path: path) } - /// 지정 경로 기준 type 미지정 `PUT` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정하지 않은 `PUT` 요청 빌더 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - Returns: 전송 전 추가 설정 가능한 request builder + /// - Parameter path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - Returns: 전송 전에 추가로 설정할 수 있는 요청 빌더 public func put(_ path: String) -> NXRequestBuilder { request(method: .put, path: path) } - /// 지정 경로 기준 type 지정 `PUT` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정한 `PUT` 요청 빌더 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - type: 성공 응답 decoding 대상 type - /// - Returns: `Response` decoding type 지정 request builder + /// - path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - type: 성공 응답 디코딩 대상 타입 + /// - Returns: `Response` 디코딩 타입을 지정한 요청 빌더 @available(*, deprecated, message: "Use put(_:) followed by send(as:).") public func put(_ path: String, as type: Response.Type) -> NXTypedRequestBuilder { typedRequest(method: .put, path: path) } - /// 지정 경로 기준 type 미지정 `PATCH` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정하지 않은 `PATCH` 요청 빌더 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - Returns: 전송 전 추가 설정 가능한 request builder + /// - Parameter path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - Returns: 전송 전에 추가로 설정할 수 있는 요청 빌더 public func patch(_ path: String) -> NXRequestBuilder { request(method: .patch, path: path) } - /// 지정 경로 기준 type 지정 `PATCH` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정한 `PATCH` 요청 빌더 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - type: 성공 응답 decoding 대상 type - /// - Returns: `Response` decoding type 지정 request builder + /// - path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - type: 성공 응답 디코딩 대상 타입 + /// - Returns: `Response` 디코딩 타입을 지정한 요청 빌더 @available(*, deprecated, message: "Use patch(_:) followed by send(as:).") public func patch(_ path: String, as type: Response.Type) -> NXTypedRequestBuilder { typedRequest(method: .patch, path: path) } - /// 지정 경로 기준 type 미지정 `DELETE` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정하지 않은 `DELETE` 요청 빌더 생성 /// - /// - Parameter path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - Returns: 전송 전 추가 설정 가능한 request builder + /// - Parameter path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - Returns: 전송 전에 추가로 설정할 수 있는 요청 빌더 public func delete(_ path: String) -> NXRequestBuilder { request(method: .delete, path: path) } - /// 지정 경로 기준 type 지정 `DELETE` request builder 생성 + /// 지정 경로를 기준으로 타입을 지정한 `DELETE` 요청 빌더 생성 /// /// - Parameters: - /// - path: 설정된 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 - /// - type: 성공 응답 decoding 대상 type - /// - Returns: `Response` decoding type 지정 request builder + /// - path: 설정된 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 + /// - type: 성공 응답 디코딩 대상 타입 + /// - Returns: `Response` 디코딩 타입을 지정한 요청 빌더 @available(*, deprecated, message: "Use delete(_:) followed by send(as:).") public func delete(_ path: String, as type: Response.Type) -> NXTypedRequestBuilder { typedRequest(method: .delete, path: path) } - /// endpoint에서 type 지정 요청 구성 + /// 엔드포인트에서 타입을 지정한 요청 구성 /// - /// - Parameter endpoint: HTTP method, 경로, 선택적 요청 customization 정의 endpoint - /// - Returns: endpoint 기반 type 지정 request builder + /// - Parameter endpoint: HTTP 메서드, 경로, 선택적 요청 설정을 정의한 엔드포인트 + /// - Returns: 엔드포인트 기반 타입 지정 요청 빌더 public func request(_ endpoint: E) -> NXTypedRequestBuilder { endpoint.configure(typedRequest(method: endpoint.method, path: endpoint.path)) } - /// endpoint 요청 전송 및 응답 decoding + /// 엔드포인트 요청 전송 및 응답 디코딩 /// - /// - Parameter endpoint: 요청 동작 정의 endpoint - /// - Returns: endpoint 응답 decoding 결과 + /// - Parameter endpoint: 요청 동작을 정의한 엔드포인트 + /// - Returns: 엔드포인트 응답 디코딩 결과 public func send(_ endpoint: E) async throws -> E.Response { try await request(endpoint).requestBuilder.send(as: E.Response.self) } diff --git a/Sources/Public/NXEndpoint.swift b/Sources/Public/NXEndpoint.swift index d8d4f07..8d67c93 100644 --- a/Sources/Public/NXEndpoint.swift +++ b/Sources/Public/NXEndpoint.swift @@ -7,11 +7,11 @@ import Foundation -/// 고정 응답 type을 갖는 재사용 가능한 API endpoint 설명 +/// 고정 응답 타입을 갖는 재사용 가능한 API 엔드포인트 정의 /// /// ## 개요 /// -/// 동일 요청 구조 반복 사용 및 응답 type 보존이 필요한 재사용 가능한 endpoint 정의 +/// 동일한 요청 구조를 반복해서 사용하고 응답 타입을 보존할 수 있는 엔드포인트 정의 /// /// ```swift /// import Foundation @@ -36,24 +36,24 @@ import Foundation /// let user = try await client.send(UserEndpoint(identifier: 42)) /// ``` public protocol NXEndpoint { - /// endpoint 요청 성공 시 decoding 대상 응답 type + /// 엔드포인트 요청 성공 시 디코딩 대상 응답 타입 associatedtype Response: Decodable /// 요청에 사용되는 HTTP 메서드 var method: NXHTTPMethod { get } - /// client 기본 URL 기준 상대 경로 또는 scheme 포함 절대 URL 문자열 + /// 클라이언트 기본 URL을 기준으로 하는 상대 경로 또는 스킴을 포함한 절대 URL 문자열 var path: String { get } - /// 전송 전 endpoint별 customization을 builder에 적용 + /// 전송 전에 엔드포인트별 추가 설정을 빌더에 적용 /// - /// - Parameter builder: `method`와 `path` 기반 기본 type 지정 request builder - /// - Returns: customization 적용 type 지정 request builder + /// - Parameter builder: `method`와 `path`를 기반으로 만든 타입 지정 요청 빌더 + /// - Returns: 추가 설정을 적용한 타입 지정 요청 빌더 func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder } public extension NXEndpoint { - /// endpoint 추가 customization 미필요 시 builder 원본 반환 + /// 엔드포인트에 추가 설정이 필요하지 않을 때 빌더 원본 반환 /// - /// - Parameter builder: `method`와 `path` 기반 기본 type 지정 request builder - /// - Returns: 동일 builder instance + /// - Parameter builder: `method`와 `path`를 기반으로 만든 타입 지정 요청 빌더 + /// - Returns: 동일한 빌더 인스턴스 func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder { builder } diff --git a/Sources/Public/NXHTTPInterceptor.swift b/Sources/Public/NXHTTPInterceptor.swift index 6327945..b5e6b7d 100644 --- a/Sources/Public/NXHTTPInterceptor.swift +++ b/Sources/Public/NXHTTPInterceptor.swift @@ -11,7 +11,7 @@ import Foundation /// /// ## 개요 /// -/// 추적, 사용자 정의 header, 응답 관찰 같은 횡단 관심사 처리용 interceptor 구현. 요청 URL/header/body 변경 허용, 설정된 HTTP method 보존 +/// 추적, 사용자 정의 헤더, 응답 관찰 같은 횡단 관심사를 처리하는 인터셉터 구현. 요청 URL, 헤더, 본문 변경을 허용하고 설정된 HTTP 메서드 보존 /// /// ```swift /// import Foundation @@ -33,18 +33,18 @@ public protocol NXHTTPInterceptor: Sendable { /// /// - Parameters: /// - context: 현재 요청 실행 상태 - /// - next: interceptor chain 연결 closure - /// - Returns: 현재 interceptor 또는 이후 단계 생성 `NXRawResponse` + /// - next: 인터셉터 체인을 연결하는 클로저 + /// - Returns: 현재 인터셉터 또는 이후 단계가 생성한 `NXRawResponse` func intercept( context: NXRequestExecutionContext, next: @escaping @Sendable (NXRequestExecutionContext) async throws -> NXRawResponse ) async throws -> NXRawResponse } -/// interceptor 노출용 현재 요청 실행 상태 snapshot +/// 인터셉터에 제공하는 현재 요청 실행 상태 스냅샷 /// -/// `NXRequestExecutionContext`의 준비된 요청, 요청 식별자, retry 시도 번호를 interceptor에서 조회 가능 -/// `userInfo`는 현재 공개 request builder에서 설정되지 않아 빈 dictionary로 전달 +/// `NXRequestExecutionContext`의 준비된 요청, 요청 식별자, 재시도 번호를 인터셉터에서 조회 가능 +/// `userInfo`는 현재 공개 요청 빌더에서 설정되지 않아 빈 딕셔너리로 전달 public struct NXRequestExecutionContext: Sendable { /// 현재 실행 중인 요청 public let request: URLRequest @@ -52,19 +52,19 @@ public struct NXRequestExecutionContext: Sendable { public let requestIdentifier: UUID /// 현재 시도 번호(`1`부터 시작) public let attemptNumber: Int - /// 현재 공개 request builder에서 설정되지 않아 빈 dictionary로 전달되는 metadata 값 + /// 현재 공개 요청 빌더에서 설정되지 않아 빈 딕셔너리로 전달되는 메타데이터 값 public let userInfo: [String: String] let specification: RequestSpec let clientConfiguration: NXClientConfiguration let authRefreshCoordinator: NXAuthRefreshCoordinator - /// 대체 요청 기반 context 사본 생성 + /// 대체 요청을 기반으로 실행 상태 사본 생성 /// - /// - Parameter request: 이후 chain에서 사용할 대체 요청(`httpMethod`는 설정된 요청 method와 동일) - /// - Returns: 갱신된 요청 포함 신규 실행 context + /// - Parameter request: 이후 체인에서 사용할 대체 요청(`httpMethod`는 설정된 요청 메서드와 동일) + /// - Returns: 갱신된 요청을 포함한 신규 실행 상태 /// - /// `httpMethod` 값 불일치 또는 누락 요청 전달 시, 이후 interceptor/logging/cache/transport 이전 ``NXError/invalidRequest(_:)`` 실행 종료 + /// `httpMethod` 값이 다르거나 누락된 요청을 전달하면 이후 인터셉터, 로깅, 캐시, 전송이 실행되기 전에 ``NXError/invalidRequest(_:)``로 종료 public func replacingRequest(_ request: URLRequest) -> Self { Self( request: request, diff --git a/Sources/Public/NXRequestBuilder.swift b/Sources/Public/NXRequestBuilder.swift index feb8502..cfde13c 100644 --- a/Sources/Public/NXRequestBuilder.swift +++ b/Sources/Public/NXRequestBuilder.swift @@ -7,11 +7,11 @@ import Foundation -/// HTTP 요청 구성·전송의 값 의미론 builder +/// 값 의미론을 사용하는 HTTP 요청 구성 및 전송 빌더 /// /// ## 개요 /// -/// `NXRequestBuilder` 준비 요청 확인, `send()`로 `NXRawResponse` 획득, `send(as:)`로 응답 decoding 지원. `NXRawResponse` 실행 API는 `send()` 단일 노출 +/// `NXRequestBuilder`로 준비된 요청을 확인하고 `send()`로 `NXRawResponse` 획득, `send(as:)`로 응답 디코딩 지원. `NXRawResponse` 실행 API는 `send()`만 노출 /// /// ```swift /// import Foundation @@ -55,46 +55,46 @@ public struct NXRequestBuilder: Sendable { /// - Parameters: /// - key: 쿼리 항목 이름 /// - value: `String(describing:)`로 변환한 쿼리 항목 값 - /// - Returns: 갱신된 request builder + /// - Returns: 갱신된 요청 빌더 public func query(_ key: String, _ value: some CustomStringConvertible) -> Self { modifying { requestSpec in requestSpec.queryItems.append(URLQueryItem(name: key, value: String(describing: value))) } } - /// 단일 HTTP header 설정/교체 + /// 단일 HTTP 헤더 설정 또는 교체 /// /// - Parameters: - /// - key: header field 이름 - /// - value: header field 값 - /// - Returns: 갱신된 request builder + /// - key: 헤더 필드 이름 + /// - value: 헤더 필드 값 + /// - Returns: 갱신된 요청 빌더 public func header(_ key: String, _ value: String) -> Self { modifying { requestSpec in requestSpec.headers[key] = value } } - /// 다수 HTTP header 병합 + /// 여러 HTTP 헤더 병합 /// - /// - Parameter values: 추가 header field 이름/값. 키 충돌 시 새 값 우선 - /// - Returns: 갱신된 request builder + /// - Parameter values: 추가할 헤더 필드 이름과 값. 키 충돌 시 새 값 우선 + /// - Returns: 갱신된 요청 빌더 public func headers(_ values: [String: String]) -> Self { modifying { requestSpec in requestSpec.headers.merge(values) { _, newValue in newValue } } } - /// `Accept` header 설정 + /// `Accept` 헤더 설정 /// - /// - Parameter value: `Accept` header media type 값 - /// - Returns: 갱신된 request builder + /// - Parameter value: `Accept` 헤더의 미디어 타입 값 + /// - Returns: 갱신된 요청 빌더 public func accept(_ value: String) -> Self { header("Accept", value) } /// 설정된 `NXAuthTokenProvider`를 통한 인증 필요 표시 /// - /// - Returns: 갱신된 request builder + /// - Returns: 갱신된 요청 빌더 public func authorized() -> Self { modifying { requestSpec in requestSpec.authRequirement = .required @@ -104,19 +104,19 @@ public struct NXRequestBuilder: Sendable { /// 요청별 타임아웃 간격 설정 /// /// - Parameter seconds: 타임아웃 간격(초). 음수 값은 `0` 보정 - /// - Returns: 갱신된 request builder + /// - Returns: 갱신된 요청 빌더 public func timeout(_ seconds: TimeInterval) -> Self { modifying { requestSpec in requestSpec.timeout = max(0, seconds) } } - /// `Encodable` 값 JSON encoding 및 `Content-Type` header 설정 + /// `Encodable` 값을 JSON으로 인코딩하고 `Content-Type` 헤더 설정 /// /// - Parameters: - /// - value: 요청 body encoding 대상 값 - /// - encoder: 사용 encoder. 미지정 시 client 설정 encoder 사용 - /// - Returns: 갱신된 request builder + /// - value: 요청 본문 인코딩 대상 값 + /// - encoder: 사용할 인코더. 지정하지 않으면 클라이언트 설정의 인코더 사용 + /// - Returns: 갱신된 요청 빌더 public func json(_ value: T, encoder: JSONEncoder? = nil) throws -> Self { let selectedEncoder = encoder ?? clientConfiguration.encoder let encodedValue = try selectedEncoder.encode(value) @@ -127,36 +127,36 @@ public struct NXRequestBuilder: Sendable { } } - /// 원시 요청 body data 설정 + /// 원시 요청 본문 데이터 설정 /// - /// - Parameter data: HTTP body 입력 data - /// - Returns: 갱신된 request builder + /// - Parameter data: HTTP 본문 입력 데이터 + /// - Returns: 갱신된 요청 빌더 public func body(_ data: Data) -> Self { modifying { requestSpec in requestSpec.body = .data(data) } } - /// `Content-Type` header 설정/교체 + /// `Content-Type` 헤더 설정 또는 교체 /// - /// - Parameter value: `Content-Type` header media type 값 - /// - Returns: 갱신된 request builder + /// - Parameter value: `Content-Type` 헤더의 미디어 타입 값 + /// - Returns: 갱신된 요청 빌더 public func contentType(_ value: String) -> Self { modifying { requestSpec in requestSpec.headers["Content-Type"] = value } } - /// 요청 retry policy 적용 + /// 요청 재시도 정책 적용 /// /// - Parameters: /// - maxAttempts: 초기 요청 포함 최대 시도 횟수(기본 `3`) - /// - backoff: retry 간 지연 전략 - /// - retryableStatusCodes: retry 유발 상태 코드 집합 + /// - backoff: 재시도 간 지연 전략 + /// - retryableStatusCodes: 재시도를 유발하는 상태 코드 집합 /// - allowing: 기본 멱등 메서드와 함께 허용할 추가 HTTP 메서드 - /// - maximumServerDelay: 서버 retry 지연 상한 + /// - maximumServerDelay: 서버 재시도 지연 상한 /// - jitter: 로컬 backoff 지연 무작위화 설정 - /// - Returns: 갱신된 request builder + /// - Returns: 갱신된 요청 빌더 public func retry( maxAttempts: Int = 3, backoff: NXRetryBackoff = .fixed(0), @@ -177,20 +177,20 @@ public struct NXRequestBuilder: Sendable { } } - /// 요청 응답 validation policy 적용 + /// 요청 응답 유효성 검사 정책 적용 /// - /// - Parameter policy: transport 응답 반환 후 적용 validation 규칙 - /// - Returns: 갱신된 request builder + /// - Parameter policy: 전송 응답 반환 후 적용할 유효성 검사 규칙 + /// - Returns: 갱신된 요청 빌더 public func validate(_ policy: NXValidationPolicy) -> Self { modifying { requestSpec in requestSpec.validationPolicy = policy } } - /// 요청 단위 interceptor 추가 + /// 요청 단위 인터셉터 추가 /// - /// - Parameter interceptor: 전역 interceptor 뒤에 추가할 interceptor - /// - Returns: 갱신된 request builder + /// - Parameter interceptor: 전역 인터셉터 뒤에 추가할 인터셉터 + /// - Returns: 갱신된 요청 빌더 public func intercept(_ interceptor: any NXHTTPInterceptor) -> Self { modifying { requestSpec in requestSpec.requestInterceptors.append(interceptor) @@ -199,14 +199,14 @@ public struct NXRequestBuilder: Sendable { /// 최종 `URLRequest` 조립(전송 제외) /// - /// - Returns: 완전 준비된 URLRequest + /// - Returns: 완전히 준비된 URLRequest public func preparedURLRequest() async throws -> URLRequest { try NXRequestAssembler.assemble(clientConfiguration: clientConfiguration, requestSpec: requestSpec) } /// 요청 전송 및 `NXRawResponse` 반환 /// - /// - Returns: `NXRawResponse`의 응답 data와 HTTP metadata + /// - Returns: `NXRawResponse`의 응답 데이터와 HTTP 메타데이터 public func send() async throws -> NXRawResponse { try await NXRequestExecutor.executeRaw( clientConfiguration: clientConfiguration, @@ -216,25 +216,25 @@ public struct NXRequestBuilder: Sendable { ) } - /// 요청 전송 및 호출 context 결정 응답 type decoding + /// 요청 전송 및 호출 문맥이 결정한 응답 타입 디코딩 /// - /// - Returns: `Response` decoding 응답 값 + /// - Returns: 디코딩된 `Response` 값 public func send() async throws -> Response { try await send(as: Response.self) } - /// 요청 전송 및 지정 응답 type decoding + /// 요청 전송 및 지정한 응답 타입 디코딩 /// - /// - Parameter type: 성공 응답 decoding 대상 type - /// - Returns: `Response` decoding 응답 값 + /// - Parameter type: 성공 응답 디코딩 대상 타입 + /// - Returns: 디코딩된 `Response` 값 public func send(as type: Response.Type) async throws -> Response { try await decoded(type) } - /// 응답 decoding용 type 지정 builder 변환 + /// 응답 디코딩용 타입 지정 빌더로 변환 /// - /// - Parameter type: 성공 응답 decoding 대상 type - /// - Returns: `Response` type 지정 request builder + /// - Parameter type: 성공 응답 디코딩 대상 타입 + /// - Returns: `Response` 타입을 지정한 요청 빌더 @available(*, deprecated, message: "Use send(as:) or a contextual send().") public func `as`(_ type: Response.Type) -> NXTypedRequestBuilder { NXTypedRequestBuilder(requestBuilder: self) diff --git a/Sources/Public/NXTypedRequestBuilder.swift b/Sources/Public/NXTypedRequestBuilder.swift index e46f461..759aee3 100644 --- a/Sources/Public/NXTypedRequestBuilder.swift +++ b/Sources/Public/NXTypedRequestBuilder.swift @@ -7,11 +7,11 @@ import Foundation -/// 특정 응답 type decoding 요청의 값 의미론 builder +/// 특정 응답 타입을 디코딩하는 값 의미론 요청 빌더 /// /// ## 개요 /// -/// `NXTypedRequestBuilder`는 `NXEndpoint` 구성 compatibility 목적 유지. `NXRawResponse` 실행 API 미노출. method 기반 요청은 `NXRequestBuilder.send(as:)` 사용 +/// `NXTypedRequestBuilder`는 `NXEndpoint` 구성 호환성을 위해 유지. `NXRawResponse` 실행 API는 노출하지 않으며 메서드 기반 요청은 `NXRequestBuilder.send(as:)` 사용 /// /// ```swift /// import Foundation @@ -36,40 +36,40 @@ public struct NXTypedRequestBuilder: Sendable where Response: Decodabl /// - Parameters: /// - key: 쿼리 항목 이름 /// - value: `String(describing:)`로 변환한 쿼리 항목 값 - /// - Returns: 갱신된 type 지정 request builder + /// - Returns: 갱신된 타입 지정 요청 빌더 public func query(_ key: String, _ value: some CustomStringConvertible) -> Self { Self(requestBuilder: requestBuilder.query(key, value)) } - /// 단일 HTTP header 설정/교체 + /// 단일 HTTP 헤더 설정 또는 교체 /// /// - Parameters: - /// - key: header field 이름 - /// - value: header field 값 - /// - Returns: 갱신된 type 지정 request builder + /// - key: 헤더 필드 이름 + /// - value: 헤더 필드 값 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func header(_ key: String, _ value: String) -> Self { Self(requestBuilder: requestBuilder.header(key, value)) } - /// 다수 HTTP header 병합 + /// 여러 HTTP 헤더 병합 /// - /// - Parameter values: 추가 header field 이름/값. 키 충돌 시 새 값 우선 - /// - Returns: 갱신된 type 지정 request builder + /// - Parameter values: 추가할 헤더 필드 이름과 값. 키 충돌 시 새 값 우선 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func headers(_ values: [String: String]) -> Self { Self(requestBuilder: requestBuilder.headers(values)) } - /// `Accept` header 설정 + /// `Accept` 헤더 설정 /// - /// - Parameter value: `Accept` header media type 값 - /// - Returns: 갱신된 type 지정 request builder + /// - Parameter value: `Accept` 헤더의 미디어 타입 값 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func accept(_ value: String) -> Self { Self(requestBuilder: requestBuilder.accept(value)) } /// 설정된 `NXAuthTokenProvider`를 통한 인증 필요 표시 /// - /// - Returns: 갱신된 type 지정 request builder + /// - Returns: 갱신된 타입 지정 요청 빌더 public func authorized() -> Self { Self(requestBuilder: requestBuilder.authorized()) } @@ -77,47 +77,47 @@ public struct NXTypedRequestBuilder: Sendable where Response: Decodabl /// 요청별 타임아웃 간격 설정 /// /// - Parameter seconds: 타임아웃 간격(초). 음수 값은 `0` 보정 - /// - Returns: 갱신된 type 지정 request builder + /// - Returns: 갱신된 타입 지정 요청 빌더 public func timeout(_ seconds: TimeInterval) -> Self { Self(requestBuilder: requestBuilder.timeout(seconds)) } - /// `Encodable` 값 JSON encoding 및 `Content-Type` header 설정 + /// `Encodable` 값을 JSON으로 인코딩하고 `Content-Type` 헤더 설정 /// /// - Parameters: - /// - value: 요청 body encoding 대상 값 - /// - encoder: 사용 encoder. 미지정 시 client 설정 encoder 사용 - /// - Returns: 갱신된 type 지정 request builder + /// - value: 요청 본문 인코딩 대상 값 + /// - encoder: 사용할 인코더. 지정하지 않으면 클라이언트 설정의 인코더 사용 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func json(_ value: T, encoder: JSONEncoder? = nil) throws -> Self { try Self(requestBuilder: requestBuilder.json(value, encoder: encoder)) } - /// 원시 요청 body data 설정 + /// 원시 요청 본문 데이터 설정 /// - /// - Parameter data: HTTP body 입력 data - /// - Returns: 갱신된 type 지정 request builder + /// - Parameter data: HTTP 본문 입력 데이터 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func body(_ data: Data) -> Self { Self(requestBuilder: requestBuilder.body(data)) } - /// `Content-Type` header 설정/교체 + /// `Content-Type` 헤더 설정 또는 교체 /// - /// - Parameter value: `Content-Type` header media type 값 - /// - Returns: 갱신된 type 지정 request builder + /// - Parameter value: `Content-Type` 헤더의 미디어 타입 값 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func contentType(_ value: String) -> Self { Self(requestBuilder: requestBuilder.contentType(value)) } - /// 요청 retry policy 적용 + /// 요청 재시도 정책 적용 /// /// - Parameters: /// - maxAttempts: 초기 요청 포함 최대 시도 횟수(기본 `3`) - /// - backoff: retry 간 지연 전략 - /// - retryableStatusCodes: retry 유발 상태 코드 집합 + /// - backoff: 재시도 간 지연 전략 + /// - retryableStatusCodes: 재시도를 유발하는 상태 코드 집합 /// - allowing: 기본 멱등 메서드와 함께 허용할 추가 HTTP 메서드 - /// - maximumServerDelay: 서버 retry 지연 상한 + /// - maximumServerDelay: 서버 재시도 지연 상한 /// - jitter: 로컬 backoff 지연 무작위화 설정 - /// - Returns: 갱신된 type 지정 request builder + /// - Returns: 갱신된 타입 지정 요청 빌더 public func retry( maxAttempts: Int = 3, backoff: NXRetryBackoff = .fixed(0), @@ -136,33 +136,33 @@ public struct NXTypedRequestBuilder: Sendable where Response: Decodabl )) } - /// 요청 응답 validation policy 적용 + /// 요청 응답 유효성 검사 정책 적용 /// - /// - Parameter policy: transport 응답 반환 후 적용 validation 규칙 - /// - Returns: 갱신된 type 지정 request builder + /// - Parameter policy: 전송 응답 반환 후 적용할 유효성 검사 규칙 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func validate(_ policy: NXValidationPolicy) -> Self { Self(requestBuilder: requestBuilder.validate(policy)) } - /// 요청 단위 interceptor 추가 + /// 요청 단위 인터셉터 추가 /// - /// - Parameter interceptor: 전역 interceptor 뒤에 추가할 interceptor - /// - Returns: 갱신된 type 지정 request builder + /// - Parameter interceptor: 전역 인터셉터 뒤에 추가할 인터셉터 + /// - Returns: 갱신된 타입 지정 요청 빌더 public func intercept(_ interceptor: any NXHTTPInterceptor) -> Self { Self(requestBuilder: requestBuilder.intercept(interceptor)) } /// 최종 `URLRequest` 조립(전송 제외) /// - /// - Returns: 완전 준비된 URLRequest + /// - Returns: 완전히 준비된 URLRequest public func preparedURLRequest() async throws -> URLRequest { try await requestBuilder.preparedURLRequest() } - /// 요청 전송 및 `Response` 응답 decoding + /// 요청 전송 및 `Response` 타입으로 응답 디코딩 /// - /// - Returns: 응답 decoding 결과 - /// - Throws: 요청 실패/decoding 실패 시 `NXError` 발생 + /// - Returns: 디코딩된 응답 + /// - Throws: 요청 또는 디코딩 실패 시 `NXError` 발생 public func send() async throws -> Response { try await requestBuilder.send(as: Response.self) } diff --git a/Sources/Runtime/NXURLSessionTransport.swift b/Sources/Runtime/NXURLSessionTransport.swift index 562e890..88a9d8e 100644 --- a/Sources/Runtime/NXURLSessionTransport.swift +++ b/Sources/Runtime/NXURLSessionTransport.swift @@ -7,12 +7,12 @@ import Foundation -/// `URLSession` 기반 기본 HTTP transport +/// `URLSession` 기반 기본 HTTP 전송 public struct NXURLSessionTransport: NXHTTPTransport, Sendable { let urlSession: URLSession let metricsObserver: (any NXNetworkMetricsObserver)? - /// `URLSession` transport만 수행하는 metrics 비수집 transport 생성 + /// `URLSession` 전송만 수행하고 측정값을 수집하지 않는 전송 생성 /// /// - Parameter urlSession: 요청 실행 세션 public init(urlSession: URLSession = .shared) { @@ -20,10 +20,10 @@ public struct NXURLSessionTransport: NXHTTPTransport, Sendable { metricsObserver = nil } - /// `URLSession` transport 및 metrics snapshot 기록 지원 transport 생성 + /// `URLSession` 전송과 측정값 스냅샷 기록을 지원하는 전송 생성 /// /// - Parameter urlSession: 요청 실행 세션 - /// - Parameter metricsObserver: `URLSession` metrics snapshot 수신 observer + /// - Parameter metricsObserver: `URLSession` 측정값 스냅샷을 받는 관측자 public init( urlSession: URLSession = .shared, metricsObserver: any NXNetworkMetricsObserver @@ -32,10 +32,10 @@ public struct NXURLSessionTransport: NXHTTPTransport, Sendable { self.metricsObserver = metricsObserver } - /// 준비된 요청 내부 `URLSession` transport + /// 준비된 요청을 내부 `URLSession`으로 전송 /// /// - Parameter request: 실행 준비 요청 - /// - Returns: `NXRawResponse`의 응답 data와 HTTP metadata + /// - Returns: `NXRawResponse`의 응답 데이터와 HTTP 메타데이터 public func send(_ request: URLRequest) async throws -> NXRawResponse { let metricsDelegate = metricsObserver.map(NXURLSessionTaskMetricsDelegate.init) let (data, response) = try await urlSession.data(for: request, delegate: metricsDelegate) From c9e60f788c09a7abc4b6aff92be8a1d0d0792ed4 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:06:53 +0900 Subject: [PATCH 09/12] =?UTF-8?q?docs:=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B3=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 38 ++++++++++++++++++++++++++------------ README.md | 38 ++++++++++++++++++++++++++------------ 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/README.en.md b/README.en.md index f6b3564..999abd1 100644 --- a/README.en.md +++ b/README.en.md @@ -188,6 +188,8 @@ flowchart TB client builder[NXRequestBuilder
NXTypedRequestBuilder] endpoint[NXEndpoint] + extensionPoints[Public Extension Contracts
NXHTTPTransport, NXHTTPInterceptor,
NXAuthTokenProvider, NXLogger, NXServerErrorDecoder] + client --> builder client --> endpoint endpoint --> builder @@ -196,30 +198,42 @@ flowchart TB subgraph core[Core Model] configuration[NXClientConfiguration] requestSpec[RequestSpec] - extensionPoints[Extension Points
transport, interceptor, auth, logger, error decoder] end client --> configuration builder --> requestSpec subgraph runtime[Execution Layer] - assembler[NXRequestAssembler] + sharedState[Client-Owned Shared State
Optional Response Cache Store
Auth Refresh Coordinator] executor[NXRequestExecutor] - interceptors[NXInterceptorChain
retry, auth, logging, cache] - transport[NXHTTPTransport] - pipeline[NXResponsePipeline] + assembler[NXRequestAssembler] + interceptors[NXInterceptorChain
Retry → Auth → Client Interceptors → Request Interceptors → Logging → Optional Response Cache] + configuredTransport[Configured Transport Implementation] + validation[NXResponsePipeline
Response Validation] + decoding[NXResponsePipeline
Optional Response Decoding] end - builder --> assembler --> executor + client -->|owns| sharedState + sharedState -.->|passed to execution via builder| executor + + builder --> executor configuration --> executor requestSpec --> executor - executor --> interceptors --> transport --> pipeline - pipeline --> rawResponse[Raw Response
NXRawResponse] - pipeline --> decodedResponse[Decoded Response
Response] - extensionPoints -.-> interceptors - extensionPoints -.-> transport - extensionPoints -.-> pipeline + executor -->|assembles request| assembler + assembler -->|assembled URLRequest| interceptors + + interceptors -->|transport needed| configuredTransport + configuredTransport --> validation + interceptors -->|response without transport| validation + + validation -->|send()| rawResponse[Raw Response
NXRawResponse] + validation -->|send(as:)| decoding + decoding --> decodedResponse[Decoded Response
Response] + + extensionPoints -.->|interceptor, auth, logging| interceptors + extensionPoints -.->|transport| configuredTransport + extensionPoints -.->|error decoder| validation ``` ## Quick Start diff --git a/README.md b/README.md index 616c874..0bcbd0e 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ flowchart TB client builder[NXRequestBuilder
NXTypedRequestBuilder] endpoint[NXEndpoint] + extensionPoints[공개 확장 계약
NXHTTPTransport, NXHTTPInterceptor,
NXAuthTokenProvider, NXLogger, NXServerErrorDecoder] + client --> builder client --> endpoint endpoint --> builder @@ -196,30 +198,42 @@ flowchart TB subgraph core[핵심 모델] configuration[NXClientConfiguration] requestSpec[RequestSpec] - extensionPoints[확장 지점
전송, 인터셉터, 인증, 로깅, 오류 디코더] end client --> configuration builder --> requestSpec subgraph runtime[실행 계층] - assembler[NXRequestAssembler] + sharedState[클라이언트 공유 상태
선택적 응답 캐시 저장소
인증 갱신 조정자] executor[NXRequestExecutor] - interceptors[NXInterceptorChain
재시도, 인증, 로깅, 응답 캐시] - transport[NXHTTPTransport] - pipeline[NXResponsePipeline] + assembler[NXRequestAssembler] + interceptors[NXInterceptorChain
재시도 → 인증 → 클라이언트 인터셉터 → 요청 인터셉터 → 로깅 → 선택적 응답 캐시] + configuredTransport[설정된 전송 구현] + validation[NXResponsePipeline
응답 검증] + decoding[NXResponsePipeline
선택적 응답 디코딩] end - builder --> assembler --> executor + client -->|소유| sharedState + sharedState -.->|빌더를 거쳐 실행에 전달| executor + + builder --> executor configuration --> executor requestSpec --> executor - executor --> interceptors --> transport --> pipeline - pipeline --> rawResponse[원시 응답
NXRawResponse] - pipeline --> decodedResponse[디코딩된 응답
Response] - extensionPoints -.-> interceptors - extensionPoints -.-> transport - extensionPoints -.-> pipeline + executor -->|요청 조립| assembler + assembler -->|조립한 URLRequest| interceptors + + interceptors -->|전송 필요| configuredTransport + configuredTransport --> validation + interceptors -->|전송 없이 응답| validation + + validation -->|send()| rawResponse[원시 응답
NXRawResponse] + validation -->|send(as:)| decoding + decoding --> decodedResponse[디코딩된 응답
Response] + + extensionPoints -.->|인터셉터, 인증, 로깅| interceptors + extensionPoints -.->|전송| configuredTransport + extensionPoints -.->|오류 디코더| validation ``` ## 빠른 시작 From f73409093c0aa663ab397da47efbeccd50a7caf1 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:09:26 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B3=90=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.en.md b/README.en.md index 999abd1..6826c94 100644 --- a/README.en.md +++ b/README.en.md @@ -227,8 +227,8 @@ flowchart TB configuredTransport --> validation interceptors -->|response without transport| validation - validation -->|send()| rawResponse[Raw Response
NXRawResponse] - validation -->|send(as:)| decoding + validation -->|raw response| rawResponse[Raw Response
NXRawResponse] + validation -->|response decoding| decoding decoding --> decodedResponse[Decoded Response
Response] extensionPoints -.->|interceptor, auth, logging| interceptors diff --git a/README.md b/README.md index 0bcbd0e..66edb06 100644 --- a/README.md +++ b/README.md @@ -227,8 +227,8 @@ flowchart TB configuredTransport --> validation interceptors -->|전송 없이 응답| validation - validation -->|send()| rawResponse[원시 응답
NXRawResponse] - validation -->|send(as:)| decoding + validation -->|원시 응답 반환| rawResponse[원시 응답
NXRawResponse] + validation -->|응답 디코딩| decoding decoding --> decodedResponse[디코딩된 응답
Response] extensionPoints -.->|인터셉터, 인증, 로깅| interceptors From 447dc44f6c4c037dd9f829c4977656ebc18972af Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:02:05 +0900 Subject: [PATCH 11/12] =?UTF-8?q?docs:=20README=20=ED=83=90=EC=83=89?= =?UTF-8?q?=EC=84=B1=EA=B3=BC=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=20=EA=B0=80=EB=8F=85=EC=84=B1=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 7 +++++-- README.md | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.en.md b/README.en.md index 6826c94..b061daf 100644 --- a/README.en.md +++ b/README.en.md @@ -15,6 +15,7 @@ Nexa preserves HTTP semantics by defining explicit boundaries for sharing cached - [Requirements](#requirements) - [Installation](#installation) - [Public API](#public-api) +- [Request Flow](#request-flow) - [Quick Start](#quick-start) - [Endpoint API](#endpoint-api) - [Request Migration](#request-migration) @@ -25,6 +26,8 @@ Nexa preserves HTTP semantics by defining explicit boundaries for sharing cached - [Authentication Refresh](#authentication-refresh) - [Response Cache](#response-cache) - [Retry Policy](#retry-policy) +- [Nexa 1.3 Migration](#nexa-13-migration) +- [Interceptor Method Contract](#interceptor-method-contract) - [Development](#development) - [Testing](#testing) @@ -83,7 +86,7 @@ The rest of the public surface is made of extension points for auth, logging, te | --- | --- | --- | | `NXAPIClient` | Main entry point for requests that share one `baseURL` and one configuration | `client.get("/users").send(as: User.self)` | | `NXRequestBuilder` | When you need a prepared `URLRequest`, `NXRawResponse`, or a decoded `Decodable` response | `try await client.get("/users").send()` | -| `NXTypedRequestBuilder` | Endpoint configuration compatibility boundary | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | +| `NXTypedRequestBuilder` | When maintaining `NXEndpoint` configuration compatibility | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | | `NXEndpoint` | When an endpoint definition should be reusable and carry its response type with it | `try await client.send(UserEndpoint(identifier: 1))` | | `NXClientConfiguration` | When shared headers, transport, logger, auth, encoder, decoder, or interceptors should be configured once | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | | `NXCache` | When successful unauthenticated `GET` responses should be reused for a TTL or revalidated with validators | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | @@ -207,7 +210,7 @@ flowchart TB sharedState[Client-Owned Shared State
Optional Response Cache Store
Auth Refresh Coordinator] executor[NXRequestExecutor] assembler[NXRequestAssembler] - interceptors[NXInterceptorChain
Retry → Auth → Client Interceptors → Request Interceptors → Logging → Optional Response Cache] + interceptors[NXInterceptorChain
Retry → Auth → Client Interceptors
Request Interceptors → Logging → Optional Response Cache] configuredTransport[Configured Transport Implementation] validation[NXResponsePipeline
Response Validation] decoding[NXResponsePipeline
Optional Response Decoding] diff --git a/README.md b/README.md index 66edb06..0052047 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Nexa는 HTTP 의미를 보존하기 위해 캐시 응답과 진행 중 작업의 - [요구 사항](#요구-사항) - [설치](#설치) - [공개 API](#공개-api) +- [요청 흐름](#요청-흐름) - [빠른 시작](#빠른-시작) - [Endpoint API](#endpoint-api) - [요청 전환](#요청-전환) @@ -25,6 +26,8 @@ Nexa는 HTTP 의미를 보존하기 위해 캐시 응답과 진행 중 작업의 - [인증 토큰 갱신](#인증-토큰-갱신) - [응답 캐시](#응답-캐시) - [재시도 정책](#재시도-정책) +- [Nexa 1.3 전환](#nexa-13-전환) +- [Interceptor 메서드 계약](#interceptor-메서드-계약) - [개발](#개발) - [테스트](#테스트) @@ -83,7 +86,7 @@ dependencies: [ | --- | --- | --- | | `NXAPIClient` | 동일한 `baseURL`과 설정을 공유하는 요청의 주 진입점 | `client.get("/users").send(as: User.self)` | | `NXRequestBuilder` | `URLRequest`, `NXRawResponse`, 또는 디코딩된 `Decodable` 응답이 필요할 때 | `try await client.get("/users").send()` | -| `NXTypedRequestBuilder` | 엔드포인트 설정 호환성 경계 | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | +| `NXTypedRequestBuilder` | `NXEndpoint` 구성 호환성을 유지할 때 | `func configure(_ builder: NXTypedRequestBuilder) -> NXTypedRequestBuilder` | | `NXEndpoint` | 엔드포인트 정의를 재사용하고 응답 타입을 함께 관리할 때 | `try await client.send(UserEndpoint(identifier: 1))` | | `NXClientConfiguration` | 공통 헤더, 전송, 로거, 인증, 인코더, 디코더, 인터셉터를 한 번에 설정할 때 | `NXClientConfiguration(baseURL: url, authTokenProvider: yourAuthTokenProvider)` | | `NXCache` | 인증이 필요 없는 성공한 `GET` 응답을 TTL 동안 재사용하거나 식별값을 사용해 재검증할 때 | `NXClientConfiguration(baseURL: url, cache: .revalidatingMemory(ttl: 300))` | @@ -207,7 +210,7 @@ flowchart TB sharedState[클라이언트 공유 상태
선택적 응답 캐시 저장소
인증 갱신 조정자] executor[NXRequestExecutor] assembler[NXRequestAssembler] - interceptors[NXInterceptorChain
재시도 → 인증 → 클라이언트 인터셉터 → 요청 인터셉터 → 로깅 → 선택적 응답 캐시] + interceptors[NXInterceptorChain
재시도 → 인증 → 클라이언트 인터셉터
요청 인터셉터 → 로깅 → 선택적 응답 캐시] configuredTransport[설정된 전송 구현] validation[NXResponsePipeline
응답 검증] decoding[NXResponsePipeline
선택적 응답 디코딩] From a88ab9c3929d74a85e533cd0c3dd366ba12e4e88 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:21:44 +0900 Subject: [PATCH 12/12] =?UTF-8?q?docs:=20requestIdentifier=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EA=B3=84=EC=95=BD=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 4 ++-- README.md | 4 ++-- Sources/Core/NXLogging.swift | 8 ++++---- Sources/Public/NXHTTPInterceptor.swift | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.en.md b/README.en.md index b061daf..564b8fd 100644 --- a/README.en.md +++ b/README.en.md @@ -384,7 +384,7 @@ Nexa currently supports: ## Structured Logging -`NXLogger` connects one logical request through `requestIdentifier` and reports the retry-policy attempt through `attemptNumber`. Attempt numbers start at 1 and do not increase when a request is replayed after a Bearer token refresh. +`NXLogger` records the `requestIdentifier` assigned when a request builder is created. Copies of the same builder and repeated `send()` calls keep the same identifier, as do retry attempts and replays after a Bearer token refresh. `attemptNumber` starts at 1 and increases only for retry-policy attempts; it does not increase for a replay after a Bearer token refresh. | Event | When emitted | Main values | | --- | --- | --- | @@ -502,7 +502,7 @@ The public `NXRetryPolicy` constructor, `NXRetryPolicy.Backoff`, `NXRetryPolicy. `NXHTTPInterceptor.replacingRequest(_:)` can change a request URL, headers, and body, but the request method must remain equal to the configured method. A different method ends the chain with `NXError.invalidRequest` before later interceptors, logging, caching, or transport. -`NXRequestExecutionContext.requestIdentifier` remains stable for one logical request and its retry attempts. `attemptNumber` starts at 1 and represents the retry-policy attempt; it does not increase when a request is replayed after a Bearer token refresh. +`NXRequestExecutionContext.requestIdentifier` is assigned when a request builder is created and remains stable across copies of that builder, repeated `send()` calls, retry attempts, and replays after a Bearer token refresh. `attemptNumber` starts at 1 and increases only for retry-policy attempts; it does not increase for a replay after a Bearer token refresh. ## Development diff --git a/README.md b/README.md index 0052047..780ea17 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,7 @@ let client = NXAPIClient(configuration: configuration) ## 구조화된 로깅 -`NXLogger`는 하나의 논리 요청을 `requestIdentifier`로 연결하고 재시도 정책의 시도 번호를 `attemptNumber`로 전달합니다. `attemptNumber`는 1부터 시작하며 Bearer 토큰 갱신 뒤의 요청 재전송에서는 증가하지 않습니다. +`NXLogger`는 요청 빌더 생성 시 부여된 `requestIdentifier`를 기록합니다. 같은 빌더의 복사본과 반복 `send()` 호출은 같은 식별자를 유지하며, 재시도와 Bearer 토큰 갱신 뒤 재전송에서도 유지됩니다. `attemptNumber`는 1부터 시작해 재시도 정책에 따른 시도에서만 증가하며 Bearer 토큰 갱신 뒤 재전송에서는 증가하지 않습니다. | 이벤트 | 발생 시점 | 주요 값 | | --- | --- | --- | @@ -502,7 +502,7 @@ Nexa 1.3에서는 공개 `NXRetryPolicy` 생성자, `NXRetryPolicy.Backoff`, `NX `NXHTTPInterceptor.replacingRequest(_:)`는 요청 URL, 헤더, 본문을 바꿀 수 있지만 메서드는 설정한 메서드와 같아야 합니다. 다른 메서드는 이후 인터셉터, 로거, 캐시, 전송이 실행되기 전에 `NXError.invalidRequest`로 종료됩니다. -`NXRequestExecutionContext.requestIdentifier`는 하나의 논리 요청과 해당 재시도에서 유지됩니다. `attemptNumber`는 1부터 시작하는 재시도 정책의 시도 번호이며 Bearer 토큰 갱신 뒤의 재전송에서는 증가하지 않습니다. +`NXRequestExecutionContext.requestIdentifier`는 요청 빌더 생성 시 부여되며 같은 빌더의 복사본과 반복 `send()` 호출, 재시도, Bearer 토큰 갱신 뒤 재전송에서 유지됩니다. `attemptNumber`는 1부터 시작해 재시도 정책에 따른 시도에서만 증가하며 Bearer 토큰 갱신 뒤 재전송에서는 증가하지 않습니다. ## 개발 diff --git a/Sources/Core/NXLogging.swift b/Sources/Core/NXLogging.swift index e8c5b76..4ea635b 100644 --- a/Sources/Core/NXLogging.swift +++ b/Sources/Core/NXLogging.swift @@ -18,7 +18,7 @@ public enum NXLogEvent: Sendable { /// 요청 시도 시작 시점의 구조화된 페이로드 public struct NXRequestStartLog: Sendable { - /// 동일한 논리 요청 시도 간 공유 안정적 식별자 + /// 요청 빌더 생성 시 부여되며 같은 빌더의 복사본과 반복 `send()`, 재시도, Bearer 토큰 갱신 뒤 재전송에서 유지되는 식별자 public let requestIdentifier: UUID /// 현재 시도 번호(`1`부터 시작) public let attemptNumber: Int @@ -47,7 +47,7 @@ public struct NXRequestStartLog: Sendable { /// 요청 성공 종료 시점의 구조화된 페이로드 public struct NXRequestEndLog: Sendable { - /// 동일한 논리 요청 시도 간 공유 안정적 식별자 + /// 요청 빌더 생성 시 부여되며 같은 빌더의 복사본과 반복 `send()`, 재시도, Bearer 토큰 갱신 뒤 재전송에서 유지되는 식별자 public let requestIdentifier: UUID /// 현재 시도 번호(`1`부터 시작) public let attemptNumber: Int @@ -76,7 +76,7 @@ public struct NXRequestEndLog: Sendable { /// 요청 실패 시점의 구조화된 페이로드 public struct NXRequestFailureLog: Sendable { - /// 동일한 논리 요청 시도 간 공유 안정적 식별자 + /// 요청 빌더 생성 시 부여되며 같은 빌더의 복사본과 반복 `send()`, 재시도, Bearer 토큰 갱신 뒤 재전송에서 유지되는 식별자 public let requestIdentifier: UUID /// 현재 시도 번호(`1`부터 시작) public let attemptNumber: Int @@ -101,7 +101,7 @@ public struct NXRequestFailureLog: Sendable { /// Nexa가 다음 재시도를 예약할 때 출력하는 구조화된 페이로드 public struct NXRetryLog: Sendable { - /// 동일한 논리 요청 시도 간 공유 안정적 식별자 + /// 요청 빌더 생성 시 부여되며 같은 빌더의 복사본과 반복 `send()`, 재시도, Bearer 토큰 갱신 뒤 재전송에서 유지되는 식별자 public let requestIdentifier: UUID /// 다음 실행 시도 번호 public let nextAttemptNumber: Int diff --git a/Sources/Public/NXHTTPInterceptor.swift b/Sources/Public/NXHTTPInterceptor.swift index b5e6b7d..1e99892 100644 --- a/Sources/Public/NXHTTPInterceptor.swift +++ b/Sources/Public/NXHTTPInterceptor.swift @@ -48,7 +48,7 @@ public protocol NXHTTPInterceptor: Sendable { public struct NXRequestExecutionContext: Sendable { /// 현재 실행 중인 요청 public let request: URLRequest - /// 동일 논리 요청 전체 시도에서 공유되는 안정적 식별자 + /// 요청 빌더 생성 시 부여되며 같은 빌더의 복사본과 반복 `send()`, 재시도, Bearer 토큰 갱신 뒤 재전송에서 유지되는 식별자 public let requestIdentifier: UUID /// 현재 시도 번호(`1`부터 시작) public let attemptNumber: Int