@things-factory/shell 10.0.10 → 10.0.13
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/dist-server/initializers/database.d.ts +33 -0
- package/dist-server/initializers/database.js +117 -0
- package/dist-server/initializers/database.js.map +1 -1
- package/dist-server/initializers/ormconfig.js +25 -0
- package/dist-server/initializers/ormconfig.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/headless-pool/index.js +78 -0
- package/dist-server/utils/headless-pool/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -27,6 +27,39 @@ export declare function getDataSourceNames(): string[];
|
|
|
27
27
|
* @returns {Repository<X>} - The repository for the specified entity.
|
|
28
28
|
*/
|
|
29
29
|
export declare function getRepository<X>(target: EntityTarget<X>, tx?: EntityManager): Repository<X>;
|
|
30
|
+
/**
|
|
31
|
+
* **무거운 읽기를 위한 저장소** — 다른 요청을 붙잡지 않게 별도 연결로 보낸다.
|
|
32
|
+
*
|
|
33
|
+
* ── 무엇을 재서 알았나 (2026-08-22) ────────────────────────────────────────
|
|
34
|
+
* TypeORM 의 sqlite 드라이버는 연결을 **하나**만 든다(풀 없음). 그래서 무거운 읽기 하나가 도는 동안
|
|
35
|
+
* 앱의 **모든** 질의가 그 뒤에 선다 — 수십 행짜리 `users`·`domains` 조회가 30초로 찍혔다.
|
|
36
|
+
*
|
|
37
|
+
* 같은 DB(16.3GB · 저널 1,134만 행)에서 실측했다. 무거운 읽기(2만 행)가 도는 동안 가벼운 읽기가
|
|
38
|
+
* 얼마나 기다리나:
|
|
39
|
+
*
|
|
40
|
+
* DataSource 하나 → 47,647ms
|
|
41
|
+
* DataSource 둘 → 2,786ms (평균 1,473ms)
|
|
42
|
+
*
|
|
43
|
+
* 즉 **sqlite 쪽 분리는 실재한다.** WAL 이면 연결마다 독립으로 읽으므로 잠금·직렬화가 풀린다.
|
|
44
|
+
*
|
|
45
|
+
* ── 남는 것을 감추지 않는다 ────────────────────────────────────────────────
|
|
46
|
+
* 평균 1,473ms 는 사라지지 않는다. 그것은 sqlite 가 아니라 **행을 객체로 만드는 JS 일**이다
|
|
47
|
+
* (같은 측정에서 한 행만 돌려주는 무거운 질의는 가벼운 읽기를 0.3ms 로 두었다). 이벤트 루프는 하나이고,
|
|
48
|
+
* 그 비용은 DB 를 바꿔도 남는다. 고칠 자리는 연결이 아니라 **그 질의**다 — 페이지 크기·필요한 컬럼만·
|
|
49
|
+
* 엔티티 대신 raw. 이 함수는 그 일을 대신해 주지 않는다.
|
|
50
|
+
*
|
|
51
|
+
* ── 언제 쓰나 ──────────────────────────────────────────────────────────────
|
|
52
|
+
* **읽기 전용이고, 느린 것이 측정된 경로**에만 쓴다. 두 가지를 주의한다.
|
|
53
|
+
*
|
|
54
|
+
* · **쓰지 않는다.** 이 연결로 쓰면 별도 쓰기 주체가 되어 잠금을 다툰다. 쓰기는 `getRepository` 다.
|
|
55
|
+
* · **쓴 뒤 곧바로 읽는 자리에는 쓰지 않는다.** 쓰기를 `await` 한 뒤라면 WAL 에서 최신 커밋이 보이므로
|
|
56
|
+
* 안전하지만, 같은 트랜잭션 안에서 읽어야 하는 값이면 `tx` 를 받는 `getRepository` 를 쓴다.
|
|
57
|
+
*
|
|
58
|
+
* 풀이 있는 드라이버(postgres·mysql)에서는 얻을 것이 없으므로 `'read'` 가 `'default'` 를 가리킨다 —
|
|
59
|
+
* 소비처는 드라이버를 알 필요가 없다. 배포가 그 연결을 원하지 않으면 `ormconfig4Read: false` 로 끈다
|
|
60
|
+
* (그때도 이 함수는 그대로 쓸 수 있다 — `'default'` 로 떨어진다).
|
|
61
|
+
*/
|
|
62
|
+
export declare function getReadRepository<X>(target: EntityTarget<X>): Repository<X>;
|
|
30
63
|
/**
|
|
31
64
|
* Initializes the database connections and data sources.
|
|
32
65
|
*/
|
|
@@ -6,6 +6,7 @@ exports.addDataSource = addDataSource;
|
|
|
6
6
|
exports.removeDataSource = removeDataSource;
|
|
7
7
|
exports.getDataSourceNames = getDataSourceNames;
|
|
8
8
|
exports.getRepository = getRepository;
|
|
9
|
+
exports.getReadRepository = getReadRepository;
|
|
9
10
|
const tslib_1 = require("tslib");
|
|
10
11
|
const typeorm_1 = require("typeorm");
|
|
11
12
|
const env_1 = require("@things-factory/env");
|
|
@@ -49,6 +50,72 @@ function getDataSourceNames() {
|
|
|
49
50
|
function getRepository(target, tx) {
|
|
50
51
|
return tx ? tx.getRepository(target) : getDataSource('default').getRepository(target);
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* **무거운 읽기를 위한 저장소** — 다른 요청을 붙잡지 않게 별도 연결로 보낸다.
|
|
55
|
+
*
|
|
56
|
+
* ── 무엇을 재서 알았나 (2026-08-22) ────────────────────────────────────────
|
|
57
|
+
* TypeORM 의 sqlite 드라이버는 연결을 **하나**만 든다(풀 없음). 그래서 무거운 읽기 하나가 도는 동안
|
|
58
|
+
* 앱의 **모든** 질의가 그 뒤에 선다 — 수십 행짜리 `users`·`domains` 조회가 30초로 찍혔다.
|
|
59
|
+
*
|
|
60
|
+
* 같은 DB(16.3GB · 저널 1,134만 행)에서 실측했다. 무거운 읽기(2만 행)가 도는 동안 가벼운 읽기가
|
|
61
|
+
* 얼마나 기다리나:
|
|
62
|
+
*
|
|
63
|
+
* DataSource 하나 → 47,647ms
|
|
64
|
+
* DataSource 둘 → 2,786ms (평균 1,473ms)
|
|
65
|
+
*
|
|
66
|
+
* 즉 **sqlite 쪽 분리는 실재한다.** WAL 이면 연결마다 독립으로 읽으므로 잠금·직렬화가 풀린다.
|
|
67
|
+
*
|
|
68
|
+
* ── 남는 것을 감추지 않는다 ────────────────────────────────────────────────
|
|
69
|
+
* 평균 1,473ms 는 사라지지 않는다. 그것은 sqlite 가 아니라 **행을 객체로 만드는 JS 일**이다
|
|
70
|
+
* (같은 측정에서 한 행만 돌려주는 무거운 질의는 가벼운 읽기를 0.3ms 로 두었다). 이벤트 루프는 하나이고,
|
|
71
|
+
* 그 비용은 DB 를 바꿔도 남는다. 고칠 자리는 연결이 아니라 **그 질의**다 — 페이지 크기·필요한 컬럼만·
|
|
72
|
+
* 엔티티 대신 raw. 이 함수는 그 일을 대신해 주지 않는다.
|
|
73
|
+
*
|
|
74
|
+
* ── 언제 쓰나 ──────────────────────────────────────────────────────────────
|
|
75
|
+
* **읽기 전용이고, 느린 것이 측정된 경로**에만 쓴다. 두 가지를 주의한다.
|
|
76
|
+
*
|
|
77
|
+
* · **쓰지 않는다.** 이 연결로 쓰면 별도 쓰기 주체가 되어 잠금을 다툰다. 쓰기는 `getRepository` 다.
|
|
78
|
+
* · **쓴 뒤 곧바로 읽는 자리에는 쓰지 않는다.** 쓰기를 `await` 한 뒤라면 WAL 에서 최신 커밋이 보이므로
|
|
79
|
+
* 안전하지만, 같은 트랜잭션 안에서 읽어야 하는 값이면 `tx` 를 받는 `getRepository` 를 쓴다.
|
|
80
|
+
*
|
|
81
|
+
* 풀이 있는 드라이버(postgres·mysql)에서는 얻을 것이 없으므로 `'read'` 가 `'default'` 를 가리킨다 —
|
|
82
|
+
* 소비처는 드라이버를 알 필요가 없다. 배포가 그 연결을 원하지 않으면 `ormconfig4Read: false` 로 끈다
|
|
83
|
+
* (그때도 이 함수는 그대로 쓸 수 있다 — `'default'` 로 떨어진다).
|
|
84
|
+
*/
|
|
85
|
+
function getReadRepository(target) {
|
|
86
|
+
return (getDataSource('read') ?? getDataSource('default')).getRepository(target);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* sqlite 일 때만 얹는 기본값 — **쓰기가 읽기를 막지 않게.**
|
|
90
|
+
*
|
|
91
|
+
* ── 무엇을 재서 알았나 (2026-08-22) ────────────────────────────────────────
|
|
92
|
+
* 개발 DB(16.3GB · 저널 1,134만 행)에서 서버가 간헐적으로 아주 느렸다. 밖에서는 보이지 않았다:
|
|
93
|
+
* `GET /` 는 1ms, CPU 는 96% 유휴, 외부 연결의 읽기는 깨끗했다. 서버가 스스로 말하게 하자
|
|
94
|
+
* (`maxQueryExecutionTime`) 20분에 **느린 질의 223건 · 합계 3,279초**가 나왔다.
|
|
95
|
+
*
|
|
96
|
+
* 결정적인 것은 **무엇이 느렸나가 아니라 무엇까지 느렸나**였다. 수십 행짜리 `users`·`domains` 조회가
|
|
97
|
+
* 32초·15초였다. 그럴 수 있는 비용이 아니다 — 그리고 종류가 다른 질의들의 최대값이 32.3 · 22.0 ·
|
|
98
|
+
* 15.7 로 **되풀이됐다.** 여러 질의가 같은 순간에 함께 풀렸다는 뜻이다: 줄서기다.
|
|
99
|
+
*
|
|
100
|
+
* 두 성질이 겹쳤다.
|
|
101
|
+
* ① sqlite 의 기본 저널 모드는 `delete` 다(아무도 고르지 않으면 그것이 된다 — 저장소 이력에
|
|
102
|
+
* `journal_mode` 를 정한 곳이 없다). 그 모드에서 **쓰는 동안 모든 읽기가 막힌다.**
|
|
103
|
+
* ② TypeORM 의 sqlite 드라이버는 연결을 **하나**만 든다(풀 없음). 그래서 느린 쓰기 하나가 앱의 모든
|
|
104
|
+
* 질의를 그 뒤에 세운다.
|
|
105
|
+
*
|
|
106
|
+
* WAL 은 ①을 없앤다 — 쓰는 중에도 읽기가 지나간다. 쓰기 자체를 빠르게 하지는 않지만, 사용자가 겪는
|
|
107
|
+
* 32초가 사라진다.
|
|
108
|
+
*
|
|
109
|
+
* ── 왜 sqlite 에만 얹나 ────────────────────────────────────────────────────
|
|
110
|
+
* `enableWAL` 은 sqlite 드라이버의 옵션이다. 다섯 드라이버를 서는 프레임워크이므로 다른 드라이버에
|
|
111
|
+
* 낯선 옵션을 흘리지 않는다(postgres·mysql 은 이 축이 없다 — 연결 풀과 MVCC 가 있다).
|
|
112
|
+
*
|
|
113
|
+
* **환경 설정이 덮을 수 있다** — 이 값은 뒤에 오는 `config.ormconfig` 보다 먼저 펼쳐진다.
|
|
114
|
+
* WAL 이 맞지 않는 자리가 있다(네트워크 파일시스템에서는 쓰지 못한다). 그때는 설정이 끈다.
|
|
115
|
+
*/
|
|
116
|
+
function sqliteWriteConcurrency(cfg) {
|
|
117
|
+
return cfg?.type === 'sqlite' ? { enableWAL: true } : {};
|
|
118
|
+
}
|
|
52
119
|
/**
|
|
53
120
|
* Initializes the database connections and data sources.
|
|
54
121
|
*/
|
|
@@ -57,6 +124,7 @@ const databaseInitializer = async () => {
|
|
|
57
124
|
const readConnectionConfig = env_1.config.get('ormconfig');
|
|
58
125
|
const dataSource = await (0, typeorm_1.createConnection)({
|
|
59
126
|
...ormconfig_js_1.default,
|
|
127
|
+
...sqliteWriteConcurrency(readConnectionConfig),
|
|
60
128
|
...readConnectionConfig
|
|
61
129
|
});
|
|
62
130
|
addDataSource('default', dataSource);
|
|
@@ -66,9 +134,58 @@ const databaseInitializer = async () => {
|
|
|
66
134
|
await dataSource.synchronize();
|
|
67
135
|
await dataSource.query('PRAGMA foreign_keys=ON');
|
|
68
136
|
}
|
|
137
|
+
/*
|
|
138
|
+
* **무거운 읽기용 연결** — sqlite 에만 만든다(§`getReadRepository` 가 이유와 실측을 든다).
|
|
139
|
+
*
|
|
140
|
+
* 풀이 없는 드라이버에서만 얻을 것이 있으므로 그때만 연결을 하나 더 연다. 다른 드라이버에서는
|
|
141
|
+
* `'read'` 를 등록하지 않고, `getReadRepository` 가 `'default'` 로 떨어진다 — 소비처는 드라이버를
|
|
142
|
+
* 알 필요가 없다.
|
|
143
|
+
*
|
|
144
|
+
* `synchronize` 는 반드시 끈다: 스키마를 만드는 주체는 하나여야 한다(위에서 `default` 가 했다).
|
|
145
|
+
* 두 연결이 같은 스키마를 만들려 하면 서로를 밀어낸다.
|
|
146
|
+
*
|
|
147
|
+
* 실패하면 **삼키지 않고 넘어간다** — 이 연결이 없어도 앱은 돈다(느릴 뿐이다). 그래서 부팅을
|
|
148
|
+
* 세우지는 않되, 없다는 사실은 로그에 남긴다: 조용히 빠지면 「고쳤는데 왜 그대로냐」가 된다.
|
|
149
|
+
*
|
|
150
|
+
* ── 설정이 덮을 수 있다 — `ormconfig4Read` ────────────────────────────────
|
|
151
|
+
* 기본은 **자동**이다: `tx` 와 달리 「어디」를 결정할 것이 없다(같은 DB · 같은 데이터이고, 다른 것은
|
|
152
|
+
* 연결이 하나 더 있다는 것뿐이다). 그래서 설정 항목 없이도 선다.
|
|
153
|
+
*
|
|
154
|
+
* 그런데 **끌 수 있어야 하는 자리가 실재한다.** WAL 을 쓸 수 없는 파일시스템(네트워크 마운트)에서는
|
|
155
|
+
* 연결을 늘려도 얻는 것이 없고 잠금 다툼만 늘어난다. 파일 핸들이 빡빡한 배포도 그렇다. 그 판단은
|
|
156
|
+
* **배포마다 갈리는 사실**이므로 코드에 박아 두지 않는다.
|
|
157
|
+
*
|
|
158
|
+
* 없으면 sqlite 면 자동으로 만든다(기본)
|
|
159
|
+
* `false` 만들지 않는다 — `getReadRepository` 가 `'default'` 로 떨어진다
|
|
160
|
+
* 객체 그 설정으로 만든다(읽기 전용 복제본을 가리키는 등)
|
|
161
|
+
*/
|
|
162
|
+
const readOverride = env_1.config.get('ormconfig4Read');
|
|
163
|
+
const wantsRead = readOverride === undefined ? readConnectionConfig.type === 'sqlite' : readOverride !== false;
|
|
164
|
+
if (!wantsRead && readOverride === false) {
|
|
165
|
+
env_1.logger.info('Read DataSource disabled by config (ormconfig4Read: false) — heavy reads share the default connection');
|
|
166
|
+
}
|
|
167
|
+
if (wantsRead) {
|
|
168
|
+
try {
|
|
169
|
+
const readSource = new typeorm_1.DataSource({
|
|
170
|
+
...ormconfig_js_1.default,
|
|
171
|
+
...readConnectionConfig,
|
|
172
|
+
...(readOverride && readOverride !== true ? readOverride : {}),
|
|
173
|
+
name: 'read',
|
|
174
|
+
synchronize: false,
|
|
175
|
+
migrationsRun: false
|
|
176
|
+
});
|
|
177
|
+
await readSource.initialize();
|
|
178
|
+
addDataSource('read', readSource);
|
|
179
|
+
env_1.logger.info('Read DataSource established (sqlite — heavy reads do not block the write connection)');
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
env_1.logger.warn('Read DataSource not established — heavy reads will share the default connection', e);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
69
185
|
if (env_1.config.get('ormconfig4Tx')) {
|
|
70
186
|
const dataSource4Tx = new typeorm_1.DataSource({
|
|
71
187
|
...ormconfig_js_1.default,
|
|
188
|
+
...sqliteWriteConcurrency(env_1.config.get('ormconfig4Tx')),
|
|
72
189
|
...env_1.config.get('ormconfig4Tx')
|
|
73
190
|
});
|
|
74
191
|
await dataSource4Tx.initialize();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../server/initializers/database.ts"],"names":[],"mappings":";;;AAYA,sCAEC;AAOD,sCAEC;AAMD,4CAEC;AAMD,gDAEC;AAOD,sCAEC;;AAhDD,qCAA+F;AAE/F,6CAAoD;AACpD,0EAAsC;AAEtC,MAAM,WAAW,GAAmC,EAAE,CAAA;AAEtD;;;;GAIG;AACH,SAAgB,aAAa,CAAC,IAAa;IACzC,OAAO,WAAW,CAAC,IAAI,IAAI,SAAS,CAAC,CAAA;AACvC,CAAC;AAED;;;;GAIG;AACH,SAAgB,aAAa,CAAC,IAAY,EAAE,UAAsB;IAChE,WAAW,CAAC,IAAI,CAAC,GAAG,UAAU,CAAA;AAChC,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,IAAY;IAC3C,OAAO,WAAW,CAAC,IAAI,CAAC,CAAA;AAC1B,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB;IAChC,OAAO,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;AACjC,CAAC;AAED;;;;GAIG;AACH,SAAgB,aAAa,CAAI,MAAuB,EAAE,EAAkB;IAC1E,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAI,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,aAAa,CAAI,MAAM,CAAC,CAAA;AAC7F,CAAC;AAED;;GAEG;AACI,MAAM,mBAAmB,GAAG,KAAK,IAAI,EAAE;IAC5C,IAAI,CAAC;QACH,MAAM,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAEpD,MAAM,UAAU,GAAG,MAAM,IAAA,0BAAgB,EAAC;YACxC,GAAG,sBAAS;YACZ,GAAG,oBAAoB;SACxB,CAAC,CAAA;QAEF,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAEpC,YAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;QAE7C,IAAI,oBAAoB,CAAC,IAAI,IAAI,QAAQ,IAAI,oBAAoB,CAAC,WAAW,IAAI,KAAK,EAAE,CAAC;YACvF,MAAM,UAAU,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAA;YACjD,MAAM,UAAU,CAAC,WAAW,EAAE,CAAA;YAC9B,MAAM,UAAU,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAA;QAClD,CAAC;QAED,IAAI,YAAM,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YAC/B,MAAM,aAAa,GAAG,IAAI,oBAAU,CAAC;gBACnC,GAAG,sBAAS;gBACZ,GAAG,YAAM,CAAC,GAAG,CAAC,cAAc,CAAC;aAC9B,CAAC,CAAA;YACF,MAAM,aAAa,CAAC,UAAU,EAAE,CAAA;YAChC,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,CAAA;YAElC,YAAM,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAA;QACnD,CAAC;aAAM,CAAC;YACN,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX;oEAC4D;QAC5D,YAAM,CAAC,KAAK,CAAC,+BAA+B,EAAE,CAAC,CAAC,CAAA;QAChD,MAAM,CAAC,CAAA;IACT,CAAC;AACH,CAAC,CAAA;AArCY,QAAA,mBAAmB,uBAqC/B","sourcesContent":["import { createConnection, DataSource, EntityManager, EntityTarget, Repository } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport ormconfig from './ormconfig.js'\n\nconst dataSources: { [name: string]: DataSource } = {}\n\n/**\n * Returns the specified DataSource by name.\n * @param {string} name - The name of the DataSource.\n * @returns {DataSource} - The DataSource with the specified name.\n */\nexport function getDataSource(name?: string): DataSource {\n return dataSources[name || 'default']\n}\n\n/**\n * Adds a new DataSource with the specified name.\n * @param {string} name - The name of the DataSource to add.\n * @param {DataSource} dataSource - The DataSource to add.\n */\nexport function addDataSource(name: string, dataSource: DataSource) {\n dataSources[name] = dataSource\n}\n\n/**\n * Removes a DataSource with the specified name.\n * @param {string} name - The name of the DataSource to remove.\n */\nexport function removeDataSource(name: string) {\n delete dataSources[name]\n}\n\n/**\n * Returns an array of all registered DataSource names.\n * @returns {string[]} - An array of DataSource names.\n */\nexport function getDataSourceNames() {\n return Object.keys(dataSources)\n}\n\n/**\n * Returns a repository for the specified entity.\n * @param {EntityTarget<X>} target - The target entity for which to get the repository.\n * @returns {Repository<X>} - The repository for the specified entity.\n */\nexport function getRepository<X>(target: EntityTarget<X>, tx?: EntityManager): Repository<X> {\n return tx ? tx.getRepository<X>(target) : getDataSource('default').getRepository<X>(target)\n}\n\n/**\n * Initializes the database connections and data sources.\n */\nexport const databaseInitializer = async () => {\n try {\n const readConnectionConfig = config.get('ormconfig')\n\n const dataSource = await createConnection({\n ...ormconfig,\n ...readConnectionConfig\n })\n\n addDataSource('default', dataSource)\n\n logger.info('Default DataSource established')\n\n if (readConnectionConfig.type == 'sqlite' && readConnectionConfig.synchronize == false) {\n await dataSource.query('PRAGMA foreign_keys=OFF')\n await dataSource.synchronize()\n await dataSource.query('PRAGMA foreign_keys=ON')\n }\n\n if (config.get('ormconfig4Tx')) {\n const dataSource4Tx = new DataSource({\n ...ormconfig,\n ...config.get('ormconfig4Tx')\n })\n await dataSource4Tx.initialize()\n addDataSource('tx', dataSource4Tx)\n\n logger.info('Transaction DataSource established')\n } else {\n addDataSource('tx', dataSource)\n }\n } catch (e) {\n /* DB 는 필수 인프라 — 초기화 실패를 삼키면 'default' DataSource 없이 부팅되어\n 이후 모든 요청이 런타임에 터진다(좀비 부팅). 실패를 전파해 즉시 중단(fail-fast)한다. */\n logger.error('Failed to initialize database', e)\n throw e\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../server/initializers/database.ts"],"names":[],"mappings":"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","sourcesContent":["import { createConnection, DataSource, EntityManager, EntityTarget, Repository } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport ormconfig from './ormconfig.js'\n\nconst dataSources: { [name: string]: DataSource } = {}\n\n/**\n * Returns the specified DataSource by name.\n * @param {string} name - The name of the DataSource.\n * @returns {DataSource} - The DataSource with the specified name.\n */\nexport function getDataSource(name?: string): DataSource {\n return dataSources[name || 'default']\n}\n\n/**\n * Adds a new DataSource with the specified name.\n * @param {string} name - The name of the DataSource to add.\n * @param {DataSource} dataSource - The DataSource to add.\n */\nexport function addDataSource(name: string, dataSource: DataSource) {\n dataSources[name] = dataSource\n}\n\n/**\n * Removes a DataSource with the specified name.\n * @param {string} name - The name of the DataSource to remove.\n */\nexport function removeDataSource(name: string) {\n delete dataSources[name]\n}\n\n/**\n * Returns an array of all registered DataSource names.\n * @returns {string[]} - An array of DataSource names.\n */\nexport function getDataSourceNames() {\n return Object.keys(dataSources)\n}\n\n/**\n * Returns a repository for the specified entity.\n * @param {EntityTarget<X>} target - The target entity for which to get the repository.\n * @returns {Repository<X>} - The repository for the specified entity.\n */\nexport function getRepository<X>(target: EntityTarget<X>, tx?: EntityManager): Repository<X> {\n return tx ? tx.getRepository<X>(target) : getDataSource('default').getRepository<X>(target)\n}\n\n/**\n * **무거운 읽기를 위한 저장소** — 다른 요청을 붙잡지 않게 별도 연결로 보낸다.\n *\n * ── 무엇을 재서 알았나 (2026-08-22) ────────────────────────────────────────\n * TypeORM 의 sqlite 드라이버는 연결을 **하나**만 든다(풀 없음). 그래서 무거운 읽기 하나가 도는 동안\n * 앱의 **모든** 질의가 그 뒤에 선다 — 수십 행짜리 `users`·`domains` 조회가 30초로 찍혔다.\n *\n * 같은 DB(16.3GB · 저널 1,134만 행)에서 실측했다. 무거운 읽기(2만 행)가 도는 동안 가벼운 읽기가\n * 얼마나 기다리나:\n *\n * DataSource 하나 → 47,647ms\n * DataSource 둘 → 2,786ms (평균 1,473ms)\n *\n * 즉 **sqlite 쪽 분리는 실재한다.** WAL 이면 연결마다 독립으로 읽으므로 잠금·직렬화가 풀린다.\n *\n * ── 남는 것을 감추지 않는다 ────────────────────────────────────────────────\n * 평균 1,473ms 는 사라지지 않는다. 그것은 sqlite 가 아니라 **행을 객체로 만드는 JS 일**이다\n * (같은 측정에서 한 행만 돌려주는 무거운 질의는 가벼운 읽기를 0.3ms 로 두었다). 이벤트 루프는 하나이고,\n * 그 비용은 DB 를 바꿔도 남는다. 고칠 자리는 연결이 아니라 **그 질의**다 — 페이지 크기·필요한 컬럼만·\n * 엔티티 대신 raw. 이 함수는 그 일을 대신해 주지 않는다.\n *\n * ── 언제 쓰나 ──────────────────────────────────────────────────────────────\n * **읽기 전용이고, 느린 것이 측정된 경로**에만 쓴다. 두 가지를 주의한다.\n *\n * · **쓰지 않는다.** 이 연결로 쓰면 별도 쓰기 주체가 되어 잠금을 다툰다. 쓰기는 `getRepository` 다.\n * · **쓴 뒤 곧바로 읽는 자리에는 쓰지 않는다.** 쓰기를 `await` 한 뒤라면 WAL 에서 최신 커밋이 보이므로\n * 안전하지만, 같은 트랜잭션 안에서 읽어야 하는 값이면 `tx` 를 받는 `getRepository` 를 쓴다.\n *\n * 풀이 있는 드라이버(postgres·mysql)에서는 얻을 것이 없으므로 `'read'` 가 `'default'` 를 가리킨다 —\n * 소비처는 드라이버를 알 필요가 없다. 배포가 그 연결을 원하지 않으면 `ormconfig4Read: false` 로 끈다\n * (그때도 이 함수는 그대로 쓸 수 있다 — `'default'` 로 떨어진다).\n */\nexport function getReadRepository<X>(target: EntityTarget<X>): Repository<X> {\n return (getDataSource('read') ?? getDataSource('default')).getRepository<X>(target)\n}\n\n/**\n * sqlite 일 때만 얹는 기본값 — **쓰기가 읽기를 막지 않게.**\n *\n * ── 무엇을 재서 알았나 (2026-08-22) ────────────────────────────────────────\n * 개발 DB(16.3GB · 저널 1,134만 행)에서 서버가 간헐적으로 아주 느렸다. 밖에서는 보이지 않았다:\n * `GET /` 는 1ms, CPU 는 96% 유휴, 외부 연결의 읽기는 깨끗했다. 서버가 스스로 말하게 하자\n * (`maxQueryExecutionTime`) 20분에 **느린 질의 223건 · 합계 3,279초**가 나왔다.\n *\n * 결정적인 것은 **무엇이 느렸나가 아니라 무엇까지 느렸나**였다. 수십 행짜리 `users`·`domains` 조회가\n * 32초·15초였다. 그럴 수 있는 비용이 아니다 — 그리고 종류가 다른 질의들의 최대값이 32.3 · 22.0 ·\n * 15.7 로 **되풀이됐다.** 여러 질의가 같은 순간에 함께 풀렸다는 뜻이다: 줄서기다.\n *\n * 두 성질이 겹쳤다.\n * ① sqlite 의 기본 저널 모드는 `delete` 다(아무도 고르지 않으면 그것이 된다 — 저장소 이력에\n * `journal_mode` 를 정한 곳이 없다). 그 모드에서 **쓰는 동안 모든 읽기가 막힌다.**\n * ② TypeORM 의 sqlite 드라이버는 연결을 **하나**만 든다(풀 없음). 그래서 느린 쓰기 하나가 앱의 모든\n * 질의를 그 뒤에 세운다.\n *\n * WAL 은 ①을 없앤다 — 쓰는 중에도 읽기가 지나간다. 쓰기 자체를 빠르게 하지는 않지만, 사용자가 겪는\n * 32초가 사라진다.\n *\n * ── 왜 sqlite 에만 얹나 ────────────────────────────────────────────────────\n * `enableWAL` 은 sqlite 드라이버의 옵션이다. 다섯 드라이버를 서는 프레임워크이므로 다른 드라이버에\n * 낯선 옵션을 흘리지 않는다(postgres·mysql 은 이 축이 없다 — 연결 풀과 MVCC 가 있다).\n *\n * **환경 설정이 덮을 수 있다** — 이 값은 뒤에 오는 `config.ormconfig` 보다 먼저 펼쳐진다.\n * WAL 이 맞지 않는 자리가 있다(네트워크 파일시스템에서는 쓰지 못한다). 그때는 설정이 끈다.\n */\nfunction sqliteWriteConcurrency(cfg: any): Record<string, unknown> {\n return cfg?.type === 'sqlite' ? { enableWAL: true } : {}\n}\n\n/**\n * Initializes the database connections and data sources.\n */\nexport const databaseInitializer = async () => {\n try {\n const readConnectionConfig = config.get('ormconfig')\n\n const dataSource = await createConnection({\n ...ormconfig,\n ...sqliteWriteConcurrency(readConnectionConfig),\n ...readConnectionConfig\n })\n\n addDataSource('default', dataSource)\n\n logger.info('Default DataSource established')\n\n if (readConnectionConfig.type == 'sqlite' && readConnectionConfig.synchronize == false) {\n await dataSource.query('PRAGMA foreign_keys=OFF')\n await dataSource.synchronize()\n await dataSource.query('PRAGMA foreign_keys=ON')\n }\n\n /*\n * **무거운 읽기용 연결** — sqlite 에만 만든다(§`getReadRepository` 가 이유와 실측을 든다).\n *\n * 풀이 없는 드라이버에서만 얻을 것이 있으므로 그때만 연결을 하나 더 연다. 다른 드라이버에서는\n * `'read'` 를 등록하지 않고, `getReadRepository` 가 `'default'` 로 떨어진다 — 소비처는 드라이버를\n * 알 필요가 없다.\n *\n * `synchronize` 는 반드시 끈다: 스키마를 만드는 주체는 하나여야 한다(위에서 `default` 가 했다).\n * 두 연결이 같은 스키마를 만들려 하면 서로를 밀어낸다.\n *\n * 실패하면 **삼키지 않고 넘어간다** — 이 연결이 없어도 앱은 돈다(느릴 뿐이다). 그래서 부팅을\n * 세우지는 않되, 없다는 사실은 로그에 남긴다: 조용히 빠지면 「고쳤는데 왜 그대로냐」가 된다.\n *\n * ── 설정이 덮을 수 있다 — `ormconfig4Read` ────────────────────────────────\n * 기본은 **자동**이다: `tx` 와 달리 「어디」를 결정할 것이 없다(같은 DB · 같은 데이터이고, 다른 것은\n * 연결이 하나 더 있다는 것뿐이다). 그래서 설정 항목 없이도 선다.\n *\n * 그런데 **끌 수 있어야 하는 자리가 실재한다.** WAL 을 쓸 수 없는 파일시스템(네트워크 마운트)에서는\n * 연결을 늘려도 얻는 것이 없고 잠금 다툼만 늘어난다. 파일 핸들이 빡빡한 배포도 그렇다. 그 판단은\n * **배포마다 갈리는 사실**이므로 코드에 박아 두지 않는다.\n *\n * 없으면 sqlite 면 자동으로 만든다(기본)\n * `false` 만들지 않는다 — `getReadRepository` 가 `'default'` 로 떨어진다\n * 객체 그 설정으로 만든다(읽기 전용 복제본을 가리키는 등)\n */\n const readOverride = config.get('ormconfig4Read')\n const wantsRead = readOverride === undefined ? readConnectionConfig.type === 'sqlite' : readOverride !== false\n if (!wantsRead && readOverride === false) {\n logger.info('Read DataSource disabled by config (ormconfig4Read: false) — heavy reads share the default connection')\n }\n if (wantsRead) {\n try {\n const readSource = new DataSource({\n ...ormconfig,\n ...readConnectionConfig,\n ...(readOverride && readOverride !== true ? readOverride : {}),\n name: 'read',\n synchronize: false,\n migrationsRun: false\n })\n await readSource.initialize()\n addDataSource('read', readSource)\n logger.info('Read DataSource established (sqlite — heavy reads do not block the write connection)')\n } catch (e) {\n logger.warn('Read DataSource not established — heavy reads will share the default connection', e)\n }\n }\n\n if (config.get('ormconfig4Tx')) {\n const dataSource4Tx = new DataSource({\n ...ormconfig,\n ...sqliteWriteConcurrency(config.get('ormconfig4Tx')),\n ...config.get('ormconfig4Tx')\n })\n await dataSource4Tx.initialize()\n addDataSource('tx', dataSource4Tx)\n\n logger.info('Transaction DataSource established')\n } else {\n addDataSource('tx', dataSource)\n }\n } catch (e) {\n /* DB 는 필수 인프라 — 초기화 실패를 삼키면 'default' DataSource 없이 부팅되어\n 이후 모든 요청이 런타임에 터진다(좀비 부팅). 실패를 전파해 즉시 중단(fail-fast)한다. */\n logger.error('Failed to initialize database', e)\n throw e\n }\n}\n"]}
|
|
@@ -20,8 +20,33 @@ subscribers = flattenDeep(subscribers);
|
|
|
20
20
|
debugLogger('entities', entities);
|
|
21
21
|
debugLogger('migrations', migrations);
|
|
22
22
|
debugLogger('subscribers', subscribers);
|
|
23
|
+
/**
|
|
24
|
+
* 느린 질의를 **스스로 말하게** 하는 문턱(ms) — 넘긴 질의만 SQL 과 함께 찍는다.
|
|
25
|
+
*
|
|
26
|
+
* ── 왜 기본값으로 두는가 (2026-08-22) ──────────────────────────────────────
|
|
27
|
+
* 서버가 간헐적으로 느리다는 것을 밖에서 재려 하면 계속 놓친다. 실제로 그랬다: `GET /` 는 1ms 이고
|
|
28
|
+
* (302 라 DB 를 거의 안 지난다) CPU 프로파일은 96% 유휴였다 — 유휴는 **기다리는 중**이라는 뜻인데
|
|
29
|
+
* 그것을 건강하다고 읽었다. 비용은 DB 를 지나는 경로에만, 실 데이터 양에서만 나타난다.
|
|
30
|
+
*
|
|
31
|
+
* 그리고 sqlite 드라이버는 연결을 **하나**만 든다(`SqliteDriver.databaseConnection`, 풀 없음). 그래서
|
|
32
|
+
* 어딘가에서 느린 질의 하나가 도는 동안 **앱의 모든 질의가 그 뒤에 줄을 선다.** 실측: 저널
|
|
33
|
+
* 1,134만 행에서 `count(*)` 한 번이 11.2초다. 그 11초 동안 모든 화면이 멈추고, CPU 는 유휴로 보인다.
|
|
34
|
+
*
|
|
35
|
+
* 그러므로 필요한 것은 추측이 아니라 **이름**이다. 넘긴 질의가 자기 SQL 을 찍으면 다음에 느려질 때
|
|
36
|
+
* 우리가 잴 일이 없다.
|
|
37
|
+
*
|
|
38
|
+
* ── 왜 `logging` 과 무관하게 켜지는가 ─────────────────────────────────────
|
|
39
|
+
* TypeORM 의 `AbstractLogger.isLogEnabledFor` 가 `'query-slow'` 에는 **무조건 true** 를 돌려준다
|
|
40
|
+
* (`logging: false` 여도 찍힌다). 즉 이 값만 주면 되고, 질의 전체가 쏟아지지는 않는다.
|
|
41
|
+
*
|
|
42
|
+
* 문턱을 1초로 둔다. 사람이 「눌렀는데 반응이 없다」고 느끼는 구간이 그 위이고, 그보다 낮추면 정상적인
|
|
43
|
+
* 무거운 질의까지 섞여 로그가 신호를 잃는다. 환경별 설정(`config.ormconfig`)이 이 값을 덮을 수 있다 —
|
|
44
|
+
* 여기 있는 것은 **기본값**이다.
|
|
45
|
+
*/
|
|
46
|
+
const SLOW_QUERY_MS = 1000;
|
|
23
47
|
module.exports = {
|
|
24
48
|
namingStrategy: new naming_strategy_js_1.NamingStrategy(),
|
|
49
|
+
maxQueryExecutionTime: SLOW_QUERY_MS,
|
|
25
50
|
entities,
|
|
26
51
|
migrations,
|
|
27
52
|
subscribers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ormconfig.js","sourceRoot":"","sources":["../../server/initializers/ormconfig.ts"],"names":[],"mappings":";;;AAAA,6CAAgE;AAChE,6DAAqD;AACrD,0DAAyB;AAEzB,MAAM,WAAW,GAAG,IAAA,eAAK,EAAC,gCAAgC,CAAC,CAAA;AAE3D,SAAS,WAAW,CAAC,GAAG;IACtB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAC5G,CAAC;AAED;;EAEE;AACF,IAAI,QAAQ,GAAG,wBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,YAAM,EAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAChH,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAEhC,IAAI,UAAU,GAAG,wBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,YAAM,EAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AACpH,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;AAEpC,IAAI,WAAW,GAAG,wBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,YAAM,EAAC,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AACtH,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,CAAA;AAEtC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;AACjC,WAAW,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;AACrC,WAAW,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;AAEvC,MAAM,CAAC,OAAO,GAAG;IACf,cAAc,EAAE,IAAI,mCAAc,EAAE;IACpC,QAAQ;IACR,UAAU;IACV,WAAW;CACZ,CAAA;AAED,kBAAe,MAAM,CAAC,OAAO,CAAA","sourcesContent":["import { orderedModuleNames, loader } from '@things-factory/env'\nimport { NamingStrategy } from './naming-strategy.js'\nimport debug from 'debug'\n\nconst debugLogger = debug('things-factory:shell:ormconfig')\n\nfunction flattenDeep(arr) {\n return arr.reduce((acc, val) => (Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val)), [])\n}\n\n/*\n dependencies list를 받아서, entities, migrations, subscribers 폴더 어레이를 빌드한다.\n*/\nvar entities = orderedModuleNames.map(dep => loader(dep).entities).filter(entity => entity && entity.length > 0)\nentities = flattenDeep(entities)\n\nvar migrations = orderedModuleNames.map(dep => loader(dep).migrations).filter(entity => entity && entity.length > 0)\nmigrations = flattenDeep(migrations)\n\nvar subscribers = orderedModuleNames.map(dep => loader(dep).subscribers).filter(entity => entity && entity.length > 0)\nsubscribers = flattenDeep(subscribers)\n\ndebugLogger('entities', entities)\ndebugLogger('migrations', migrations)\ndebugLogger('subscribers', subscribers)\n\nmodule.exports = {\n namingStrategy: new NamingStrategy(),\n entities,\n migrations,\n subscribers\n}\n\nexport default module.exports\n"]}
|
|
1
|
+
{"version":3,"file":"ormconfig.js","sourceRoot":"","sources":["../../server/initializers/ormconfig.ts"],"names":[],"mappings":";;;AAAA,6CAAgE;AAChE,6DAAqD;AACrD,0DAAyB;AAEzB,MAAM,WAAW,GAAG,IAAA,eAAK,EAAC,gCAAgC,CAAC,CAAA;AAE3D,SAAS,WAAW,CAAC,GAAG;IACtB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAC5G,CAAC;AAED;;EAEE;AACF,IAAI,QAAQ,GAAG,wBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,YAAM,EAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAChH,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAEhC,IAAI,UAAU,GAAG,wBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,YAAM,EAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AACpH,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;AAEpC,IAAI,WAAW,GAAG,wBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,YAAM,EAAC,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AACtH,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,CAAA;AAEtC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;AACjC,WAAW,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;AACrC,WAAW,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,aAAa,GAAG,IAAI,CAAA;AAE1B,MAAM,CAAC,OAAO,GAAG;IACf,cAAc,EAAE,IAAI,mCAAc,EAAE;IACpC,qBAAqB,EAAE,aAAa;IACpC,QAAQ;IACR,UAAU;IACV,WAAW;CACZ,CAAA;AAED,kBAAe,MAAM,CAAC,OAAO,CAAA","sourcesContent":["import { orderedModuleNames, loader } from '@things-factory/env'\nimport { NamingStrategy } from './naming-strategy.js'\nimport debug from 'debug'\n\nconst debugLogger = debug('things-factory:shell:ormconfig')\n\nfunction flattenDeep(arr) {\n return arr.reduce((acc, val) => (Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val)), [])\n}\n\n/*\n dependencies list를 받아서, entities, migrations, subscribers 폴더 어레이를 빌드한다.\n*/\nvar entities = orderedModuleNames.map(dep => loader(dep).entities).filter(entity => entity && entity.length > 0)\nentities = flattenDeep(entities)\n\nvar migrations = orderedModuleNames.map(dep => loader(dep).migrations).filter(entity => entity && entity.length > 0)\nmigrations = flattenDeep(migrations)\n\nvar subscribers = orderedModuleNames.map(dep => loader(dep).subscribers).filter(entity => entity && entity.length > 0)\nsubscribers = flattenDeep(subscribers)\n\ndebugLogger('entities', entities)\ndebugLogger('migrations', migrations)\ndebugLogger('subscribers', subscribers)\n\n/**\n * 느린 질의를 **스스로 말하게** 하는 문턱(ms) — 넘긴 질의만 SQL 과 함께 찍는다.\n *\n * ── 왜 기본값으로 두는가 (2026-08-22) ──────────────────────────────────────\n * 서버가 간헐적으로 느리다는 것을 밖에서 재려 하면 계속 놓친다. 실제로 그랬다: `GET /` 는 1ms 이고\n * (302 라 DB 를 거의 안 지난다) CPU 프로파일은 96% 유휴였다 — 유휴는 **기다리는 중**이라는 뜻인데\n * 그것을 건강하다고 읽었다. 비용은 DB 를 지나는 경로에만, 실 데이터 양에서만 나타난다.\n *\n * 그리고 sqlite 드라이버는 연결을 **하나**만 든다(`SqliteDriver.databaseConnection`, 풀 없음). 그래서\n * 어딘가에서 느린 질의 하나가 도는 동안 **앱의 모든 질의가 그 뒤에 줄을 선다.** 실측: 저널\n * 1,134만 행에서 `count(*)` 한 번이 11.2초다. 그 11초 동안 모든 화면이 멈추고, CPU 는 유휴로 보인다.\n *\n * 그러므로 필요한 것은 추측이 아니라 **이름**이다. 넘긴 질의가 자기 SQL 을 찍으면 다음에 느려질 때\n * 우리가 잴 일이 없다.\n *\n * ── 왜 `logging` 과 무관하게 켜지는가 ─────────────────────────────────────\n * TypeORM 의 `AbstractLogger.isLogEnabledFor` 가 `'query-slow'` 에는 **무조건 true** 를 돌려준다\n * (`logging: false` 여도 찍힌다). 즉 이 값만 주면 되고, 질의 전체가 쏟아지지는 않는다.\n *\n * 문턱을 1초로 둔다. 사람이 「눌렀는데 반응이 없다」고 느끼는 구간이 그 위이고, 그보다 낮추면 정상적인\n * 무거운 질의까지 섞여 로그가 신호를 잃는다. 환경별 설정(`config.ormconfig`)이 이 값을 덮을 수 있다 —\n * 여기 있는 것은 **기본값**이다.\n */\nconst SLOW_QUERY_MS = 1000\n\nmodule.exports = {\n namingStrategy: new NamingStrategy(),\n maxQueryExecutionTime: SLOW_QUERY_MS,\n entities,\n migrations,\n subscribers\n}\n\nexport default module.exports\n"]}
|