runtypex 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/cjs/generator/generate-jsdoc.js +13 -7
- package/dist/esm/generator/generate-jsdoc.js +13 -7
- package/docs/build-integrations.md +4 -0
- package/docs/jsdoc-generation.md +58 -7
- package/docs/ko/build-integrations.md +186 -0
- package/docs/ko/jsdoc-generation.md +276 -0
- package/docs/ko/mapper.md +343 -0
- package/docs/ko/mapping-policy.md +192 -0
- package/docs/ko/runtime-validation.md +292 -0
- package/docs/mapper.md +87 -0
- package/docs/runtime-validation.md +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# 매퍼
|
|
4
|
+
|
|
5
|
+
`runtypex`의 매퍼는 타입이 지정된 매핑 명세를 사용해 DTO 객체를 도메인 객체로 변환합니다.
|
|
6
|
+
|
|
7
|
+
API 응답, 데이터베이스 레코드, 외부 시스템의 필드 이름이 애플리케이션 내부에서 사용하는 도메인 모델과 다를 때 유용합니다.
|
|
8
|
+
|
|
9
|
+
예를 들어 DTO에서는 `user_id`라는 필드를 사용하지만, 애플리케이션 내부에서는 `id`라는 이름을 사용하고 싶을 수 있습니다. 이런 경우 매퍼를 사용하면 필드 이름 변환, 중첩 필드 접근, 값 변환을 하나의 명세로 관리할 수 있습니다.
|
|
10
|
+
|
|
11
|
+
<br/>
|
|
12
|
+
|
|
13
|
+
## 기본 예제
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineMap, makeMapper, source, transform } from "runtypex/mapper";
|
|
17
|
+
|
|
18
|
+
interface UserDto {
|
|
19
|
+
user_id: string;
|
|
20
|
+
profile: { name: string };
|
|
21
|
+
status: "ACTIVE" | "INACTIVE";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface User {
|
|
25
|
+
id: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
isActive: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const userMap = defineMap<UserDto, User>()({
|
|
31
|
+
id: source("user_id"),
|
|
32
|
+
displayName: source("profile.name"),
|
|
33
|
+
isActive: transform("status", (value) => value === "ACTIVE"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const toUser = makeMapper<UserDto, User>(userMap);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
위 매퍼는 `UserDto`를 받아 다음과 같은 `User` 객체로 변환합니다.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const user = toUser({
|
|
43
|
+
user_id: "u1",
|
|
44
|
+
profile: { name: "Lux" },
|
|
45
|
+
status: "ACTIVE",
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
결과는 다음과 같습니다.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
{
|
|
53
|
+
id: "u1",
|
|
54
|
+
displayName: "Lux",
|
|
55
|
+
isActive: true
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
<br/>
|
|
60
|
+
|
|
61
|
+
## 매핑 명세 작성하기
|
|
62
|
+
|
|
63
|
+
매핑 명세는 `defineMap<TDto, TDomain>()`으로 정의합니다.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const userMap = defineMap<UserDto, User>()({
|
|
67
|
+
id: source("user_id"),
|
|
68
|
+
displayName: source("profile.name"),
|
|
69
|
+
isActive: transform("status", (value) => value === "ACTIVE"),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
각 도메인 필드는 다음 방식으로 매핑할 수 있습니다.
|
|
74
|
+
|
|
75
|
+
| 헬퍼 | 용도 |
|
|
76
|
+
| --------------------------- | --------------------------------- |
|
|
77
|
+
| `source(path)` | DTO의 특정 경로에서 값을 그대로 가져옵니다. |
|
|
78
|
+
| `transform(path, callback)` | DTO의 특정 경로에서 값을 가져온 뒤 콜백으로 변환합니다. |
|
|
79
|
+
|
|
80
|
+
예를 들어:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
id: source("user_id")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
는 DTO의 `user_id` 값을 도메인 객체의 `id` 필드에 넣습니다.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
displayName: source("profile.name")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
는 DTO의 중첩 필드인 `profile.name` 값을 도메인 객체의 `displayName` 필드에 넣습니다.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
isActive: transform("status", (value) => value === "ACTIVE")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
는 DTO의 `status` 값을 읽은 뒤, 도메인 객체에서 사용할 `boolean` 값으로 변환합니다.
|
|
99
|
+
|
|
100
|
+
<br/>
|
|
101
|
+
|
|
102
|
+
## 타입 레벨 보장
|
|
103
|
+
|
|
104
|
+
`defineMap<TDto, TDomain>()`은 컴파일 시점에 매핑 명세를 검사합니다.
|
|
105
|
+
|
|
106
|
+
다음 조건이 타입 레벨에서 확인됩니다.
|
|
107
|
+
|
|
108
|
+
* 모든 도메인 필드가 매핑 명세에 포함되어야 합니다.
|
|
109
|
+
* `source()` 또는 `transform()`에 전달한 경로는 DTO 타입에 존재해야 합니다.
|
|
110
|
+
* `transform()` 콜백은 최종 도메인 필드에 할당 가능한 값을 반환해야 합니다.
|
|
111
|
+
|
|
112
|
+
덕분에 DTO 구조가 바뀌었을 때 런타임에서 조용히 실패하는 대신, 컴파일 시점에 문제를 발견할 수 있습니다.
|
|
113
|
+
|
|
114
|
+
예를 들어 DTO에서 `user_id`가 제거되었는데 매퍼가 여전히 다음과 같이 작성되어 있다면:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
id: source("user_id")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
TypeScript가 매핑 경로 오류를 감지할 수 있습니다.
|
|
121
|
+
|
|
122
|
+
<br/>
|
|
123
|
+
|
|
124
|
+
## 런타임 동작
|
|
125
|
+
|
|
126
|
+
트랜스포머를 사용하지 않아도 `makeMapper()`는 런타임에서 매핑 명세를 해석해 동작합니다.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const toUser = makeMapper<UserDto, User>(userMap);
|
|
130
|
+
|
|
131
|
+
const user = toUser({
|
|
132
|
+
user_id: "u1",
|
|
133
|
+
profile: { name: "Lux" },
|
|
134
|
+
status: "ACTIVE",
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
런타임에서 매퍼는 각 도메인 필드에 대해 다음 순서로 동작합니다.
|
|
139
|
+
|
|
140
|
+
1. 매핑 명세에 설정된 DTO 경로에서 값을 읽습니다.
|
|
141
|
+
2. 소스 값이 `undefined`이고 `default` 값이 있으면 `default`를 적용합니다.
|
|
142
|
+
3. `transform` 콜백이 있으면 값을 변환합니다.
|
|
143
|
+
4. 결과를 도메인 출력 객체에 기록합니다.
|
|
144
|
+
|
|
145
|
+
즉, 위 예제에서는 다음과 같은 작업이 일어납니다.
|
|
146
|
+
|
|
147
|
+
| 도메인 필드 | DTO 경로 | 처리 방식 |
|
|
148
|
+
| ------------- | -------------- | --------------------------------- |
|
|
149
|
+
| `id` | `user_id` | 값을 그대로 복사합니다. |
|
|
150
|
+
| `displayName` | `profile.name` | 중첩 값을 읽어 복사합니다. |
|
|
151
|
+
| `isActive` | `status` | `"ACTIVE"` 여부를 `boolean`으로 변환합니다. |
|
|
152
|
+
|
|
153
|
+
런타임 폴백은 별도의 빌드 연동 없이도 매퍼 기능을 사용할 수 있게 해줍니다.
|
|
154
|
+
|
|
155
|
+
이 모드에서 `makeMapper()`는 `userMap` 객체를 순회하고, 입력 DTO에서 각 `from` 경로를 읽은 뒤, 필요한 경우 `default`와 `transform`을 적용해 도메인 객체를 반환합니다.
|
|
156
|
+
|
|
157
|
+
새 소스 파일을 만들지 않으며, 생성 코드를 인라인하지도 않습니다.
|
|
158
|
+
|
|
159
|
+
<br/>
|
|
160
|
+
|
|
161
|
+
## 트랜스포머 동작
|
|
162
|
+
|
|
163
|
+
트랜스포머를 활성화하면 다음 호출은:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const toUser = makeMapper<UserDto, User>(userMap);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
빌드 시점에 인라인 매퍼 함수로 대체됩니다.
|
|
170
|
+
|
|
171
|
+
생성된 함수는 설정에 따라 다음 검증을 포함할 수 있습니다.
|
|
172
|
+
|
|
173
|
+
* 매핑 전 DTO 입력 검증
|
|
174
|
+
* 매핑 후 도메인 출력 검증
|
|
175
|
+
|
|
176
|
+
변환된 소스에는 다음과 같은 형태의 코드가 포함됩니다.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const toUser = (function () {
|
|
180
|
+
const S = {
|
|
181
|
+
id: { from: "user_id" },
|
|
182
|
+
displayName: { from: "profile.name" },
|
|
183
|
+
isActive: {
|
|
184
|
+
from: "status",
|
|
185
|
+
transform: (value) => value === "ACTIVE",
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const VD = (input) =>
|
|
190
|
+
typeof input === "object" &&
|
|
191
|
+
input !== null &&
|
|
192
|
+
typeof input.user_id === "string" &&
|
|
193
|
+
typeof input.profile === "object" &&
|
|
194
|
+
input.profile !== null &&
|
|
195
|
+
typeof input.profile.name === "string" &&
|
|
196
|
+
(input.status === "ACTIVE" || input.status === "INACTIVE");
|
|
197
|
+
|
|
198
|
+
const VO = (input) =>
|
|
199
|
+
typeof input === "object" &&
|
|
200
|
+
input !== null &&
|
|
201
|
+
typeof input.id === "string" &&
|
|
202
|
+
typeof input.displayName === "string" &&
|
|
203
|
+
typeof input.isActive === "boolean";
|
|
204
|
+
|
|
205
|
+
return (input) => {
|
|
206
|
+
if (!VD(input)) throw new TypeError("[runtypex] DTO validation failed.");
|
|
207
|
+
|
|
208
|
+
const R = (key, raw) => {
|
|
209
|
+
const rule = S[key];
|
|
210
|
+
const value =
|
|
211
|
+
raw === undefined && Object.prototype.hasOwnProperty.call(rule, "default")
|
|
212
|
+
? rule.default
|
|
213
|
+
: raw;
|
|
214
|
+
|
|
215
|
+
return typeof rule.transform === "function"
|
|
216
|
+
? rule.transform(value, input)
|
|
217
|
+
: value;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const output = {
|
|
221
|
+
id: R("id", input["user_id"]),
|
|
222
|
+
displayName: R("displayName", input["profile"]["name"]),
|
|
223
|
+
isActive: R("isActive", input["status"]),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (!VO(output)) {
|
|
227
|
+
throw new TypeError("[runtypex] Domain validation failed.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return output;
|
|
231
|
+
};
|
|
232
|
+
})();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
트랜스포머는 별도의 매퍼 파일을 만들지 않습니다.
|
|
236
|
+
|
|
237
|
+
대신 `makeMapper<TDto, TDomain>()` 호출이 있던 파일 안에 매퍼 함수를 인라인합니다. 이 방식은 하나의 매핑 선언을 유지하면서도, 빌드 시점에 최적화된 런타임 코드를 얻을 수 있게 해줍니다.
|
|
238
|
+
|
|
239
|
+
<br/>
|
|
240
|
+
|
|
241
|
+
## 프로덕션 빌드에서 검증 제거하기
|
|
242
|
+
|
|
243
|
+
트랜스포머 옵션에서 `removeInProd: true`를 활성화하고 `NODE_ENV`가 `production`이면, 매퍼 자체는 계속 생성되지만 DTO 및 도메인 검증 가드는 제거됩니다.
|
|
244
|
+
|
|
245
|
+
즉, 다음과 같은 검증 코드는 프로덕션 빌드에서 빠질 수 있습니다.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
if (!VD(input)) {
|
|
249
|
+
throw new TypeError("[runtypex] DTO validation failed.");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!VO(output)) {
|
|
253
|
+
throw new TypeError("[runtypex] Domain validation failed.");
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
검증이 제거된 매퍼는 매핑 결과를 바로 반환합니다.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const output = {
|
|
261
|
+
id: R("id", input["user_id"]),
|
|
262
|
+
displayName: R("displayName", input["profile"]["name"]),
|
|
263
|
+
isActive: R("isActive", input["status"]),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return output;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
이 옵션은 개발 및 테스트 환경에서는 검증을 유지하고, 프로덕션 환경에서는 런타임 비용을 줄이고 싶을 때 사용할 수 있습니다.
|
|
270
|
+
|
|
271
|
+
<br/>
|
|
272
|
+
|
|
273
|
+
## 메타데이터
|
|
274
|
+
|
|
275
|
+
매핑 규칙에는 선택적으로 메타데이터를 포함할 수 있습니다.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
id: source("user_id", {
|
|
279
|
+
db: "users.user_id",
|
|
280
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
메타데이터는 매핑 동작에 필수는 아닙니다.
|
|
285
|
+
|
|
286
|
+
주로 JSDoc 생성이나 문서화 도구에서 사용됩니다.
|
|
287
|
+
|
|
288
|
+
| 필드 | 용도 |
|
|
289
|
+
| ---------------- | ------------------------------------- |
|
|
290
|
+
| `db` | 원본 데이터베이스 필드 또는 외부 시스템의 원본 경로를 나타냅니다. |
|
|
291
|
+
| `dtoDescription` | DTO 필드에 대한 설명을 제공합니다. |
|
|
292
|
+
|
|
293
|
+
도메인 필드의 일반적인 의미는 도메인 타입의 JSDoc에 작성하는 것이 좋습니다.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
interface User {
|
|
297
|
+
/** User id */
|
|
298
|
+
id: string;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
반면 매퍼 메타데이터에는 DTO나 데이터베이스처럼 특정 소스에 종속된 정보를 넣는 것이 좋습니다.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
id: source("user_id", {
|
|
306
|
+
db: "users.user_id",
|
|
307
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
이렇게 나누면 도메인 모델의 의미와 외부 데이터 출처 정보를 분리해서 관리할 수 있습니다.
|
|
312
|
+
|
|
313
|
+
<br/>
|
|
314
|
+
|
|
315
|
+
## 타입 안전 헬퍼
|
|
316
|
+
|
|
317
|
+
헬퍼 콜백에서 DTO를 타입에 맞게 다뤄야 한다면 `mapperHelpers<TDto>()`를 사용할 수 있습니다.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { mapperHelpers } from "runtypex/mapper";
|
|
321
|
+
|
|
322
|
+
const h = mapperHelpers<UserDto>();
|
|
323
|
+
|
|
324
|
+
const userMap = defineMap<UserDto, User>()({
|
|
325
|
+
id: h.source("user_id"),
|
|
326
|
+
displayName: h.source("profile.name"),
|
|
327
|
+
isActive: h.transform("status", (value, dto) => {
|
|
328
|
+
return dto.status === "ACTIVE";
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`mapperHelpers<TDto>()`를 사용하면 `source()`와 `transform()`을 DTO 타입에 맞게 사용할 수 있습니다.
|
|
334
|
+
|
|
335
|
+
특히 `transform()` 콜백의 두 번째 인자인 `dto`를 사용할 때 유용합니다.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
isActive: h.transform("status", (value, dto) => {
|
|
339
|
+
return dto.status === "ACTIVE";
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
위 코드에서 `dto`는 `UserDto`로 타입이 지정되므로, 콜백 안에서도 DTO 필드를 타입 안전하게 참조할 수 있습니다.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# 매핑 정책
|
|
4
|
+
|
|
5
|
+
매핑 정책은 DTO 경로와 도메인 필드 이름의 대응 관계를 일관되게 유지하기 위한 기능입니다.
|
|
6
|
+
|
|
7
|
+
같은 DTO 필드가 여러 매퍼에서 사용될 때, 매퍼마다 서로 다른 도메인 이름으로 변환하면 코드베이스의 도메인 언어가 흐트러질 수 있습니다. 매핑 정책을 사용하면 특정 DTO 경로에 대해 표준 도메인 필드 이름을 한 번만 정의하고, 이후 매퍼들이 그 규칙을 따르도록 검사할 수 있습니다.
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
## 문제
|
|
12
|
+
|
|
13
|
+
정책이 없으면 서로 다른 매퍼가 같은 DTO 경로를 서로 다른 이름으로 바꿀 수 있습니다.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const userMap = defineMap<UserDto, User>()({
|
|
17
|
+
userId: source("user_id"),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const auditMap = defineMap<UserDto, AuditUser>()({
|
|
21
|
+
realMemberID: source("user_id"),
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
위 두 매핑은 타입 관점에서는 모두 유효합니다.
|
|
26
|
+
|
|
27
|
+
하지만 같은 DTO 경로인 `user_id`가 한 곳에서는 `userId`, 다른 곳에서는 `realMemberID`로 매핑되고 있습니다.
|
|
28
|
+
|
|
29
|
+
이런 차이가 쌓이면 다음과 같은 문제가 생길 수 있습니다.
|
|
30
|
+
|
|
31
|
+
* 같은 개념을 여러 이름으로 부르게 됩니다.
|
|
32
|
+
* 도메인 모델 간 일관성이 떨어집니다.
|
|
33
|
+
* 새 매퍼를 작성할 때 어떤 이름을 써야 하는지 판단하기 어려워집니다.
|
|
34
|
+
* 문서와 타입 이름이 실제 도메인 언어를 안정적으로 반영하지 못합니다.
|
|
35
|
+
|
|
36
|
+
매핑 정책은 이런 문제를 막기 위해 **DTO 경로별 표준 도메인 이름**을 정의합니다.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
## 정책 선언하기
|
|
41
|
+
|
|
42
|
+
먼저 DTO 타입을 기준으로 매핑 정책을 선언합니다.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { defineMappingPolicy, source } from "runtypex/mapper";
|
|
46
|
+
|
|
47
|
+
const userPolicy = defineMappingPolicy<UserDto>()({
|
|
48
|
+
userId: source("user_id"),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
위 정책은 다음 규칙을 의미합니다.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
user_id -> userId
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
즉, `UserDto`의 `user_id` 경로를 도메인 필드로 사용할 때는 `userId`라는 이름을 표준으로 사용하겠다는 뜻입니다.
|
|
59
|
+
|
|
60
|
+
<br/>
|
|
61
|
+
|
|
62
|
+
## 매퍼에 정책 적용하기
|
|
63
|
+
|
|
64
|
+
정책을 선언한 뒤에는 `makeMapper()` 옵션으로 전달합니다.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const toAuditUser = makeMapper<UserDto, AuditUser>(auditMap, {
|
|
68
|
+
policy: userPolicy,
|
|
69
|
+
policyMode: "error",
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
이제 `auditMap`이 정책과 맞지 않는 이름을 사용하면 정책 위반으로 처리됩니다.
|
|
74
|
+
|
|
75
|
+
예를 들어 정책에서는 `user_id`의 표준 이름을 `userId`로 선언했는데, 매퍼에서 다음과 같이 작성하면:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const auditMap = defineMap<UserDto, AuditUser>()({
|
|
79
|
+
realMemberID: source("user_id"),
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`user_id`가 `realMemberID`로 매핑되었기 때문에 정책 위반이 됩니다.
|
|
84
|
+
|
|
85
|
+
정책을 따르려면 다음처럼 작성해야 합니다.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const auditMap = defineMap<UserDto, AuditUser>()({
|
|
89
|
+
userId: source("user_id"),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
<br/>
|
|
94
|
+
|
|
95
|
+
## 정책 모드
|
|
96
|
+
|
|
97
|
+
`policyMode`는 정책 위반을 어떻게 처리할지 결정합니다.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
policyMode: "warn"; // 기본값, 경고를 출력합니다.
|
|
101
|
+
policyMode: "error"; // 오류를 발생시킵니다.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| 모드 | 동작 | 사용 시점 |
|
|
105
|
+
| --------- | ----------------- | ----------------------------- |
|
|
106
|
+
| `"warn"` | 정책 위반을 경고로 출력합니다. | 기존 코드에 정책을 점진적으로 도입할 때 적합합니다. |
|
|
107
|
+
| `"error"` | 정책 위반을 오류로 처리합니다. | 명명 규칙을 반드시 강제해야 할 때 적합합니다. |
|
|
108
|
+
|
|
109
|
+
기존 프로젝트에 처음 정책을 도입할 때는 `"warn"`으로 시작하는 것이 좋습니다.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const toAuditUser = makeMapper<UserDto, AuditUser>(auditMap, {
|
|
113
|
+
policy: userPolicy,
|
|
114
|
+
policyMode: "warn",
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
경고를 확인하면서 기존 매퍼를 정리한 뒤, 규칙을 강제할 준비가 되면 `"error"`로 전환할 수 있습니다.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const toAuditUser = makeMapper<UserDto, AuditUser>(auditMap, {
|
|
122
|
+
policy: userPolicy,
|
|
123
|
+
policyMode: "error",
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
<br/>
|
|
128
|
+
|
|
129
|
+
## 런타임 및 트랜스포머 검사
|
|
130
|
+
|
|
131
|
+
정책 검증은 다음 두 경로에서 모두 실행됩니다.
|
|
132
|
+
|
|
133
|
+
* 런타임 `makeMapper()` 폴백
|
|
134
|
+
* 빌드 시점 `makeMapper<TDto, TDomain>()` 트랜스포머 출력
|
|
135
|
+
|
|
136
|
+
즉, 트랜스포머를 사용하지 않는 환경에서도 정책 검증이 동작합니다.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const toUser = makeMapper<UserDto, User>(userMap, {
|
|
140
|
+
policy: userPolicy,
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
트랜스포머가 없는 경우에는 런타임에서 `makeMapper()`가 매핑 명세를 해석하면서 정책을 검사합니다.
|
|
145
|
+
|
|
146
|
+
반대로 트랜스포머를 활성화한 경우에는 빌드 시점에 인라인 매퍼를 생성하면서 정책 검증이 반영됩니다.
|
|
147
|
+
|
|
148
|
+
따라서 빌드 연동 여부와 관계없이 같은 정책을 사용할 수 있습니다.
|
|
149
|
+
|
|
150
|
+
<br/>
|
|
151
|
+
|
|
152
|
+
## 중복 정책 항목
|
|
153
|
+
|
|
154
|
+
정책 자체에서도 같은 DTO 경로를 여러 도메인 이름에 매핑하면 안 됩니다.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const invalidPolicy = defineMappingPolicy<UserDto>()({
|
|
158
|
+
userId: source("user_id"),
|
|
159
|
+
realMemberID: source("user_id"),
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
위 정책은 유효하지 않습니다.
|
|
164
|
+
|
|
165
|
+
`user_id`라는 하나의 DTO 경로에 대해 `userId`와 `realMemberID`라는 두 개의 표준 이름을 선언하고 있기 때문입니다.
|
|
166
|
+
|
|
167
|
+
매핑 정책의 목적은 DTO 경로별로 하나의 표준 도메인 이름을 정하는 것입니다. 따라서 같은 DTO 경로는 정책 안에서 하나의 이름으로만 선언해야 합니다.
|
|
168
|
+
|
|
169
|
+
올바른 정책은 다음과 같습니다.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const userPolicy = defineMappingPolicy<UserDto>()({
|
|
173
|
+
userId: source("user_id"),
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
<br/>
|
|
178
|
+
|
|
179
|
+
## 정리
|
|
180
|
+
|
|
181
|
+
매핑 정책은 매퍼의 타입 안전성을 넘어, 코드베이스의 도메인 언어를 일관되게 유지하기 위한 장치입니다.
|
|
182
|
+
|
|
183
|
+
| 항목 | 설명 |
|
|
184
|
+
| ----- | ------------------------------------------- |
|
|
185
|
+
| 목적 | DTO 경로와 도메인 필드 이름의 표준 대응 관계를 정의합니다. |
|
|
186
|
+
| 선언 | `defineMappingPolicy<TDto>()`로 작성합니다. |
|
|
187
|
+
| 적용 | `makeMapper()` 옵션의 `policy`로 전달합니다. |
|
|
188
|
+
| 위반 처리 | `policyMode`로 `"warn"` 또는 `"error"`를 선택합니다. |
|
|
189
|
+
| 검사 위치 | 런타임 폴백과 트랜스포머 출력 모두에서 동작합니다. |
|
|
190
|
+
| 제한 | 같은 DTO 경로를 여러 도메인 이름에 매핑할 수 없습니다. |
|
|
191
|
+
|
|
192
|
+
이 기능은 여러 매퍼가 같은 DTO를 다루는 프로젝트에서 특히 유용합니다. 같은 원본 필드가 항상 같은 도메인 이름으로 표현되도록 만들어, 매퍼가 늘어나도 도메인 모델의 언어를 안정적으로 유지할 수 있습니다.
|