{"id":136,"date":"2026-10-14T08:02:59","date_gmt":"2026-10-14T08:02:59","guid":{"rendered":"https:\/\/dingdonghome.duckdns.org\/?p=136"},"modified":"2026-10-14T08:11:13","modified_gmt":"2026-10-14T08:11:13","slug":"%ea%b0%9c%eb%b0%9c%ec%9d%bc%ec%a7%80-05-%eb%a9%94%ec%8b%a0%ec%a0%80-db-%ec%8a%a4%ed%82%a4%eb%a7%88-%ec%84%a4%ea%b3%84%ec%99%80-go%ec%9d%98-%ec%b5%9c%ea%b0%95-%ec%a1%b0%ed%95%a9-sqlc-%ec%97%b0","status":"publish","type":"post","link":"https:\/\/dinggo.duckdns.org\/?p=136","title":{"rendered":"[\uac1c\ubc1c\uc77c\uc9c0 #05] \uba54\uc2e0\uc800 DB \uc2a4\ud0a4\ub9c8 \uc124\uacc4\uc640 Go\uc758 \ucd5c\uac15 \uc870\ud569 sqlc \uc5f0\ub3d9\ud558\uae30 (PostgreSQL + sqlc)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">\uc548\ub155\ud558\uc138\uc694! \uce74\uce74\uc624\ud1a1 \uc2a4\ud0c0\uc77c\uc758 \uac00\uc871\/\uce5c\uad6c\uc6a9 \ud504\ub77c\uc774\ube57 \ud06c\ub85c\uc2a4\ud50c\ub7ab\ud3fc \uba54\uc2e0\uc800 <strong>&#8216;\ub529\ub3d9(DingDong)&#8217;<\/strong> \ub9cc\ub4e4\uae30 \ud504\ub85c\uc81d\ud2b8 5\ud3b8\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc9c0\ub09c [4\ud3b8]\uc5d0\uc11c\ub294 \uc720\uadf8\ub9b0 NAS\uc758 \uc800\uc0ac\uc591 \ud558\ub4dc\uc6e8\uc5b4\uc5d0\uc11c\ub3c4 \uae43\ud138\ucc98\ub7fc \uac00\ubccd\uac8c \ub3cc\uc544\uac00\ub294 **Go \ubc31\uc5d4\ub4dc API \uc5d4\uc9c4(chi \ub77c\uc6b0\ud130, Graceful Shutdown, 25MB \ucd08\uacbd\ub7c9 Dockerfile, Redis \uc5f0\ub3d9)**\uc744 \uc138\ud305\ud588\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\ubc88 5\ud3b8\uc5d0\uc11c\ub294 \uba54\uc2e0\uc800 \ubc31\uc5d4\ub4dc\uc758 \uc2ec\uc7a5\uc778 **\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc2a4\ud0a4\ub9c8(\ud14c\uc774\ube14 \uad6c\uc870)**\ub97c \uc644\ubcbd\ud558\uac8c \uc124\uacc4\ud558\uace0, Go \uc9c4\uc601\uc5d0\uc11c \uac00\uc7a5 \uc6b0\uc544\ud558\uace0 \uac15\ub825\ud55c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ud234\uc778 **<code>sqlc<\/code>**\ub97c \uc5f0\ub3d9\ud558\ub294 \uacfc\uc815\uc744 \ub2e4\ub8f9\ub2c8\ub2e4.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. \uba54\uc2e0\uc800 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc124\uacc4 \uc2dc \ubc18\ub4dc\uc2dc \uace0\ub824\ud574\uc57c \ud560 4\uac00\uc9c0<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\ub2e8\uc21c\ud55c \uac8c\uc2dc\ud310\uc774\ub098 \uc1fc\ud551\ubab0\uacfc \ub2ec\ub9ac, \uce74\uce74\uc624\ud1a1 \uc2a4\ud0c0\uc77c\uc758 <strong>\uc2e4\uc2dc\uac04 \uba54\uc2e0\uc800<\/strong>\ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc124\uacc4 \uc2dc \ub9e4\uc6b0 \uae4c\ub2e4\ub85c\uc6b4 \uc694\uad6c\uc0ac\ud56d\ub4e4\uc774 \uc788\uc2b5\ub2c8\ub2e4:<\/p>\n\n\n\n<div class=\"wp-block-canaryblue-mermaid-diagram is-theme-default wp-dark-mode-ignore\" data-mermaid-label=\"Mermaid diagram\" data-mermaid-theme=\"default\"><pre class=\"wp-block-canaryblue-mermaid-diagram__source\" data-mermaid-source=\"true\">flowchart TD\n    A[\"\uba54\uc2e0\uc800 DB \ud575\uc2ec \uc124\uacc4 \uc6d0\uce59\"] --> B[\"1. \ubc29\ubcc4 \ub2e8\uc870 \uc99d\uac00 \uc21c\ubc88(seq)\"]\n    A --> C[\"2. 1:1 \ucc44\ud305\ubc29 \uc911\ubcf5 \uc0dd\uc131 \ubc29\uc9c0\"]\n    A --> D[\"3. \uac1c\uc778\uc815\ubcf4 \ubcf4\ud638 \uc5f0\ub77d\ucc98 \ud574\uc2dc \ub9e4\uce6d\"]\n    A --> E[\"4. \ub2e4\uc911 \uae30\uae30 \uc138\uc158 \ubc0f \uc77d\uc74c \ud655\uc778\"]\n\n    linkStyle default stroke-width:3px,stroke:#333333<\/pre><div class=\"wp-block-canaryblue-mermaid-diagram__output\"><\/div><\/div>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ubc29\ubcc4 \ub2e8\uc870 \uc99d\uac00 \uc21c\ubc88 (<code>seq<\/code>)<\/strong>:\n<ul class=\"wp-block-list\">\n<li>\uc2dc\uac04(Timestamp)\uc73c\ub85c\ub9cc \uba54\uc2dc\uc9c0\ub97c \uc815\ub82c\ud558\uba74 \uae30\uae30 \uac04 \uc2dc\uacc4 \uc624\ucc28\ub098 1\ubc00\ub9ac\ucd08 \ub0b4 \ub2e4\uc911 \uba54\uc2dc\uc9c0 \ubc1c\uc0dd \uc2dc \uc21c\uc11c\uac00 \ub4a4\ud2c0\ub9bd\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\ubc29\ub9c8\ub2e4 1, 2, 3\u2026 \uc21c\ubc88(<code>seq<\/code>)\uc744 \uc6d0\uc790\uc801(Atomic)\uc73c\ub85c \ubd80\uc5ec\ud574\uc57c \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \ub04a\uacbc\ub2e4 \ub2e4\uc2dc \uc5f0\uacb0\ub418\uc5c8\uc744 \ub54c **&#8221;5\ubc88 \uc774\ud6c4\uc758 \uba54\uc2dc\uc9c0\ub9cc \uc8fc\uc138\uc694!&#8221;**\ub77c\uba70 \ub204\ub77d \uc5c6\uc774 \uc644\ubcbd\ud55c \uc99d\ubd84 \ub3d9\uae30\ud654\ub97c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>1:1 \ucc44\ud305\ubc29 \uc911\ubcf5 \uc0dd\uc131 \ubc29\uc9c0 (<code>direct_key<\/code>)<\/strong>:\n<ul class=\"wp-block-list\">\n<li>A\uac00 B\uc5d0\uac8c \ub300\ud654\ub97c \uac78\uace0, B\ub3c4 A\uc5d0\uac8c \ub300\ud654\ub97c \uac78 \ub54c \uc11c\ub85c \ub2e4\ub978 1:1 \ubc29\uc774 \uc911\ubcf5\uc73c\ub85c \uc0dd\uae30\uba74 \uc548 \ub429\ub2c8\ub2e4.<\/li>\n\n\n\n<li>\ub450 \uc720\uc800\uc758 UUID\ub97c \uc624\ub984\ucc28\uc21c\uc73c\ub85c \uc815\ub82c\ud574 \uc720\ub2c8\ud06c \ud0a4(<code>\uc791\uc740UUID:\ud070UUID<\/code>)\ub85c \uac15\uc81c\ud558\uc5ec DB \ub808\ubca8\uc5d0\uc11c \uc911\ubcf5\uc744 \uc6d0\ucc9c \ucc28\ub2e8\ud569\ub2c8\ub2e4.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\uc804\ud654\ubc88\ud638 SHA-256 \ud574\uc2dc \ub9e4\uce6d (<code>phone_hash<\/code>)<\/strong>:\n<ul class=\"wp-block-list\">\n<li>\uac1c\uc778\uc815\ubcf4 \ubcf4\ud638\ub97c \uc704\ud574 \ud3c9\ubb38 \uc804\ud654\ubc88\ud638\ub294 DB\uc5d0 \ub0a8\uae30\uc9c0 \uc54a\uace0, E.164 \uad6d\uc81c \ud45c\uc900 \uc804\ud654\ubc88\ud638 \uaddc\uaca9\uc758 SHA-256 \ud574\uc2dc\uac12\ub9cc \uc800\uc7a5\ud558\uc5ec \uc8fc\uc18c\ub85d \ub3d9\uae30\ud654 \uc2dc \uce5c\uad6c\ub97c \uc548\uc804\ud558\uac8c \ub9e4\uce6d\ud569\ub2c8\ub2e4.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\ub2e4\uc911 \uae30\uae30(\ubaa8\ubc14\uc77c\/\ud0dc\ube14\ub9bf\/PC) \uc138\uc158 \uad00\ub9ac<\/strong>:\n<ul class=\"wp-block-list\">\n<li>\ud55c \uc0ac\uc6a9\uc790\uac00 \uc2a4\ub9c8\ud2b8\ud3f0, \uc544\uc774\ud328\ub4dc, \ub9e5\ubd81\uc5d0\uc11c \ub3d9\uc2dc \uc811\uc18d\ud560 \uc218 \uc788\uc73c\ubbc0\ub85c \uae30\uae30\ubcc4(<code>devices<\/code>) \uc138\uc158\uacfc \uac1c\ubcc4 \ud478\uc2dc \ud1a0\ud070(FCM\/APNs)\uc744 \ub3c5\ub9bd\uc801\uc73c\ub85c \uad00\ub9ac\ud569\ub2c8\ub2e4.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. \uc65c \ubb34\uac70\uc6b4 ORM \ub300\uc2e0 <code>sqlc<\/code>\ub97c \uc120\ud0dd\ud588\uc744\uae4c?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js\ub098 Python\uc5d0\uc11c\ub294 TypeORM, Prisma, SQLAlchemy \uac19\uc740 \ubb34\uac70\uc6b4 ORM\uc744 \ud754\ud788 \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \ud558\uc9c0\ub9cc Go \uc5b8\uc5b4\uc5d0\uc11c\ub294 ORM\ubcf4\ub2e4 **<code>sqlc<\/code>**\uac00 \uc555\ub3c4\uc801\uc778 \ucc2c\uc0ac\ub97c \ubc1b\uace0 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\ube44\uad50 \ud56d\ubaa9<\/th><th>\uc804\ud1b5\uc801\uc778 \ubb34\uac70\uc6b4 ORM (GORM \ub4f1)<\/th><th>Go \uc9c4\uc601\uc758 \ud45c\uc900 <code>sqlc<\/code><\/th><\/tr><\/thead><tbody><tr><td><strong>\ub3d9\uc791 \ubc29\uc2dd<\/strong><\/td><td>\ub7f0\ud0c0\uc784\uc5d0 \ub9ac\ud50c\ub809\uc158(Reflection)\uc73c\ub85c SQL \ub3d9\uc801 \uc0dd\uc131<\/td><td><strong>\ucef4\ud30c\uc77c \ud0c0\uc784\uc5d0 \uc21c\uc218 Go \ucf54\ub4dc\ub97c \ubbf8\ub9ac \uc0dd\uc131<\/strong><\/td><\/tr><tr><td><strong>\uba54\ubaa8\ub9ac \ubc0f CPU<\/strong><\/td><td>\uac1d\uccb4 \ub9e4\ud551 \uc624\ubc84\ud5e4\ub4dc\ub85c \uba54\ubaa8\ub9ac \uc18c\ubaa8 \ud07c<\/td><td>\uc21c\uc218 \ub4dc\ub77c\uc774\ubc84 \uc218\uc900\uc758 <strong>\uadf9\ud55c\uc758 \uc18d\ub3c4\uc640 \ucd08\uacbd\ub7c9 RAM<\/strong><\/td><\/tr><tr><td><strong>\ud0c0\uc785 \uc548\uc815\uc131<\/strong><\/td><td>\ub7f0\ud0c0\uc784\uc5d0 \uceec\ub7fc\uba85 \uc624\ud0c0 \uc5d0\ub7ec \ubc1c\uc0dd \uac00\ub2a5<\/td><td><strong>SQL \ubb38\ubc95 \uac80\uc0ac + Go \uad6c\uc870\uccb4 \ud0c0\uc785 100% \ubcf4\uc7a5<\/strong><\/td><\/tr><tr><td><strong>SQL \ud559\uc2b5 \ube44\uc6a9<\/strong><\/td><td>\ud504\ub808\uc784\uc6cc\ud06c \uc804\uc6a9 DSL \ubb38\ubc95\uc744 \uc0c8\ub85c \ubc30\uc6cc\uc57c \ud568<\/td><td><strong>\uac1c\ubc1c\uc790\uac00 \uc6d0\ub798 \uc54c\ub358 \ud45c\uc900 PostgreSQL SQL \uadf8\ub300\ub85c \uc791\uc131<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <strong>sqlc\uc758 \ub9c8\ubc95<\/strong>: \uac1c\ubc1c\uc790\uac00 <code>.sql<\/code> \ud30c\uc77c\uc5d0 \uc21c\uc218\ud55c PostgreSQL \ucffc\ub9ac\ub97c \uc791\uc131\ud558\uace0 \ud130\ubbf8\ub110\uc5d0\uc11c <code>sqlc generate<\/code>\ub97c \uce58\uba74, sqlc\uac00 PostgreSQL \ubb38\ubc95\uc744 \ud30c\uc2f1\ud558\uc5ec Go \uad6c\uc870\uccb4\uc640 \ud568\uc218 \ucf54\ub4dc\ub97c 1\ubc00\ub9ac\ucd08 \ub9cc\uc5d0 \uc790\ub3d9\uc73c\ub85c \ub9cc\ub4e4\uc5b4 \uc90d\ub2c8\ub2e4!<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. \uc804\uccb4 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc2a4\ud0a4\ub9c8 (DDL) \ubc0f \ud30c\uc77c \uc0dd\uc131<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 \ud30c\uc77c \uacbd\ub85c \ubc0f \ud3f4\ub354 \uad6c\uc870 \uc548\ub0b4<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ub9c8\uc774\uadf8\ub808\uc774\uc158 \ud30c\uc77c\uc740 <code>server\/<\/code> \ub514\ub809\ud1a0\ub9ac \uc544\ub798\uc5d0 <strong><code>migrations\/<\/code><\/strong> \ud3f4\ub354\ub97c \ub9cc\ub4e4\uc5b4 \uad00\ub9ac\ud569\ub2c8\ub2e4.<br>DB \ub3c4\uad6c(<code>golang-migrate<\/code>, <code>sqlc<\/code>)\uac00 \uc778\uc2dd\ud560 \uc218 \uc788\ub3c4\ub85d \ud30c\uc77c \uc774\ub984 \ub4a4\uc5d0 <code>.up.sql<\/code>(\uc801\uc6a9\uc6a9)\uacfc <code>.down.sql<\/code>(\ub864\ubc31\uc6a9)\uc744 \ubd99\uc5ec\uc8fc\ub294 \uac83\uc774 \ud45c\uc900 \uaddc\uce59\uc785\ub2c8\ub2e4.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">server\/\n \u251c\u2500 migrations\/\n \u2502   \u251c\u2500 000001_init_schema.up.sql    # \ud83d\udc48 [\uc9c0\uae08 \ub9cc\ub4e4 \ud30c\uc77c: \ud14c\uc774\ube14 \uc0dd\uc131 DDL]\n \u2502   \u2514\u2500 000001_init_schema.down.sql  # [\ub864\ubc31\uc6a9 \ud30c\uc77c: \ud14c\uc774\ube14 \uc0ad\uc81c DROP]\n \u251c\u2500 queries\/                         # 5\ubc88\uc5d0\uc11c \ub9cc\ub4e4 SQL \ucffc\ub9ac\ub4e4\n \u251c\u2500 sqlc.yaml                        # 4\ubc88\uc5d0\uc11c \ub9cc\ub4e4 sqlc \uc124\uc815\n \u251c\u2500 cmd\/api\/main.go\n \u251c\u2500 Dockerfile\n \u251c\u2500 go.mod\n \u2514\u2500 go.sum<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 \ud14c\uc774\ube14 \uc0dd\uc131 \uc2a4\ud0a4\ub9c8 (<code>server\/migrations\/000001_init_schema.up.sql<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">VS Code\uc5d0\uc11c <code>server\/migrations\/<\/code> \ud3f4\ub354\ub97c \uc0dd\uc131\ud558\uace0, <strong><code>000001_init_schema.up.sql<\/code><\/strong> \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4 \uc544\ub798 DDL \ucf54\ub4dc\ub97c \uadf8\ub300\ub85c \ubd99\uc5ec\ub123\uc2b5\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- UUID \uc0dd\uc131 \ud655\uc7a5 \uae30\ub2a5 \ud65c\uc131\ud654\nCREATE EXTENSION IF NOT EXISTS \"pgcrypto\";\n\n-- 1. \uc0ac\uc6a9\uc790 (Users)\nCREATE TABLE users (\n    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    display_name    VARCHAR(50)  NOT NULL,\n    status_message  VARCHAR(100),\n    avatar_file_id  UUID,\n    phone_hash      VARCHAR(64)  UNIQUE,       -- E.164 \ubc88\ud638\uc758 SHA-256 \ud574\uc2dc\uac12\n    role            VARCHAR(10)  NOT NULL DEFAULT 'user',   -- 'admin' | 'user'\n    status          VARCHAR(10)  NOT NULL DEFAULT 'active', -- 'active' | 'suspended' | 'deleted'\n    invited_by      UUID REFERENCES users(id),\n    created_at      TIMESTAMPTZ  NOT NULL DEFAULT now(),\n    updated_at      TIMESTAMPTZ  NOT NULL DEFAULT now()\n);\nCREATE INDEX idx_users_phone_hash ON users(phone_hash);\n\n-- 2. \ub85c\uadf8\uc778 \uacc4\uc815 \uc218\ub2e8 (Auth Identities)\nCREATE TABLE auth_identities (\n    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    user_id        UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    provider       VARCHAR(10) NOT NULL,          -- 'password' | 'google' | 'apple'\n    provider_uid   VARCHAR(255) NOT NULL,         -- \ub85c\uadf8\uc778 \uc544\uc774\ub514 \ub610\ub294 \uc18c\uc15c sub\n    password_hash  TEXT,                          -- Argon2id \ud574\uc2dc (\ube44\ubc00\ubc88\ud638 \ub85c\uadf8\uc778 \uc2dc)\n    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),\n    UNIQUE (provider, provider_uid)\n);\n\n-- 3. \uae30\uae30 \uc138\uc158 \ubc0f \ud478\uc2dc \ud1a0\ud070 (Devices)\nCREATE TABLE devices (\n    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    user_id             UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    platform            VARCHAR(10) NOT NULL,     -- 'android' | 'ios' | 'macos' | 'windows'\n    name                VARCHAR(100),             -- \uc608: \"\uc544\ube60 Galaxy S24 Ultra\"\n    refresh_token_hash  TEXT NOT NULL,\n    refresh_expires_at  TIMESTAMPTZ NOT NULL,\n    fcm_token           TEXT,\n    voip_token          TEXT,                     -- iOS PushKit \ud1b5\ud654\uc6a9\n    app_version         VARCHAR(20),\n    last_active_at      TIMESTAMPTZ,\n    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()\n);\nCREATE INDEX idx_devices_user_id ON devices(user_id);\n\n-- 4. \ucd08\ub300 \ucf54\ub4dc (Invite Codes)\nCREATE TABLE invite_codes (\n    code        CHAR(6) PRIMARY KEY,\n    owner_id    UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    max_uses    INT NOT NULL DEFAULT 10,\n    used_count  INT NOT NULL DEFAULT 0,\n    expires_at  TIMESTAMPTZ NOT NULL,\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\n-- 5. \uce5c\uad6c \uad00\uacc4 (Friendships)\nCREATE TABLE friendships (\n    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    friend_id   UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    tag         VARCHAR(10) NOT NULL DEFAULT 'friend',  -- 'family' | 'friend'\n    nickname    VARCHAR(50),\n    blocked     BOOLEAN NOT NULL DEFAULT false,\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),\n    PRIMARY KEY (user_id, friend_id)\n);\n\n-- 6. \ucc44\ud305\ubc29 (Rooms)\nCREATE TABLE rooms (\n    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    type            VARCHAR(10) NOT NULL,             -- 'direct' | 'group'\n    direct_key      VARCHAR(80) UNIQUE,               -- 1:1 \ubc29 \uc911\ubcf5 \ubc29\uc9c0 \ud0a4 (\"\uc791\uc740UUID:\ud070UUID\")\n    name            VARCHAR(50),\n    avatar_file_id  UUID,\n    last_seq        BIGINT NOT NULL DEFAULT 0,        -- \ubc29 \uba54\uc2dc\uc9c0 \ucd5c\uc2e0 \uc21c\ubc88\n    created_by      UUID REFERENCES users(id),\n    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),\n    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\n-- 7. \ucc44\ud305\ubc29 \ucc38\uc5ec \uba64\ubc84 (Room Members)\nCREATE TABLE room_members (\n    room_id        UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,\n    user_id        UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    role           VARCHAR(10) NOT NULL DEFAULT 'member',  -- 'owner' | 'member'\n    last_read_seq  BIGINT NOT NULL DEFAULT 0,              -- \ub9c8\uc9c0\ub9c9\uc73c\ub85c \uc77d\uc740 \uba54\uc2dc\uc9c0 \uc21c\ubc88\n    joined_seq     BIGINT NOT NULL DEFAULT 0,              -- \uc785\uc7a5 \uc2dc\uc810 \uc21c\ubc88 (\uc774\uc804 \ub300\ud654 \uc228\uae40\uc6a9)\n    muted          BOOLEAN NOT NULL DEFAULT false,\n    pinned         BOOLEAN NOT NULL DEFAULT false,\n    joined_at      TIMESTAMPTZ NOT NULL DEFAULT now(),\n    PRIMARY KEY (room_id, user_id)\n);\nCREATE INDEX idx_room_members_user ON room_members(user_id);\n\n-- 8. \uba54\uc2dc\uc9c0 (Messages)\nCREATE TABLE messages (\n    id           UUID PRIMARY KEY,                  -- \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc0dd\uc131\ud55c \uc911\ubcf5 \ubc29\uc9c0\uc6a9 UUID v7\n    room_id      UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,\n    seq          BIGINT NOT NULL,                   -- \ubc29 \ub0b4 1\uc529 \uc99d\uac00\ud558\ub294 \ub2e8\uc870 \uc21c\ubc88\n    sender_id    UUID REFERENCES users(id),         -- \ud0c8\ud1f4 \uc2dc NULL\n    type         VARCHAR(10) NOT NULL,              -- 'text' | 'image' | 'file' | 'call_log' | 'system'\n    text         TEXT CHECK (char_length(text) &lt;= 10000),\n    attachments  JSONB,                             -- [{fileId, name, size, mime, width, height, thumbFileId}]\n    call_log     JSONB,                             -- {callId, durationSec, result}\n    deleted_at   TIMESTAMPTZ,\n    created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),\n    UNIQUE (room_id, seq)                           -- \uc99d\ubd84 \ub3d9\uae30\ud654 \ucffc\ub9ac \uace0\uc18d \uc778\ub371\uc2a4 \uacb8\uc6a9\n);\nCREATE INDEX idx_messages_room_created ON messages(room_id, created_at DESC);\n\n-- 9. \ud30c\uc77c \uc800\uc7a5\uc18c \uba54\ud0c0\ub370\uc774\ud130 (Files)\nCREATE TABLE files (\n    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    uploader_id   UUID NOT NULL REFERENCES users(id),\n    message_id    UUID REFERENCES messages(id) ON DELETE SET NULL,\n    purpose       VARCHAR(10) NOT NULL,             -- 'message' | 'avatar'\n    storage_path  TEXT NOT NULL,                    -- NAS \uc800\uc7a5 \uc0c1\ub300\uacbd\ub85c (\uc608: 2026\/10\/uuid.jpg)\n    name          VARCHAR(255) NOT NULL,\n    mime_type     VARCHAR(100),\n    size          BIGINT NOT NULL,\n    sha256        CHAR(64),\n    status        VARCHAR(10) NOT NULL DEFAULT 'uploading',  -- 'uploading' | 'ready'\n    created_at    TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\n-- 10. \ubcf4\uc774\uc2a4\ud1a1 \ud1b5\ud654 \uae30\ub85d (Calls &amp; Call Participants)\nCREATE TABLE calls (\n    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    room_id       UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,\n    caller_id     UUID NOT NULL REFERENCES users(id),\n    status        VARCHAR(10) NOT NULL,             -- 'ringing'|'accepted'|'declined'|'missed'|'ended'\n    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),\n    accepted_at   TIMESTAMPTZ,\n    ended_at      TIMESTAMPTZ\n);\n\nCREATE TABLE call_participants (\n    call_id    UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE,\n    user_id    UUID NOT NULL REFERENCES users(id),\n    joined_at  TIMESTAMPTZ,\n    left_at    TIMESTAMPTZ,\n    PRIMARY KEY (call_id, user_id)\n);<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">3.3 \ub864\ubc31\uc6a9 \uc2a4\ud0a4\ub9c8 (<code>server\/migrations\/000001_init_schema.down.sql<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ub9c8\uc774\uadf8\ub808\uc774\uc158 \ub3c4\uad6c\ub294 \uc2e4\uc218\ub97c \ub418\ub3cc\ub9b4 \ub54c\ub97c \ub300\ube44\ud574 \ud14c\uc774\ube14\uc744 \uc9c0\uc6b0\ub294 <code>.down.sql<\/code>\ub3c4 \ud544\uc694\ub85c \ud569\ub2c8\ub2e4.<br>\uc0dd\uc131 \uc21c\uc11c\uc758 \uc5ed\uc21c\uc73c\ub85c \uc678\ub798\ud0a4 \uc81c\uc57d\uc870\uac74\uc5d0 \uac78\ub9ac\uc9c0 \uc54a\uac8c \uae54\ub054\ud558\uac8c \uc9c0\uc6b0\ub294 \ucf54\ub4dc\uc785\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">DROP TABLE IF EXISTS call_participants CASCADE;\nDROP TABLE IF EXISTS calls CASCADE;\nDROP TABLE IF EXISTS files CASCADE;\nDROP TABLE IF EXISTS messages CASCADE;\nDROP TABLE IF EXISTS room_members CASCADE;\nDROP TABLE IF EXISTS rooms CASCADE;\nDROP TABLE IF EXISTS friendships CASCADE;\nDROP TABLE IF EXISTS invite_codes CASCADE;\nDROP TABLE IF EXISTS devices CASCADE;\nDROP TABLE IF EXISTS auth_identities CASCADE;\nDROP TABLE IF EXISTS users CASCADE;<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. <code>sqlc<\/code> \uc124\uce58 \ubc0f \uc124\uc815 (<code>sqlc.yaml<\/code>)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">4.1 sqlc CLI \uc124\uce58 (Mac \uae30\uc900)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ub9e5\ubd81 \ud130\ubbf8\ub110\uc5d0\uc11c Homebrew\ub85c \uac04\ud3b8\ud558\uac8c \uc124\uce58\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">brew install sqlc<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.2 <code>sqlc.yaml<\/code> \uc124\uc815 \ud30c\uc77c \uc791\uc131 (\uc2e4\uc804 \uafc0\ud301: Engine \ud544\uc218, UUID &amp; Pointer \ub9e4\ud551)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>server\/sqlc.yaml<\/code> \uacbd\ub85c\uc5d0 \uc124\uc815 \ud30c\uc77c\uc744 \uc0dd\uc131\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <strong>\ud575\uc2ec \ud301 3\uac00\uc9c0: sqlc v2 + pgx\/v5 \uc644\ubcbd \ud638\ud658 \uc124\uc815<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>engine: \"postgresql\"<\/code><\/strong>: sqlc v2\uc5d0\uc11c\ub294 \uc5d4\uc9c4 \uba85\uc2dc\uac00 \ud544\uc218\uc785\ub2c8\ub2e4. \ube60\uc9c0\uba74 <code>engine is required<\/code> \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.<\/li>\n\n\n\n<li><strong><code>emit_pointers_for_null_types: true<\/code><\/strong>: sqlc + pgx\/v5 \uc870\ud569\uc5d0\uc11c \uc774 \uc635\uc158\uc774 \uc5c6\uc73c\uba74 Nullable \uceec\ub7fc\uc774\ub098 <code>sqlc.narg<\/code> \ud30c\ub77c\ubbf8\ud130\uac00 \ub2e4\ub8e8\uae30 \ubc88\uac70\ub85c\uc6b4 <code>pgtype.Text<\/code> \uac19\uc740 \uad6c\uc870\uccb4\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4. \uc774 \uc635\uc158\uc744 \ucf1c\uba74 \uce5c\uc219\ud55c Go\uc758 \ub124\uc774\ud2f0\ube0c \ud3ec\uc778\ud130(<code>*string<\/code>, <code>*int<\/code> \ub4f1)\ub85c \uc0dd\uc131\ub418\uc5b4 \ucf54\ub4dc\uac00 \ube44\uc57d\uc801\uc73c\ub85c \uae54\ub054\ud574\uc9d1\ub2c8\ub2e4!<\/li>\n\n\n\n<li><strong>Nullable UUID\uc758 <code>pointer: true<\/code> \uac1d\uccb4 \ubb38\ubc95<\/strong>: \ubb38\uc790\uc5f4\uc5d0 <code>*github...<\/code>\ucc98\ub7fc <code>*<\/code>\ub97c \ubd99\uc774\uba74 sqlc \ud30c\uc11c\uac00 \uc798\ubabb\ub41c import \ucf54\ub4dc\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4. \ud3ec\uc778\ud130 \ud0c0\uc785\uc740 \ubc18\ub4dc\uc2dc \uc544\ub798\ucc98\ub7fc <code>import<\/code>, <code>type<\/code>, <code>pointer: true<\/code> \uac1d\uccb4 \ud615\ud0dc\ub85c \uc120\uc5b8\ud574\uc57c \uc62c\ubc14\ub978 <code>*uuid.UUID<\/code> \ucf54\ub4dc\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.<\/li>\n<\/ol>\n<\/blockquote>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">version: \"2\"\nsql:\n  - engine: \"postgresql\"\n    schema: \"migrations\"\n    queries: \"queries\"\n    gen:\n      go:\n        package: \"db\"\n        out: \"internal\/database\/db\"\n        sql_package: \"pgx\/v5\"\n        emit_json_tags: true\n        emit_prepared_queries: false\n        emit_interface: true\n        emit_empty_slices: true\n        emit_pointers_for_null_types: true\n        overrides:\n          - db_type: \"uuid\"\n            go_type: \"[github.com\/google\/uuid.UUID](https:\/\/github.com\/google\/uuid.UUID)\"\n          - db_type: \"uuid\"\n            nullable: true\n            go_type:\n              import: \"[github.com\/google\/uuid](https:\/\/github.com\/google\/uuid)\"\n              type: \"UUID\"\n              pointer: true<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. SQL \ucffc\ub9ac \uc791\uc131 \ubc0f \uc790\ub3d9 \ucf54\ub4dc \uc0dd\uc131 \uc2e4\uc2b5<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc774\uc81c <code>server\/queries\/<\/code> \ud3f4\ub354\uc5d0 \uc6b0\ub9ac\uac00 \ud544\uc694\ud55c \uc2e4\uc81c \ucffc\ub9ac\ub97c \uc791\uc131\ud574 \ubd05\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.1 \ud68c\uc6d0 \uad00\ub828 \ucffc\ub9ac (<code>server\/queries\/users.sql<\/code>) &#8211; <code>sqlc.narg<\/code>\ub85c \uc548\uc804\ud55c \uc120\ud0dd\uc801 \uc5c5\ub370\uc774\ud2b8<\/h3>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <strong>\ud575\uc2ec \ud301: <code>sqlc.narg<\/code>\ub97c \ud65c\uc6a9\ud55c Nullable \ud30c\ub77c\ubbf8\ud130 \ucc98\ub9ac<\/strong><br><code>display_name<\/code> \uceec\ub7fc\uc774 <code>NOT NULL<\/code>\uc77c \ub54c \ub2e8\uc21c\ud788 <code>COALESCE($2, display_name)<\/code>\uc744 \uc4f0\uba74 <code>sqlc<\/code>\uac00 <code>$2<\/code>\ub97c \uc77c\ubc18 <code>string<\/code>\uc73c\ub85c \ub9cc\ub4e4\uc5b4 \ubc84\ub9bd\ub2c8\ub2e4. Go\uc5d0\uc11c \ube48 \ubb38\uc790\uc5f4(<code>\"\"<\/code>)\uc740 SQL\uc758 <code>NULL<\/code>\uc774 \uc544\ub2c8\ubbc0\ub85c, \uc774\ub984\uc744 \ubc14\uafb8\uc9c0 \uc54a\uc73c\ub824\uace0 \ube48 \uac12\uc744 \ub118\uacbc\ub2e4\uac00 \uc774\ub984\uc774 \uacf5\ubc31\uc73c\ub85c \uc9c0\uc6cc\uc9c0\ub294 \ub300\ud615 \ubc84\uadf8\uac00 \uc0dd\uae41\ub2c8\ub2e4.<br><code>sqlc.narg('\uc774\ub984')<\/code>\uc744 \uc4f0\uba74 Go\uc5d0\uc11c \ud3ec\uc778\ud130(<code>*string<\/code>) \ud0c0\uc785\uc73c\ub85c \uc790\ub3d9 \uc0dd\uc131\ub418\uc5b4, <code>nil<\/code>\uc744 \ub118\uacbc\uc744 \ub54c \uc548\uc804\ud558\uac8c \uae30\uc874 \uac12\uc744 \uc720\uc9c0\ud569\ub2c8\ub2e4!<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- name: CreateUser :one\nINSERT INTO users (display_name, status_message, phone_hash, role)\nVALUES ($1, $2, $3, $4)\nRETURNING *;\n\n-- name: GetUserByID :one\nSELECT * FROM users\nWHERE id = $1 AND status != 'deleted';\n\n-- name: GetUserByPhoneHash :one\nSELECT * FROM users\nWHERE phone_hash = $1 AND status = 'active';\n\n-- name: UpdateUserProfile :one\nUPDATE users\nSET display_name = COALESCE(sqlc.narg('display_name'), display_name),\n    status_message = COALESCE(sqlc.narg('status_message'), status_message),\n    avatar_file_id = COALESCE(sqlc.narg('avatar_file_id'), avatar_file_id),\n    updated_at = now()\nWHERE id = sqlc.arg('id')\nRETURNING *;<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">5.2 \ucc44\ud305 \uba54\uc2dc\uc9c0 \uc804\uc1a1 \ucffc\ub9ac (<code>server\/queries\/messages.sql<\/code>) &#8211; CTE\ub97c \ud1b5\ud55c \uc6d0\uc790\uc131 \ud655\ubcf4<\/h3>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <strong>\ud575\uc2ec \ud301: CTE(WITH \uad6c\ubb38)\ub85c \ud2b8\ub79c\uc7ad\uc158 \uc5c6\uc774 1\ubc29\uc5d0 \uc6d0\uc790\uc801(Atomic) \ubc1c\uc1a1<\/strong><br>\uba54\uc2dc\uc9c0 \ubc1c\uc1a1 \uc2dc **&#8221;\ubc29 \ucd5c\uc2e0 \uc21c\ubc88 1 \uc99d\uac00(IncrementRoomSeq)&#8221;**\uc640 **&#8221;\uba54\uc2dc\uc9c0 \uc0bd\uc785(InsertMessage)&#8221;**\uc744 \ubcc4\ub3c4 \ucffc\ub9ac\ub85c \ub098\ub204\uba74 \ub124\ud2b8\uc6cc\ud06c \uc655\ubcf5\uc774 2\ubc88 \ubc1c\uc0dd\ud558\uace0, \uc911\uac04\uc5d0 \uc11c\ubc84\uac00 \uaebc\uc9c8 \uacbd\uc6b0 \uc21c\ubc88\ub9cc \uc62c\ub77c\uac00\uace0 \uba54\uc2dc\uc9c0\ub294 \uc720\uc2e4\ub418\ub294 \ubd88\uc77c\uce58\uac00 \uc0dd\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br>PostgreSQL\uc758 <strong>CTE(<code>WITH ...<\/code>)<\/strong> \uad6c\ubb38\uc744 \uc4f0\uba74 **\ub2e8 1\ud68c\uc758 \ucffc\ub9ac\ub85c \uc21c\ubc88 \uc99d\uac00\uc640 \uba54\uc2dc\uc9c0 \uc800\uc7a5\uc744 \uc644\ubcbd\ud55c \uc6d0\uc790\uc131(Atomic)**\uc73c\ub85c \ucc98\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4!<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- name: SendMessageWithSeq :one\nWITH next_seq AS (\n    UPDATE rooms\n    SET last_seq = last_seq + 1,\n        updated_at = now()\n    WHERE id = $1\n    RETURNING last_seq\n)\nINSERT INTO messages (\n    id,\n    room_id,\n    seq,\n    sender_id,\n    type,\n    text,\n    attachments,\n    call_log\n)\nSELECT\n    $2,\n    $1,\n    next_seq.last_seq,\n    $3,\n    $4,\n    $5,\n    $6,\n    $7\nFROM next_seq\nRETURNING *;\n\n-- name: GetMessagesAfterSeq :many\nSELECT * FROM messages\nWHERE room_id = $1 AND seq > $2\nORDER BY seq ASC\nLIMIT $3;<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">5.3 \ub9c8\ubc95\uc758 \ud55c \uc904 \uc2e4\ud589: <code>sqlc generate<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud130\ubbf8\ub110\uc5d0\uc11c <code>server\/<\/code> \ub514\ub809\ud1a0\ub9ac\ub85c \uc774\ub3d9\ud55c \ub4a4 \uc544\ub798 \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">cd server\nsqlc generate<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\uba85\ub839\uc5b4\uac00 \ub05d\ub098\uba74 <code>server\/internal\/database\/db\/<\/code> \ud3f4\ub354\uc5d0 \ub2e4\uc74c\uacfc \uac19\uc740 Go \ud30c\uc77c\ub4e4\uc774 \ub9c8\ubc95\ucc98\ub7fc \uc0dd\uc131\ub429\ub2c8\ub2e4:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>models.go<\/code>: \ubaa8\ub4e0 \ud14c\uc774\ube14 \uad6c\uc870\uccb4 (<code>UUID<\/code>\uac00 <code>uuid.UUID<\/code>\ub85c \ub9e4\ud551\ub428!)<\/li>\n\n\n\n<li><code>users.sql.go<\/code>: \ud3ec\uc778\ud130 \ud30c\ub77c\ubbf8\ud130\ub97c \ubc1b\ub294 <code>UpdateUserProfileParams<\/code> \ubc0f \uc870\ud68c \ud568\uc218\ub4e4<\/li>\n\n\n\n<li><code>messages.sql.go<\/code>: CTE\ub85c \ubb36\uc778 \uace0\uc131\ub2a5 <code>SendMessageWithSeq<\/code> \ud568\uc218<\/li>\n\n\n\n<li><code>db.go<\/code> &amp; <code>querier.go<\/code>: DB \uc778\ud130\ud398\uc774\uc2a4 \ubc0f \ud2b8\ub79c\uc7ad\uc158 \ub798\ud37c<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. Go \uc11c\ubc84\uc5d0 \uace0\uc131\ub2a5 DB \uc5f0\uacb0 \ud480 (<code>pgxpool<\/code>) \ubc0f \ud2b8\ub79c\uc7ad\uc158(WithTx) \ud5ec\ud37c \uad6c\ud604<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\ub2e8\uc77c \ucffc\ub9ac\ub294 \uc704\ucc98\ub7fc \ud574\uacb0\ud560 \uc218 \uc788\uc9c0\ub9cc, &#8220;\ubc29 \uc0dd\uc131 + \uc5ec\ub7ec \uba64\ubc84 \ucd94\uac00&#8221; \uac19\uc740 \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1\uc740 Go \ucf54\ub4dc \ucc28\uc6d0\uc5d0\uc11c \ud2b8\ub79c\uc7ad\uc158\uc774 \uaf2d \ud544\uc694\ud569\ub2c8\ub2e4.<br>\uc774\ub97c \uc704\ud574 \uae54\ub054\ud55c <strong><code>WithTx<\/code> \ud2b8\ub79c\uc7ad\uc158 \ub798\ud37c<\/strong>\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.1 \ud2b8\ub79c\uc7ad\uc158 \ub798\ud37c (<code>server\/internal\/database\/tx.go<\/code>)<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"[github.com\/jackc\/pgx\/v5](https:\/\/github.com\/jackc\/pgx\/v5)\"\n\t\"[github.com\/jackc\/pgx\/v5\/pgxpool](https:\/\/github.com\/jackc\/pgx\/v5\/pgxpool)\"\n\t\"[github.com\/happyforevergithub\/dingdong-server\/internal\/database\/db](https:\/\/github.com\/happyforevergithub\/dingdong-server\/internal\/database\/db)\"\n)\n\n\/\/ WithTx: \ud2b8\ub79c\uc7ad\uc158 \ub0b4\uc5d0\uc11c sqlc \ucffc\ub9ac\ub97c \uc548\uc804\ud558\uac8c \uc2e4\ud589\ud558\uace0 \uc131\uacf5 \uc2dc Commit, \uc2e4\ud328 \uc2dc Rollback\ud569\ub2c8\ub2e4.\nfunc WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(q *db.Queries) error) error {\n\ttx, err := pool.BeginTx(ctx, pgx.TxOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"\ud2b8\ub79c\uc7ad\uc158 \uc2dc\uc791 \uc2e4\ud328: %w\", err)\n\t}\n\n\tq := db.New(tx)\n\tif err := fn(q); err != nil {\n\t\tif rbErr := tx.Rollback(ctx); rbErr != nil {\n\t\t\treturn fmt.Errorf(\"\ub864\ubc31 \uc2e4\ud328 (%v): %w\", rbErr, err)\n\t\t}\n\t\treturn err\n\t}\n\n\tif err := tx.Commit(ctx); err != nil {\n\t\treturn fmt.Errorf(\"\ucee4\ubc0b \uc2e4\ud328: %w\", err)\n\t}\n\n\treturn nil\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6.2 \uc11c\ubc84 \uc9c4\uc785\uc810 \uc5f0\uacb0 (<code>server\/cmd\/api\/main.go<\/code>)<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"golang\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"[github.com\/jackc\/pgx\/v5\/pgxpool](https:\/\/github.com\/jackc\/pgx\/v5\/pgxpool)\"\n\t\"[github.com\/happyforevergithub\/dingdong-server\/internal\/database\/db](https:\/\/github.com\/happyforevergithub\/dingdong-server\/internal\/database\/db)\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tdbURL := os.Getenv(\"DATABASE_URL\")\n\tif dbURL == \"\" {\n\t\tdbURL = \"postgres:\/\/dingdong:dingdong_secret_2026@localhost:5432\/dingdong?sslmode=disable\"\n\t}\n\n\t\/\/ 1. \ucee4\ub125\uc158 \ud480 \uc124\uc815 (\uc800\uc0ac\uc591 NAS \ub9de\ucda4 \ud29c\ub2dd)\n\tconfig, err := pgxpool.ParseConfig(dbURL)\n\tif err != nil {\n\t\tlog.Fatalf(\"DB \uc124\uc815 \ud30c\uc2f1 \uc2e4\ud328: %v\", err)\n\t}\n\n\tconfig.MaxConns = 25                      \/\/ \ucd5c\ub300 \uc5f0\uacb0 \uc218 \uc81c\ud55c (RAM \uc808\uc57d)\n\tconfig.MinConns = 5                       \/\/ \uc720\ud734 \ucd5c\uc18c \uc5f0\uacb0 \uc720\uc9c0\n\tconfig.MaxConnIdleTime = 5 * time.Minute \/\/ \uc720\ud734 \ucee4\ub125\uc158 \ud68c\uc218 \uc2dc\uac04\n\n\t\/\/ 2. PostgreSQL \uc5f0\uacb0\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\tlog.Fatalf(\"PostgreSQL \uc5f0\uacb0 \ud480 \uc0dd\uc131 \uc2e4\ud328: %v\", err)\n\t}\n\tdefer pool.Close()\n\n\t\/\/ \uc5f0\uacb0 \uc0c1\ud0dc \ud655\uc778 (Ping)\n\tif err := pool.Ping(ctx); err != nil {\n\t\tlog.Fatalf(\"PostgreSQL Ping \uc2e4\ud328: %v\", err)\n\t}\n\tlog.Println(\"\ud83d\udc18 PostgreSQL 16 \uc5f0\uacb0 \ud480 \ucd08\uae30\ud654 \uc131\uacf5!\")\n\n\t\/\/ 3. sqlc \ucffc\ub9ac \uac1d\uccb4 \ucd08\uae30\ud654\n\tqueries := db.New(pool)\n\t_ = queries \/\/ \ub2e4\uc74c 6\ud3b8 \uc778\uc99d API\uc5d0\uc11c \ubc14\ub85c \uc8fc\uc785\ud558\uc5ec \uc0ac\uc6a9\ud569\ub2c8\ub2e4!\n}<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7. \uc774\ubc88 \ud3b8 \uc694\uc57d \ubc0f \ub2e4\uc74c \ub2e8\uacc4<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\uc624\ub298 \uad6c\ucd95\ud55c DB \ub808\uc774\uc5b4 \ub355\ubd84\uc5d0 \uc6b0\ub9ac \ub529\ub3d9 \uba54\uc2e0\uc800\ub294 \ub2e4\uc74c\uacfc \uac19\uc740 \uac15\ub825\ud55c \uae30\ubc18\uc744 \uac16\ucd94\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\ud0c0\uc785 \uc548\uc804\ud55c SQL<\/strong>: \uc624\ud0c0\ub098 \uc798\ubabb\ub41c \ud0c0\uc785 \uc804\ub2ec\ub85c \uc778\ud55c \ub7f0\ud0c0\uc784 \uc11c\ubc84 \ub2e4\uc6b4\uc744 \ucef4\ud30c\uc77c \ub2e8\uacc4\uc5d0\uc11c 100% \ucc28\ub2e8.<\/li>\n\n\n\n<li><strong>\uce74\ud1a1\uae09 \uc2e4\uc2dc\uac04\uc131<\/strong>: \ubc29\ubcc4 \ub2e8\uc870 \uc99d\uac00 <code>seq<\/code>\ub97c \ud1b5\ud574 \ub124\ud2b8\uc6cc\ud06c\uac00 \ub04a\uaca8\ub3c4 \uc624\ucc28 \uc5c6\uc774 \ub300\ud654\ub97c \ubcf5\uad6c\ud558\ub294 \uc99d\ubd84 \ub3d9\uae30\ud654 \ud1a0\ub300 \ub9c8\ub828.<\/li>\n\n\n\n<li><strong>\uadf9\uc800\uc0ac\uc591 \ucd5c\uc801\ud654<\/strong>: ORM\uc758 \ubb34\uac70\uc6b4 \uc624\ubc84\ud5e4\ub4dc\ub97c \uc644\uc804\ud788 \uac77\uc5b4\ub0b4\uc5b4 \uc720\uadf8\ub9b0 NAS\uc5d0\uc11c 30MB \uc774\ud558\uc758 RAM\uc73c\ub85c \uc218\ucc9c \uac74\uc758 \ucffc\ub9ac\ub97c \uc989\uac01 \ucc98\ub9ac.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\ub2e4\uc74c **[\uac1c\ubc1c\uc77c\uc9c0 #06]**\uc5d0\uc11c\ub294 \uc774 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc704\uc5d0 <strong>\ucd08\ub300\ucf54\ub4dc \uae30\ubc18 \ud68c\uc6d0\uac00\uc785, Argon2id \ube44\ubc00\ubc88\ud638 \uc554\ud638\ud654, JWT Access\/Refresh \ud1a0\ud070 \ubc1c\uae09 \ubc0f \ubcf4\uc548 \ubbf8\ub4e4\uc6e8\uc5b4<\/strong>\ub97c \uc644\uc131\ud574 \ubcf4\uaca0\uc2b5\ub2c8\ub2e4!<\/p>\n\n\n\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 1. \uae30\ud68d &amp; \uc544\ud0a4\ud14d\ucc98 \uc124\uacc4<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #01 | \uae30\ud68d] <a href=\"https:\/\/dingdonghome.duckdns.org\/?p=20\" data-type=\"post\" data-id=\"20\">\uc720\uadf8\ub9b0 NAS\uc640 Flutter\ub85c \ub9cc\ub4dc\ub294 \ub098\ub9cc\uc758 \ud504\ub77c\uc774\ube57 \uba54\uc2e0\uc800 &#8216;\ub529\ub3d9&#8217; (\uae30\ud68d &amp; \uc544\ud0a4\ud14d\ucc98 \ud3b8)<\/a><\/strong><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 2. \ud648\uc11c\ubc84 &amp; \uc778\ud504\ub77c \uad6c\ucd95 (UGREEN + Synology)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #02 | \uc778\ud504\ub77c] <a href=\"https:\/\/dingdonghome.duckdns.org\/?p=52\" data-type=\"post\" data-id=\"52\">Mac\uc5d0\uc11c Flutter &amp; VS Code \uac1c\ubc1c \ud658\uacbd \uad6c\ucd95\ud558\uae30 (\ucd08\ubcf4\uc790 \uc644\ubcbd \uac00\uc774\ub4dc)<\/a><\/strong><\/li>\n\n\n\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #03 | \uc778\ud504\ub77c] <a href=\"https:\/\/dingdonghome.duckdns.org\/?p=68\" data-type=\"post\" data-id=\"68\">\uc720\uadf8\ub9b0 NAS\uc5d0 Caddy &amp; PostgreSQL \uad6c\ucd95\ud558\uae30 (\ud648\uc11c\ubc84 443 \ud3ec\ud2b8 \ucda9\ub3cc \ud574\uacb0 \uafc0\ud301 \ud3ec\ud568)<\/a><\/strong><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 3. \ubc31\uc5d4\ub4dc(API) &amp; \uc2e4\uc2dc\uac04 \ud1b5\ud654 \uc11c\ubc84<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2705 <strong>[\uac1c\ubc1c\uc77c\uc9c0 #04 | \ubc31\uc5d4\ub4dc] <a href=\"https:\/\/dingdonghome.duckdns.org\/?p=117\" data-type=\"post\" data-id=\"117\">\uc720\uadf8\ub9b0 NAS\uc5d0 Go(Golang) \ucd08\uacbd\ub7c9 \uba54\uc2e0\uc800 \ubc31\uc5d4\ub4dc \uc11c\ubc84 \uad6c\ucd95\ud558\uae30<\/a><\/strong><\/li>\n\n\n\n<li>\u23f3 <strong>[#05 | \ubc31\uc5d4\ub4dc]<\/strong> JWT \uae30\ubc18 \uac00\uc871 \uc778\uc99d \ubc0f \uc2e4\uc2dc\uac04 WebSocket \ucc44\ud305 \uad6c\ud604 <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#06 | \ud1b5\ud654]<\/strong> LiveKit \uc5f0\ub3d9\uc73c\ub85c \ubb34\ub8cc \uc74c\uc131\/\uc601\uc0c1 \ud1b5\ud654 \uc11c\ubc84 \uad6c\ucd95\ud558\uae30 <em>(\uc608\uc815)<\/em><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 4. \ubaa8\ubc14\uc77c \uc571 \uac1c\ubc1c (iOS \/ Android)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u23f3 <strong>[#07 | \uc571-\uacf5\ud1b5]<\/strong> \ubaa8\ubc14\uc77c \uc571 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 \ubc0f \ub85c\uadf8\uc778\/\ucc44\ud305\ubc29 UI \uad6c\ud604 <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#08 | \uc571-\uacf5\ud1b5]<\/strong> 300MB \ub300\uc6a9\ub7c9 \uc6d0\ubcf8 \uc0ac\uc9c4\/\uc601\uc0c1 \uc804\uc1a1 \ubc0f \ubbf8\ub514\uc5b4 \ucc98\ub9ac <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#09 | \uc571-\ud478\uc2dc]<\/strong> \uc571\uc774 \uaebc\uc838 \uc788\uc5b4\ub3c4 \uc6b8\ub9ac\ub294 \ud478\uc2dc \uc54c\ub9bc(FCM\/APNs) \uc5f0\ub3d9 <em>(\uc608\uc815)<\/em><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc2 Part 5. \uc815\uc2dd \ubc30\ud3ec &amp; \uc6b4\uc601<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u23f3 <strong>[#10 | \ubc30\ud3ec]<\/strong> \ubc31\uc5d4\ub4dc \uc11c\ubc84 \ub3c4\ucee4 \uc774\ubbf8\uc9c0 \ube4c\ub4dc \ubc0f \uc720\uadf8\ub9b0 NAS \uc815\uc2dd \ubc30\ud3ec <em>(\uc608\uc815)<\/em><\/li>\n\n\n\n<li>\u23f3 <strong>[#11 | \uc6b4\uc601]<\/strong> DB \uc790\ub3d9 \ubc31\uc5c5 \uc2a4\ud06c\ub9bd\ud2b8 \uad6c\uc131 \ubc0f \uac00\uc871 \uc2e4\uc0ac\uc6a9 \ud6c4\uae30 <em>(\uc608\uc815)<\/em><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n<\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>\uc548\ub155\ud558\uc138\uc694! \uce74\uce74\uc624\ud1a1 \uc2a4\ud0c0\uc77c\uc758 \uac00\uc871\/\uce5c\uad6c\uc6a9 \ud504\ub77c\uc774\ube57 \ud06c\ub85c\uc2a4\ud50c\ub7ab\ud3fc \uba54\uc2e0\uc800 &#8216;\ub529\ub3d9(DingDong)&#8217; \ub9cc\ub4e4\uae30 \ud504\ub85c\uc81d\ud2b8 5\ud3b8\uc785\ub2c8\ub2e4. \uc9c0\ub09c [4\ud3b8]\uc5d0\uc11c\ub294 \uc720\uadf8\ub9b0 NAS\uc758 \uc800\uc0ac\uc591 \ud558\ub4dc\uc6e8\uc5b4\uc5d0\uc11c\ub3c4 \uae43\ud138\ucc98\ub7fc \uac00\ubccd\uac8c \ub3cc\uc544\uac00\ub294 **Go \ubc31\uc5d4\ub4dc API \uc5d4\uc9c4(chi \ub77c\uc6b0\ud130, Graceful Shutdown, 25MB \ucd08\uacbd\ub7c9 Dockerfile, Redis \uc5f0\ub3d9)**\uc744 \uc138\ud305\ud588\uc2b5\ub2c8\ub2e4. \uc774\ubc88 5\ud3b8\uc5d0\uc11c\ub294 \uba54\uc2e0\uc800 \ubc31\uc5d4\ub4dc\uc758 \uc2ec\uc7a5\uc778 **\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc2a4\ud0a4\ub9c8(\ud14c\uc774\ube14 \uad6c\uc870)**\ub97c \uc644\ubcbd\ud558\uac8c \uc124\uacc4\ud558\uace0, Go \uc9c4\uc601\uc5d0\uc11c \uac00\uc7a5 \uc6b0\uc544\ud558\uace0 \uac15\ub825\ud55c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ud234\uc778 **sqlc**\ub97c \uc5f0\ub3d9\ud558\ub294 \uacfc\uc815\uc744 \ub2e4\ub8f9\ub2c8\ub2e4. 1. &#8230; <a title=\"[\uac1c\ubc1c\uc77c\uc9c0 #05] \uba54\uc2e0\uc800 DB \uc2a4\ud0a4\ub9c8 \uc124\uacc4\uc640 Go\uc758 \ucd5c\uac15 \uc870\ud569 sqlc \uc5f0\ub3d9\ud558\uae30 (PostgreSQL + sqlc)\" class=\"read-more\" href=\"https:\/\/dinggo.duckdns.org\/?p=136\" aria-label=\"[\uac1c\ubc1c\uc77c\uc9c0 #05] \uba54\uc2e0\uc800 DB \uc2a4\ud0a4\ub9c8 \uc124\uacc4\uc640 Go\uc758 \ucd5c\uac15 \uc870\ud569 sqlc \uc5f0\ub3d9\ud558\uae30 (PostgreSQL + sqlc)\uc5d0 \ub300\ud574 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uc138\uc694\">\ub354 \uc77d\uae30<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[7],"class_list":["post-136","post","type-post","status-publish","format-standard","hentry","category-dingdong-","tag-7"],"_links":{"self":[{"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=\/wp\/v2\/posts\/136","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=136"}],"version-history":[{"count":3,"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=\/wp\/v2\/posts\/136\/revisions"}],"predecessor-version":[{"id":140,"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=\/wp\/v2\/posts\/136\/revisions\/140"}],"wp:attachment":[{"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=136"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=136"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dinggo.duckdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=136"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}