[개발일지 #05] 메신저 DB 스키마 설계와 Go의 최강 조합 sqlc 연동하기 (PostgreSQL + sqlc)

안녕하세요! 카카오톡 스타일의 가족/친구용 프라이빗 크로스플랫폼 메신저 ‘딩동(DingDong)’ 만들기 프로젝트 5편입니다.

지난 [4편]에서는 유그린 NAS의 저사양 하드웨어에서도 깃털처럼 가볍게 돌아가는 **Go 백엔드 API 엔진(chi 라우터, Graceful Shutdown, 25MB 초경량 Dockerfile, Redis 연동)**을 세팅했습니다.

이번 5편에서는 메신저 백엔드의 심장인 **데이터베이스 스키마(테이블 구조)**를 완벽하게 설계하고, Go 진영에서 가장 우아하고 강력한 데이터베이스 툴인 **sqlc**를 연동하는 과정을 다룹니다.


1. 메신저 데이터베이스 설계 시 반드시 고려해야 할 4가지

단순한 게시판이나 쇼핑몰과 달리, 카카오톡 스타일의 실시간 메신저는 데이터베이스 설계 시 매우 까다로운 요구사항들이 있습니다:

flowchart TD
    A["메신저 DB 핵심 설계 원칙"] --> B["1. 방별 단조 증가 순번(seq)"]
    A --> C["2. 1:1 채팅방 중복 생성 방지"]
    A --> D["3. 개인정보 보호 연락처 해시 매칭"]
    A --> E["4. 다중 기기 세션 및 읽음 확인"]

    linkStyle default stroke-width:3px,stroke:#333333
  1. 방별 단조 증가 순번 (seq):
    • 시간(Timestamp)으로만 메시지를 정렬하면 기기 간 시계 오차나 1밀리초 내 다중 메시지 발생 시 순서가 뒤틀립니다.
    • 방마다 1, 2, 3… 순번(seq)을 원자적(Atomic)으로 부여해야 클라이언트가 끊겼다 다시 연결되었을 때 **”5번 이후의 메시지만 주세요!”**라며 누락 없이 완벽한 증분 동기화를 할 수 있습니다.
  2. 1:1 채팅방 중복 생성 방지 (direct_key):
    • A가 B에게 대화를 걸고, B도 A에게 대화를 걸 때 서로 다른 1:1 방이 중복으로 생기면 안 됩니다.
    • 두 유저의 UUID를 오름차순으로 정렬해 유니크 키(작은UUID:큰UUID)로 강제하여 DB 레벨에서 중복을 원천 차단합니다.
  3. 전화번호 SHA-256 해시 매칭 (phone_hash):
    • 개인정보 보호를 위해 평문 전화번호는 DB에 남기지 않고, E.164 국제 표준 전화번호 규격의 SHA-256 해시값만 저장하여 주소록 동기화 시 친구를 안전하게 매칭합니다.
  4. 다중 기기(모바일/태블릿/PC) 세션 관리:
    • 한 사용자가 스마트폰, 아이패드, 맥북에서 동시 접속할 수 있으므로 기기별(devices) 세션과 개별 푸시 토큰(FCM/APNs)을 독립적으로 관리합니다.

2. 왜 무거운 ORM 대신 sqlc를 선택했을까?

Node.js나 Python에서는 TypeORM, Prisma, SQLAlchemy 같은 무거운 ORM을 흔히 사용합니다. 하지만 Go 언어에서는 ORM보다 **sqlc**가 압도적인 찬사를 받고 있습니다.

비교 항목전통적인 무거운 ORM (GORM 등)Go 진영의 표준 sqlc
동작 방식런타임에 리플렉션(Reflection)으로 SQL 동적 생성컴파일 타임에 순수 Go 코드를 미리 생성
메모리 및 CPU객체 매핑 오버헤드로 메모리 소모 큼순수 드라이버 수준의 극한의 속도와 초경량 RAM
타입 안정성런타임에 컬럼명 오타 에러 발생 가능SQL 문법 검사 + Go 구조체 타입 100% 보장
SQL 학습 비용프레임워크 전용 DSL 문법을 새로 배워야 함개발자가 원래 알던 표준 PostgreSQL SQL 그대로 작성

💡 sqlc의 마법: 개발자가 .sql 파일에 순수한 PostgreSQL 쿼리를 작성하고 터미널에서 sqlc generate를 치면, sqlc가 PostgreSQL 문법을 파싱하여 Go 구조체와 함수 코드를 1밀리초 만에 자동으로 만들어 줍니다!


3. 전체 데이터베이스 스키마 (DDL) 및 파일 생성

3.1 파일 경로 및 폴더 구조 안내

마이그레이션 파일은 server/ 디렉토리 아래에 migrations/ 폴더를 만들어 관리합니다.
DB 도구(golang-migrate, sqlc)가 인식할 수 있도록 파일 이름 뒤에 .up.sql(적용용)과 .down.sql(롤백용)을 붙여주는 것이 표준 규칙입니다.

server/
 ├─ migrations/
 │   ├─ 000001_init_schema.up.sql    # 👈 [지금 만들 파일: 테이블 생성 DDL]
 │   └─ 000001_init_schema.down.sql  # [롤백용 파일: 테이블 삭제 DROP]
 ├─ queries/                         # 5번에서 만들 SQL 쿼리들
 ├─ sqlc.yaml                        # 4번에서 만들 sqlc 설정
 ├─ cmd/api/main.go
 ├─ Dockerfile
 ├─ go.mod
 └─ go.sum

3.2 테이블 생성 스키마 (server/migrations/000001_init_schema.up.sql)

VS Code에서 server/migrations/ 폴더를 생성하고, 000001_init_schema.up.sql 파일을 만들어 아래 DDL 코드를 그대로 붙여넣습니다:

-- UUID 생성 확장 기능 활성화
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- 1. 사용자 (Users)
CREATE TABLE users (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    display_name    VARCHAR(50)  NOT NULL,
    status_message  VARCHAR(100),
    avatar_file_id  UUID,
    phone_hash      VARCHAR(64)  UNIQUE,       -- E.164 번호의 SHA-256 해시값
    role            VARCHAR(10)  NOT NULL DEFAULT 'user',   -- 'admin' | 'user'
    status          VARCHAR(10)  NOT NULL DEFAULT 'active', -- 'active' | 'suspended' | 'deleted'
    invited_by      UUID REFERENCES users(id),
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ  NOT NULL DEFAULT now()
);
CREATE INDEX idx_users_phone_hash ON users(phone_hash);

-- 2. 로그인 계정 수단 (Auth Identities)
CREATE TABLE auth_identities (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id        UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    provider       VARCHAR(10) NOT NULL,          -- 'password' | 'google' | 'apple'
    provider_uid   VARCHAR(255) NOT NULL,         -- 로그인 아이디 또는 소셜 sub
    password_hash  TEXT,                          -- Argon2id 해시 (비밀번호 로그인 시)
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (provider, provider_uid)
);

-- 3. 기기 세션 및 푸시 토큰 (Devices)
CREATE TABLE devices (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id             UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    platform            VARCHAR(10) NOT NULL,     -- 'android' | 'ios' | 'macos' | 'windows'
    name                VARCHAR(100),             -- 예: "아빠 Galaxy S24 Ultra"
    refresh_token_hash  TEXT NOT NULL,
    refresh_expires_at  TIMESTAMPTZ NOT NULL,
    fcm_token           TEXT,
    voip_token          TEXT,                     -- iOS PushKit 통화용
    app_version         VARCHAR(20),
    last_active_at      TIMESTAMPTZ,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_devices_user_id ON devices(user_id);

-- 4. 초대 코드 (Invite Codes)
CREATE TABLE invite_codes (
    code        CHAR(6) PRIMARY KEY,
    owner_id    UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    max_uses    INT NOT NULL DEFAULT 10,
    used_count  INT NOT NULL DEFAULT 0,
    expires_at  TIMESTAMPTZ NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 5. 친구 관계 (Friendships)
CREATE TABLE friendships (
    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    friend_id   UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    tag         VARCHAR(10) NOT NULL DEFAULT 'friend',  -- 'family' | 'friend'
    nickname    VARCHAR(50),
    blocked     BOOLEAN NOT NULL DEFAULT false,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (user_id, friend_id)
);

-- 6. 채팅방 (Rooms)
CREATE TABLE rooms (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    type            VARCHAR(10) NOT NULL,             -- 'direct' | 'group'
    direct_key      VARCHAR(80) UNIQUE,               -- 1:1 방 중복 방지 키 ("작은UUID:큰UUID")
    name            VARCHAR(50),
    avatar_file_id  UUID,
    last_seq        BIGINT NOT NULL DEFAULT 0,        -- 방 메시지 최신 순번
    created_by      UUID REFERENCES users(id),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 7. 채팅방 참여 멤버 (Room Members)
CREATE TABLE room_members (
    room_id        UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
    user_id        UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role           VARCHAR(10) NOT NULL DEFAULT 'member',  -- 'owner' | 'member'
    last_read_seq  BIGINT NOT NULL DEFAULT 0,              -- 마지막으로 읽은 메시지 순번
    joined_seq     BIGINT NOT NULL DEFAULT 0,              -- 입장 시점 순번 (이전 대화 숨김용)
    muted          BOOLEAN NOT NULL DEFAULT false,
    pinned         BOOLEAN NOT NULL DEFAULT false,
    joined_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (room_id, user_id)
);
CREATE INDEX idx_room_members_user ON room_members(user_id);

-- 8. 메시지 (Messages)
CREATE TABLE messages (
    id           UUID PRIMARY KEY,                  -- 클라이언트가 생성한 중복 방지용 UUID v7
    room_id      UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
    seq          BIGINT NOT NULL,                   -- 방 내 1씩 증가하는 단조 순번
    sender_id    UUID REFERENCES users(id),         -- 탈퇴 시 NULL
    type         VARCHAR(10) NOT NULL,              -- 'text' | 'image' | 'file' | 'call_log' | 'system'
    text         TEXT CHECK (char_length(text) <= 10000),
    attachments  JSONB,                             -- [{fileId, name, size, mime, width, height, thumbFileId}]
    call_log     JSONB,                             -- {callId, durationSec, result}
    deleted_at   TIMESTAMPTZ,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (room_id, seq)                           -- 증분 동기화 쿼리 고속 인덱스 겸용
);
CREATE INDEX idx_messages_room_created ON messages(room_id, created_at DESC);

-- 9. 파일 저장소 메타데이터 (Files)
CREATE TABLE files (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    uploader_id   UUID NOT NULL REFERENCES users(id),
    message_id    UUID REFERENCES messages(id) ON DELETE SET NULL,
    purpose       VARCHAR(10) NOT NULL,             -- 'message' | 'avatar'
    storage_path  TEXT NOT NULL,                    -- NAS 저장 상대경로 (예: 2026/10/uuid.jpg)
    name          VARCHAR(255) NOT NULL,
    mime_type     VARCHAR(100),
    size          BIGINT NOT NULL,
    sha256        CHAR(64),
    status        VARCHAR(10) NOT NULL DEFAULT 'uploading',  -- 'uploading' | 'ready'
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 10. 보이스톡 통화 기록 (Calls & Call Participants)
CREATE TABLE calls (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    room_id       UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
    caller_id     UUID NOT NULL REFERENCES users(id),
    status        VARCHAR(10) NOT NULL,             -- 'ringing'|'accepted'|'declined'|'missed'|'ended'
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    accepted_at   TIMESTAMPTZ,
    ended_at      TIMESTAMPTZ
);

CREATE TABLE call_participants (
    call_id    UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE,
    user_id    UUID NOT NULL REFERENCES users(id),
    joined_at  TIMESTAMPTZ,
    left_at    TIMESTAMPTZ,
    PRIMARY KEY (call_id, user_id)
);

3.3 롤백용 스키마 (server/migrations/000001_init_schema.down.sql)

마이그레이션 도구는 실수를 되돌릴 때를 대비해 테이블을 지우는 .down.sql도 필요로 합니다.
생성 순서의 역순으로 외래키 제약조건에 걸리지 않게 깔끔하게 지우는 코드입니다:

DROP TABLE IF EXISTS call_participants CASCADE;
DROP TABLE IF EXISTS calls CASCADE;
DROP TABLE IF EXISTS files CASCADE;
DROP TABLE IF EXISTS messages CASCADE;
DROP TABLE IF EXISTS room_members CASCADE;
DROP TABLE IF EXISTS rooms CASCADE;
DROP TABLE IF EXISTS friendships CASCADE;
DROP TABLE IF EXISTS invite_codes CASCADE;
DROP TABLE IF EXISTS devices CASCADE;
DROP TABLE IF EXISTS auth_identities CASCADE;
DROP TABLE IF EXISTS users CASCADE;

4. sqlc 설치 및 설정 (sqlc.yaml)

4.1 sqlc CLI 설치 (Mac 기준)

맥북 터미널에서 Homebrew로 간편하게 설치할 수 있습니다:

brew install sqlc

4.2 sqlc.yaml 설정 파일 작성 (실전 꿀팁: Engine 필수, UUID & Pointer 매핑)

server/sqlc.yaml 경로에 설정 파일을 생성합니다.

💡 핵심 팁 3가지: sqlc v2 + pgx/v5 완벽 호환 설정

  1. engine: "postgresql": sqlc v2에서는 엔진 명시가 필수입니다. 빠지면 engine is required 에러가 발생합니다.
  2. emit_pointers_for_null_types: true: sqlc + pgx/v5 조합에서 이 옵션이 없으면 Nullable 컬럼이나 sqlc.narg 파라미터가 다루기 번거로운 pgtype.Text 같은 구조체로 생성됩니다. 이 옵션을 켜면 친숙한 Go의 네이티브 포인터(*string, *int 등)로 생성되어 코드가 비약적으로 깔끔해집니다!
  3. Nullable UUID의 pointer: true 객체 문법: 문자열에 *github...처럼 *를 붙이면 sqlc 파서가 잘못된 import 코드를 생성합니다. 포인터 타입은 반드시 아래처럼 import, type, pointer: true 객체 형태로 선언해야 올바른 *uuid.UUID 코드가 생성됩니다.
version: "2"
sql:
  - engine: "postgresql"
    schema: "migrations"
    queries: "queries"
    gen:
      go:
        package: "db"
        out: "internal/database/db"
        sql_package: "pgx/v5"
        emit_json_tags: true
        emit_prepared_queries: false
        emit_interface: true
        emit_empty_slices: true
        emit_pointers_for_null_types: true
        overrides:
          - db_type: "uuid"
            go_type: "[github.com/google/uuid.UUID](https://github.com/google/uuid.UUID)"
          - db_type: "uuid"
            nullable: true
            go_type:
              import: "[github.com/google/uuid](https://github.com/google/uuid)"
              type: "UUID"
              pointer: true

5. SQL 쿼리 작성 및 자동 코드 생성 실습

이제 server/queries/ 폴더에 우리가 필요한 실제 쿼리를 작성해 봅니다.

5.1 회원 관련 쿼리 (server/queries/users.sql) – sqlc.narg로 안전한 선택적 업데이트

💡 핵심 팁: sqlc.narg를 활용한 Nullable 파라미터 처리
display_name 컬럼이 NOT NULL일 때 단순히 COALESCE($2, display_name)을 쓰면 sqlc가 $2를 일반 string으로 만들어 버립니다. Go에서 빈 문자열("")은 SQL의 NULL이 아니므로, 이름을 바꾸지 않으려고 빈 값을 넘겼다가 이름이 공백으로 지워지는 대형 버그가 생깁니다.
sqlc.narg('이름')을 쓰면 Go에서 포인터(*string) 타입으로 자동 생성되어, nil을 넘겼을 때 안전하게 기존 값을 유지합니다!

-- name: CreateUser :one
INSERT INTO users (display_name, status_message, phone_hash, role)
VALUES ($1, $2, $3, $4)
RETURNING *;

-- name: GetUserByID :one
SELECT * FROM users
WHERE id = $1 AND status != 'deleted';

-- name: GetUserByPhoneHash :one
SELECT * FROM users
WHERE phone_hash = $1 AND status = 'active';

-- name: UpdateUserProfile :one
UPDATE users
SET display_name = COALESCE(sqlc.narg('display_name'), display_name),
    status_message = COALESCE(sqlc.narg('status_message'), status_message),
    avatar_file_id = COALESCE(sqlc.narg('avatar_file_id'), avatar_file_id),
    updated_at = now()
WHERE id = sqlc.arg('id')
RETURNING *;

5.2 채팅 메시지 전송 쿼리 (server/queries/messages.sql) – CTE를 통한 원자성 확보

💡 핵심 팁: CTE(WITH 구문)로 트랜잭션 없이 1방에 원자적(Atomic) 발송
메시지 발송 시 **”방 최신 순번 1 증가(IncrementRoomSeq)”**와 **”메시지 삽입(InsertMessage)”**을 별도 쿼리로 나누면 네트워크 왕복이 2번 발생하고, 중간에 서버가 꺼질 경우 순번만 올라가고 메시지는 유실되는 불일치가 생길 수 있습니다.
PostgreSQL의 CTE(WITH ...) 구문을 쓰면 **단 1회의 쿼리로 순번 증가와 메시지 저장을 완벽한 원자성(Atomic)**으로 처리할 수 있습니다!

-- name: SendMessageWithSeq :one
WITH next_seq AS (
    UPDATE rooms
    SET last_seq = last_seq + 1,
        updated_at = now()
    WHERE id = $1
    RETURNING last_seq
)
INSERT INTO messages (
    id,
    room_id,
    seq,
    sender_id,
    type,
    text,
    attachments,
    call_log
)
SELECT
    $2,
    $1,
    next_seq.last_seq,
    $3,
    $4,
    $5,
    $6,
    $7
FROM next_seq
RETURNING *;

-- name: GetMessagesAfterSeq :many
SELECT * FROM messages
WHERE room_id = $1 AND seq > $2
ORDER BY seq ASC
LIMIT $3;

5.3 마법의 한 줄 실행: sqlc generate

터미널에서 server/ 디렉토리로 이동한 뒤 아래 명령어를 실행합니다:

cd server
sqlc generate

명령어가 끝나면 server/internal/database/db/ 폴더에 다음과 같은 Go 파일들이 마법처럼 생성됩니다:

  • models.go: 모든 테이블 구조체 (UUID가 uuid.UUID로 매핑됨!)
  • users.sql.go: 포인터 파라미터를 받는 UpdateUserProfileParams 및 조회 함수들
  • messages.sql.go: CTE로 묶인 고성능 SendMessageWithSeq 함수
  • db.go & querier.go: DB 인터페이스 및 트랜잭션 래퍼

6. Go 서버에 고성능 DB 연결 풀 (pgxpool) 및 트랜잭션(WithTx) 헬퍼 구현

단일 쿼리는 위처럼 해결할 수 있지만, “방 생성 + 여러 멤버 추가” 같은 비즈니스 로직은 Go 코드 차원에서 트랜잭션이 꼭 필요합니다.
이를 위해 깔끔한 WithTx 트랜잭션 래퍼를 구성합니다.

6.1 트랜잭션 래퍼 (server/internal/database/tx.go)

package database

import (
	"context"
	"fmt"

	"[github.com/jackc/pgx/v5](https://github.com/jackc/pgx/v5)"
	"[github.com/jackc/pgx/v5/pgxpool](https://github.com/jackc/pgx/v5/pgxpool)"
	"[github.com/happyforevergithub/dingdong-server/internal/database/db](https://github.com/happyforevergithub/dingdong-server/internal/database/db)"
)

// WithTx: 트랜잭션 내에서 sqlc 쿼리를 안전하게 실행하고 성공 시 Commit, 실패 시 Rollback합니다.
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(q *db.Queries) error) error {
	tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return fmt.Errorf("트랜잭션 시작 실패: %w", err)
	}

	q := db.New(tx)
	if err := fn(q); err != nil {
		if rbErr := tx.Rollback(ctx); rbErr != nil {
			return fmt.Errorf("롤백 실패 (%v): %w", rbErr, err)
		}
		return err
	}

	if err := tx.Commit(ctx); err != nil {
		return fmt.Errorf("커밋 실패: %w", err)
	}

	return nil
}

6.2 서버 진입점 연결 (server/cmd/api/main.go)

package main

import (
	"context"
	"log"
	"os"
	"time"

	"[github.com/jackc/pgx/v5/pgxpool](https://github.com/jackc/pgx/v5/pgxpool)"
	"[github.com/happyforevergithub/dingdong-server/internal/database/db](https://github.com/happyforevergithub/dingdong-server/internal/database/db)"
)

func main() {
	ctx := context.Background()
	dbURL := os.Getenv("DATABASE_URL")
	if dbURL == "" {
		dbURL = "postgres://dingdong:dingdong_secret_2026@localhost:5432/dingdong?sslmode=disable"
	}

	// 1. 커넥션 풀 설정 (저사양 NAS 맞춤 튜닝)
	config, err := pgxpool.ParseConfig(dbURL)
	if err != nil {
		log.Fatalf("DB 설정 파싱 실패: %v", err)
	}

	config.MaxConns = 25                      // 최대 연결 수 제한 (RAM 절약)
	config.MinConns = 5                       // 유휴 최소 연결 유지
	config.MaxConnIdleTime = 5 * time.Minute // 유휴 커넥션 회수 시간

	// 2. PostgreSQL 연결
	pool, err := pgxpool.NewWithConfig(ctx, config)
	if err != nil {
		log.Fatalf("PostgreSQL 연결 풀 생성 실패: %v", err)
	}
	defer pool.Close()

	// 연결 상태 확인 (Ping)
	if err := pool.Ping(ctx); err != nil {
		log.Fatalf("PostgreSQL Ping 실패: %v", err)
	}
	log.Println("🐘 PostgreSQL 16 연결 풀 초기화 성공!")

	// 3. sqlc 쿼리 객체 초기화
	queries := db.New(pool)
	_ = queries // 다음 6편 인증 API에서 바로 주입하여 사용합니다!
}

7. 이번 편 요약 및 다음 단계

오늘 구축한 DB 레이어 덕분에 우리 딩동 메신저는 다음과 같은 강력한 기반을 갖추게 되었습니다:

  1. 타입 안전한 SQL: 오타나 잘못된 타입 전달로 인한 런타임 서버 다운을 컴파일 단계에서 100% 차단.
  2. 카톡급 실시간성: 방별 단조 증가 seq를 통해 네트워크가 끊겨도 오차 없이 대화를 복구하는 증분 동기화 토대 마련.
  3. 극저사양 최적화: ORM의 무거운 오버헤드를 완전히 걷어내어 유그린 NAS에서 30MB 이하의 RAM으로 수천 건의 쿼리를 즉각 처리.

다음 **[개발일지 #06]**에서는 이 데이터베이스 위에 초대코드 기반 회원가입, Argon2id 비밀번호 암호화, JWT Access/Refresh 토큰 발급 및 보안 미들웨어를 완성해 보겠습니다!

📂 Part 1. 기획 & 아키텍처 설계

📂 Part 2. 홈서버 & 인프라 구축 (UGREEN + Synology)

📂 Part 3. 백엔드(API) & 실시간 통화 서버

📂 Part 4. 모바일 앱 개발 (iOS / Android)

  • ⏳ [#07 | 앱-공통] 모바일 앱 프로젝트 생성 및 로그인/채팅방 UI 구현 (예정)
  • ⏳ [#08 | 앱-공통] 300MB 대용량 원본 사진/영상 전송 및 미디어 처리 (예정)
  • ⏳ [#09 | 앱-푸시] 앱이 꺼져 있어도 울리는 푸시 알림(FCM/APNs) 연동 (예정)

📂 Part 5. 정식 배포 & 운영

  • ⏳ [#10 | 배포] 백엔드 서버 도커 이미지 빌드 및 유그린 NAS 정식 배포 (예정)
  • ⏳ [#11 | 운영] DB 자동 백업 스크립트 구성 및 가족 실사용 후기 (예정)

댓글 남기기