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
+
+[](https://github.com/opficdev/Nexa/actions/workflows/build.yml)
+[](https://www.swift.org)
+[](https://github.com/opficdev/Nexa)
+[](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
-
-[](https://github.com/opficdev/Nexa/actions/workflows/build.yml)
-[](https://www.swift.org)
-[](https://github.com/opficdev/Nexa)
-[](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 @@
[](https://github.com/opficdev/Nexa)
[](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