ClickHouse Backend Rules#
ํ๋ง๋๋ก (๋น๊ฐ๋ฐ์์ฉ)#
์ด ๋ฌธ์๋ ๋์ฉ๋ ๋ฐ์ดํฐ ๋ถ์ ์ฐฝ๊ณ ์ธ ClickHouse๋ฅผ ๋ค๋ฃฐ ๋ ์์ฃผ ์๊ธฐ๋ ์ค์๋ฅผ ๋ง์ ์ฃผ๋ ์์ ์์น ๋ชจ์์ ๋๋ค. ๋ง์น ํฐ ๋ฌผ๋ฅ ์ฐฝ๊ณ ์์ ๋ฌผ๊ฑด์ ๋ฃ์ ๋์ ๊บผ๋ผ ๋ ๊ท์น์ ์ ํด ๋์ด ๋ฌผ๊ฑด์ด ์ฌ๋ผ์ง๊ฑฐ๋ ์๋ฑํ ์นธ์ ๋ค์ด๊ฐ๋ ์ผ์ ๋ง๋ ๊ฒ๊ณผ ๊ฐ์ต๋๋ค. ๊ฐ๋ฐ์(๋๋ AI)๊ฐ ๋ถ์ ๊ธฐ๋ฅ์ ๋ง๋ค ๋ ์ด ์์น์ ๋ฐ๋ฅด๋ฉด ์ซ์๊ฐ ์ด๊ธ๋๊ฑฐ๋ ๋ฐ์ดํฐ๊ฐ ์ ๋ณด์ด๋ ํํ ๋ฒ๊ทธ๋ฅผ ์๋ฐฉํ ์ ์์ต๋๋ค.
๋ฌด์์ยท์ธ์ (๋น๊ฐ๋ฐ์์ฉ)#
- ๋ฌด์์ ํด ์ฃผ๋์: ์ซ์ ๋ฐ์ดํฐ๋ฅผ ์๋ชป ์ฝ์ด ์๊ธฐ๋ ์ค๋ฅ, ์ ์ฅํ ๊ณณ๊ณผ ์ฐพ๋ ๊ณณ์ด ๋ฌ๋ผ ๋ฐ์ดํฐ๊ฐ ์ ๋ณด์ด๋ ๋ฌธ์ , ๋น ๊ฐ(NULL)์ด๋ ์๊ฐ๋(ํ๊ตญ ์๊ฐ) ์ฒ๋ฆฌ ์ค์๋ฅผ ๋ง์ ์ค๋๋ค.
- ์ธ์ ์๋ํ๋์: ํต๊ณ๋ ๋ถ์ ๊ฐ์ ์ ๊ธฐ๋ฅ์ ClickHouse ๋ฐ์ดํฐ๋ก ๋ง๋ค ๋ ์ ์ฉํฉ๋๋ค. ์๋ฅผ ๋ค์ด "ํ๊ท ์ฝ์ ์๊ฐ", "์ฌ์ฉ์ ์ด๋ฒคํธ ์" ๊ฐ์ ์ง๊ณ ํ๋ฉด์ ๋ง๋ค ๋ ์ฐ์ ๋๋ค.
- ๊ฒฐ๊ณผ์ ์ผ๋ก: ๋ถ์ ํ๋ฉด์ ์ซ์๊ฐ ์ ํํด์ง๊ณ , ๋ฐ์ดํฐ๊ฐ ๋น ์ง๊ฑฐ๋ ์๊ฐ์ด ์ด๊ธ๋๋ ์ผ์ด ์ค์ด๋ญ๋๋ค.
ํต์ฌ ์ฉ์ด (๋น๊ฐ๋ฐ์์ฉ)#
| ์ฉ์ด | ์ฌ์ด ์ค๋ช |
|---|---|
| ClickHouse | ์์ฃผ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋น ๋ฅด๊ฒ ๋ชจ์ ๋ถ์ํ๊ธฐ ์ํ ๋ฐ์ดํฐ ์ ์ฅ ์ฐฝ๊ณ (๋ถ์ ์ ์ฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค). |
| CDC | "๋ณ๊ฒฝ ์ฌํญ ๋ฐ๋ผ ์ฎ๊ธฐ๊ธฐ". ํ์ชฝ ๋ฐ์ดํฐ๋ฒ ์ด์ค(PostgreSQL)์ ์ ์ฅํ๋ฉด ๊ทธ ๋ณํ๋ฅผ ์๋์ผ๋ก ๋ถ์ ์ฐฝ๊ณ (ClickHouse)๋ก ๋ณต์ฌํด ์ฃผ๋ ๋ฐฉ์. |
| PostgreSQL | ๊ฑฐ๋ ๊ธฐ๋ก์ฒ๋ผ ์ ํํจ์ด ์ค์ํ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๋ณด๊ดํ๋ ์ผ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค. |
| NULL | "๊ฐ์ด ๋น์ด ์์"์ ๋ปํ๋ ์ํ. ๊ทธ๋ฅ ๋๋ฉด 0์ฒ๋ผ ์๋ชป ๊ณ์ฐ๋ ์ ์์ด ๋ฐ๋ก ์ฒ๋ฆฌํด ์ค์ผ ํจ. |
| ํ์ (type) ์บ์คํ | ๋ฐ์ดํฐ์ ํํ(์ ์/์์ ๋ฑ)๋ฅผ ๋ค๋ฅธ ํํ๋ก ๋ฐ๊พธ๋ ์ผ. ์๋ชป ๋ฐ๊พธ๋ฉด ์ค๋ฅ๊ฐ ๋จ. |
| ํธ๋์ญ์ | ์ฌ๋ฌ ์์ ์ "์ ๋ถ ์ฑ๊ณต ์๋๋ฉด ์ ๋ถ ์ทจ์"๋ก ๋ฌถ์ด ๋ฐ์ดํฐ๊ฐ ์ด์ค๊ฐํ๊ฒ ๋จ์ง ์๊ฒ ํ๋ ์์ ์ฅ์น. |
| UTC / KST | UTC๋ ์ธ๊ณ ํ์ค ์๊ฐ, KST๋ ํ๊ตญ ์๊ฐ. ์ ์ฅ์ UTC๋ก ํ๊ณ ํ๋ฉด์๋ ํ๊ตญ ์๊ฐ์ผ๋ก ๋ฐ๊ฟ ๋ณด์ฌ ์ค. |
ClickHouse ๋ฐฑ์๋ ๊ฐ๋ฐ ์ ๋ฐ์ํ๋ ๋ฒ๊ทธ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํ ๊ท์น์ ๋๋ค.
ํ์ ์์ ์ฑ ๊ท์น#
scalar() ๋ฉ์๋ ํ์ ์บ์คํ #
๋ฌธ์ : ClickHouse scalar<double>() ๋ฉ์๋๋ ๊ฐ์ด 0์ผ ๋ int๋ฅผ ๋ฐํํฉ๋๋ค.
// โ WRONG: ๊ฐ์ด 0์ผ ๋ int ๋ฐํ โ ํ์
์๋ฌ
final avgTime = await client.scalar<double>(
'SELECT avg(reading_time) FROM ...',
); // ๊ฒฐ๊ณผ๊ฐ 0์ด๋ฉด int(0) ๋ฐํ โ double ์บ์คํ
์คํจ!
// โ
CORRECT: num์ผ๋ก ๋ฐ์์ toDouble() ๋ณํ
final avgTimeRaw = await client.scalar<num>(
'SELECT avg(reading_time) FROM ...',
);
final avgTime = avgTimeRaw?.toDouble() ?? 0.0;
ํฌํผ ํจ์ ํจํด (๊ถ์ฅ)#
// analytics_service.dart ์ฐธ์กฐ
double _toDouble(Object? value) {
if (value == null) return 0;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is num) return value.toDouble();
return 0;
}
// ์ฌ์ฉ ์์
final result = await client.scalar<num>('SELECT sum(amount) FROM ...');
final amount = _toDouble(result);
Row ๋ฐ์ดํฐ ์ฒ๋ฆฌ#
// โ WRONG: ์ง์ ์บ์คํ
final rows = await client.select('SELECT value FROM ...');
final value = rows.first['value'] as double; // int์ผ ์ ์์!
// โ
CORRECT: ํฌํผ ํจ์ ์ฌ์ฉ
final rows = await client.select('SELECT value FROM ...');
final value = _toDouble(rows.first['value']);
CDC ๋ฐ์ดํฐ ํ๋ฆ ๊ท์น#
๋ฐ์ดํฐ ์ ์ฅ๊ณผ ์กฐํ ํ ์ด๋ธ ์ผ์น ํ์ธ#
๋ฌธ์ : ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ํ ์ด๋ธ๊ณผ ์กฐํํ๋ ํ ์ด๋ธ์ด ๋ค๋ฅด๋ฉด ๋ฐ์ดํฐ๊ฐ ๋ณด์ด์ง ์์ต๋๋ค.
// โ WRONG: ์ ์ฅ๊ณผ ์กฐํ ํ
์ด๋ธ ๋ถ์ผ์น
// EventTrackingService โ ClickHouse `events` ํ
์ด๋ธ์ ์ ์ฅ
await eventService.track('screen_view', ...);
// AnalyticsService โ ClickHouse `user_event` ํ
์ด๋ธ์์ ์กฐํ
final result = await client.select('SELECT * FROM user_event');
// ๊ฒฐ๊ณผ: ๋น ๋ฐ์ดํฐ! (๋ค๋ฅธ ํ
์ด๋ธ)
// โ
CORRECT: CDC ํ๋ฆ ์ฌ์ฉ
// PostgreSQL `user_event` โ CDC โ ClickHouse `user_event`
await UserEvent.db.insertRow(session, event);
final result = await client.select('SELECT * FROM user_event');
CDC vs ์ง์ ์ ์ฅ ์ ํ ๊ธฐ์ค#
| ๋ฐฉ์ | ์ฌ์ฉ ์์ | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| PostgreSQL CDC | ํธ๋์ญ์ ์ผ๊ด์ฑ ํ์, ๊ฐ์ฌ ์ถ์ ํ์ | ACID, ๊ฐ์ฌ ๋ก๊ทธ, ๋ฐฑ์ | ์ง์ฐ ์๊ฐ (1-5์ด) |
| ์ง์ ClickHouse | ๊ณ ๋น๋ ์ด๋ฒคํธ, ์ค์๊ฐ ๋ถ์ ์ ์ฉ | ๋น ๋ฅธ ์ ์ฅ | ํธ๋์ญ์ ์์ |
// CDC ๋ฐฉ์ (๊ถ์ฅ - ๋๋ถ๋ถ์ ๋น์ฆ๋์ค ์ด๋ฒคํธ)
final event = UserEvent(
userId: userId,
eventName: 'screen_view',
properties: jsonEncode(properties),
timestamp: DateTime.now(),
);
await UserEvent.db.insertRow(session, event);
// ์ง์ ์ ์ฅ ๋ฐฉ์ (๊ณ ๋น๋ ์ด๋ฒคํธ ์ ์ฉ)
await eventService.track('mouse_move', properties: {...});
ClickHouse ์ฟผ๋ฆฌ ๊ท์น#
NULL ์ฒ๋ฆฌ#
// ClickHouse์์ NULL์ ๊ธฐ๋ณธ์ ์ผ๋ก 0 ๋๋ ๋น ๋ฌธ์์ด๋ก ์ฒ๋ฆฌ๋จ
// ๋ช
์์ NULL ์ฒ๋ฆฌ ๊ถ์ฅ
// โ WRONG: NULL ๋ฏธ์ฒ๋ฆฌ
SELECT avg(value) FROM table // NULL ํฌํจ ์ ์์๊ณผ ๋ค๋ฅธ ๊ฒฐ๊ณผ
// โ
CORRECT: NULL ๋ช
์์ ์ฒ๋ฆฌ
SELECT avgIf(value, value IS NOT NULL) FROM table
SELECT coalesce(avg(value), 0) FROM table
๋ ์ง/์๊ฐ ์ฒ๋ฆฌ#
// ClickHouse๋ UTC ๊ธฐ์ค์ผ๋ก ์ ์ฅ
// ํ๊ตญ ์๊ฐ(KST) ๋ณํ ํ์
// โ
์๊ฐ๋ ๋ณํ
SELECT toDateTime(timestamp, 'Asia/Seoul') as local_time FROM ...
// โ
๋ ์ง ๊ทธ๋ฃนํ (ํ๊ตญ ์๊ฐ ๊ธฐ์ค)
SELECT toDate(timestamp, 'Asia/Seoul') as date, count() FROM ...
GROUP BY date
์ฒดํฌ๋ฆฌ์คํธ#
์๋ก์ด ๋ถ์ ๊ธฐ๋ฅ ๊ตฌํ ์:
scalar<num>()์ฌ์ฉ +_toDouble()ํฌํผ ์ ์ฉ- ์ ์ฅ ํ ์ด๋ธ๊ณผ ์กฐํ ํ ์ด๋ธ ์ผ์น ํ์ธ
- CDC ํ๋ฆ vs ์ง์ ์ ์ฅ ๊ฒฐ์
- NULL ์ฒ๋ฆฌ ๋ช ์
- ์๊ฐ๋(KST) ๋ณํ ์ ์ฉ
๊ด๋ จ ํ์ผ#
-
backend/kobic_server/lib/src/feature/analytics/service/analytics_service.dart-_toDouble()ํฌํผ ํจํด -
backend/kobic_server/lib/src/feature/analytics/endpoint/screen_view_endpoint.dart- CDC ๋ฐฉ์ ์์ .claude/skills/clickhouse/SKILL.md- CDC ํ ์ด๋ธ ๋ชฉ๋ก