카카오톡 같은 크로스플랫폼 메신저 ‘딩동(DingDong)’ 만들기 프로젝트 4탄!
이번 글에서는 우리 집 유그린 NAS의 제한된 하드웨어 자원(RAM, CPU)을 200% 활용하기 위해, 메모리를 30MB도 쓰지 않는 초경량 고성능 Go(Golang) 백엔드 서버 뼈대를 구축하고 유그린 NAS 도커 환경에 컨테이너로 올리는 과정을 다룹니다.
1. 왜 Node.js나 Python 대신 ‘Go(Golang)’를 선택했을까요?
개인용 NAS나 홈서버에서 24시간 백엔드 서버를 띄울 때 가장 신경 쓰이는 것은 메모리(RAM) 점유율입니다.
- 극도의 저전력 & 초경량 메모리:
- Node.js나 Spring, Python 등은 런타임과 가비지 컬렉터 기본 상주 메모리 때문에 기본 150MB~500MB를 차지합니다.
- 반면 Go는 단일 기계어 바이너리로 컴파일되어 20~50MB 수준의 메모리만 사용합니다. 4GB 저사양 NAS에서도 부담이 전혀 없습니다.
- 동시성 처리의 끝판왕 (고루틴, Goroutine):
- 메신저는 수많은 사용자가 동시에 WebSocket 연결을 유지해야 합니다. Go의 경량 스레드인 고루틴은 수만 개의 실시간 연결도 CPU 낭비 없이 매끄럽게 처리합니다.
- 초소형 Docker 배포:
- 의존성 라이브러리가 모두 실행 파일 하나에 압축되어 들어가므로, Docker
scratch또는alpine베이스를 사용하면 도커 이미지 크기가 고작 20~30MB에 불과합니다.
- 의존성 라이브러리가 모두 실행 파일 하나에 압축되어 들어가므로, Docker
2. Go 백엔드 핵심 아키텍처 & 기술 스택
실제 메신저 운영에 필요한 핵심 부품들을 엄선했습니다.
flowchart LR
A["스마트폰 앱 (Flutter)"] -->|REST / WebSocket| B["Chi 라우터 & Gorilla WS"]
B --> C["비즈니스 로직 계층 (Service)"]
C -->|Type-safe 쿼리| D["sqlc & pgx 드라이버"]
D --> E[(PostgreSQL 16 DB)]
C -->|비동기 작업 큐| F["Asynq (Redis)"]
F -->|푸시 발송| G["FCM / APNs"]
C -->|토큰 발급| H["LiveKit Go SDK"]
linkStyle default stroke-width:3px,stroke:#333333- HTTP 라우터:
go-chi/chi(Go 표준net/http와 100% 호환되는 가장 가볍고 빠른 라우터) - 실시간 소켓:
gorilla/websocket(채팅방 브로드캐스트 및 실시간 이벤트 중계) - DB 쿼리/생성기:
sqlc(순수 SQL을 작성하면 타입 안전한 Go 코드를 자동 생성, ORM 오버헤드 0) - DB 마이그레이션:
golang-migrate(SQL 버전 파일로 테이블 자동 생성/관리) - 비동기 작업 큐:
asynq+ Redis (FCM 푸시 알림 비동기 발송 및 45초 부재중 전화 체크)
3. Mac 개발 환경에 Go 및 도구 설치하기
내 컴퓨터(Mac)에서 Go 코드를 작성하고 빌드할 수 있도록 기본 도구를 설치합니다.
3.1 Go 컴파일러 설치
터미널을 열고 Homebrew로 Go 최신 버전을 설치합니다:
# Go 설치 brew install go # 설치 확인 (go1.23 이상 권장) go version
3.2 SQL 코드 생성기(sqlc) 및 마이그레이션 도구 설치
# SQL 코드를 Go 코드로 변환해주는 sqlc 설치 brew install sqlc # DB 마이그레이션 도구 설치 brew install golang-migrate
4. 백엔드 프로젝트 초기화 (server/)
프로젝트 루트 폴더에 server 디렉토리를 만들고 Go 모듈을 생성합니다.
# 1. server 디렉토리 생성 및 이동 mkdir -p server cd server # 2. Go 모듈 초기화 (본인 깃허브 계정에 맞게 변경) go mod init github.com/yourname/dingdong-server # 3. 필수 라이브러리 설치 go get github.com/go-chi/chi/v5 go get github.com/go-chi/cors go get github.com/gorilla/websocket go get github.com/jackc/pgx/v5 go get github.com/redis/go-redis/v9 go get github.com/hibiken/asynq
5. 클린 아키텍처 폴더 구조
복잡하지 않으면서도 유지보수가 쉬운 Go 표준 패키지 레이아웃을 구성합니다:
server/ ├─ cmd/ │ └─ api/ │ └─ main.go # 서버 진입점 (설정 로드, DB 연결, 서버 실행) ├─ internal/ │ ├─ config/ # 환경변수 로더 │ ├─ database/ # PostgreSQL 연결 풀 (pgx) │ ├─ auth/ # JWT 발급, 검증, 미들웨어 │ ├─ user/ # 회원가입, 프로필, 연락처 매칭 │ ├─ chat/ # 채팅방, 메시지, WebSocket 허브 │ ├─ call/ # LiveKit 통화 시그널링 │ └─ push/ # FCM / APNs 푸시 작업 ├─ migrations/ # DB 테이블 생성용 .sql 파일 ├─ sqlc.yaml # sqlc 설정 파일 ├─ Dockerfile # 유그린 NAS 배포용 멀티스테이지 도커파일 ├─ go.mod └─ go.sum
“※ 위 폴더 구조는 이번 시리즈를 진행하며 단계별로 하나씩 채워나갈 전체 설계도입니다. 지금 단계에서는 당장 실행에 필요한 cmd/api/main.go 파일부터 차근차근 만들어 보겠습니다.”
6. 첫 번째 헬스체크 API 만들기 (cmd/api/main.go)
서버가 잘 작동하는지 확인하기 위해 기본 HTTP 서버와 /health 엔드포인트를 만듭니다.
package main
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
r := chi.NewRouter()
// 1. 공통 기반 미들웨어
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// 2. CORS 설정 (모바일 앱은 영향 없으나, 웹 클라이언트 및 보안 표준 준수)
r.Use(cors.Handler(cors.Options{
// 실제 운영 환경에서는 허용할 프론트엔드 도메인으로 한정하는 것이 안전합니다.
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false, // 와일드카드(*) Origin 사용 시 false가 웹 표준상 안전합니다.
MaxAge: 300,
}))
// 3. 일반 REST API 라우터 그룹 (타임아웃 60초 적용)
r.Group(func(api chi.Router) {
api.Use(middleware.Timeout(60 * time.Second))
api.Get("/health", func(w http.ResponseWriter, r *http.Request) {
res := map[string]string{
"status": "ok",
"server": "dingdong-go",
"time": time.Now().Format(time.RFC3339),
"version": "v0.1.0",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
})
})
// 4. 실시간 WebSocket 엔드포인트 그룹
// [중요!] WebSocket은 상시 연결이 유지되어야 하므로 Timeout 미들웨어가 없는 그룹에 둡니다.
r.Group(func(ws chi.Router) {
ws.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("WebSocket 엔드포인트 (추후 구현)"))
})
})
// 5. 서버 설정 및 Graceful Shutdown (우아한 종료)
server := &http.Server{
Addr: ":" + port,
Handler: r,
ReadTimeout: 15 * time.Second,
// [중요 실무 팁!] WriteTimeout이 0이 아니면 WebSocket으로 업그레이드된 후에도
// 정해진 시간 뒤 소켓 쓰기가 강제 차단됩니다.
// 따라서 WriteTimeout은 0(비활성)으로 두고, 일반 REST API는 chi의 middleware.Timeout으로 제어합니다.
WriteTimeout: 0,
IdleTimeout: 60 * time.Second,
}
// 백그라운드 고루틴으로 서버 리슨
go func() {
log.Printf("🚀 딩동 Go 백엔드 서버가 포트 %s에서 시작되었습니다!", port)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("서버 시작 실패: %v", err)
}
}()
// 종료 시그널(Ctrl+C, SIGTERM 등) 대기
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("서버 종료 신호를 감지했습니다. 안전하게 종료합니다...")
// 최대 10초 대기하며 진행 중인 작업 마무리 후 종료
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("강제 종료 에러: %v", err)
}
log.Println("서버가 안전하게 종료되었습니다.")
}
로컬 실행 테스트
# 서버 실행 go run cmd/api/main.go # 다른 터미널에서 헬스체크 요청 curl http://localhost:3000/health
응답 결과:
{
"server": "dingdong-go",
"status": "ok",
"time": "2026-10-14T13:20:00+09:00",
"version": "v0.1.0"
}
7. 유그린 NAS 배포용 초경량 Dockerfile 만들기
7.1 Dockerfile이 도대체 무슨 의미인가요? (쉽게 이해하기)
방금 6번에서는 내 맥북에서 go run 명령어로 서버를 띄웠습니다.
하지만 우리의 최종 목표는 맥북을 끄더라도 집에 있는 유그린 NAS에서 24시간 내내 이 서버가 돌아가게 만드는 것입니다.
그런데 유그린 NAS에는 Go 언어나 컴파일러가 깔려 있지 않습니다.
그래서 **”이 서버 프로그램을 실행할 수 있는 초소형 포장 박스(Docker 컨테이너)”**를 만들어야 합니다.
Dockerfile은 바로 그 포장 박스를 만드는 **’조립 설명서’**입니다:
- 1단계 (Builder 작업실): “Go가 설치된 임시 작업실에서 우리가 짠
main.go코드를 실행 파일(server) 하나로 딱 구워낸다!” - 2단계 (가벼운 도시락통): “다 구웠으면 무거운 Go 개발 도구들은 다 버리고, 잘 구워진 작은 빵(
server) 하나만 아주 가벼운 도시락통(alpine리눅스)에 쏙 담는다!”
이 기법을 **멀티스테이지 빌드(Multi-stage Build)**라고 부르며, NAS에 이것저것 설치할 필요 없이 이 가벼운 포장 박스 하나만 NAS에 올려주면 알아서 24시간 돌아가게 됩니다.
7.2 어디에, 어떻게 만드나요?
- VS Code의 좌측 탐색기에서
server폴더를 우클릭 ➡️ **[새 파일]**을 누릅니다. - 파일 이름으로 확장자 없이 대소문자를 정확히 지켜
Dockerfile이라고 입력합니다.
(최종 파일 위치:server/Dockerfile) - 열린 빈 파일에 아래 코드를 그대로 붙여넣고 저장(
Cmd + S)합니다.
# 1단계: 빌드 환경 (Builder 작업실) FROM golang:1.23-alpine AS builder WORKDIR /app RUN apk add --no-cache git ca-certificates # 의존성 캐시 활용 COPY go.mod go.sum ./ RUN go mod download # 소스코드 복사 및 단일 정적 바이너리로 컴파일 COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /app/server cmd/api/main.go # 2단계: 실제 실행 환경 (가벼운 도시락통) FROM alpine:3.20 WORKDIR /app RUN apk add --no-cache ca-certificates tzdata ENV TZ=Asia/Seoul COPY --from=builder /app/server /app/server EXPOSE 3000 ENTRYPOINT ["/app/server"]
💡 도커 이미지 크기의 기적: Node.js 기반 이미지가 보통 300MB~1GB인 반면, 위 Dockerfile로 빌드된 Go 이미지는 단 25MB에 불과합니다! 유그린 NAS에 배포할 때 다운로드와 업데이트가 눈 깜짝할 사이에 끝납니다.
7.3 이걸 만들고 나면 어떻게 쓰이나요?
이렇게 server/Dockerfile을 만들어 두면, 바로 다음 **8번의 docker-compose.yml**에서:
api:
build:
context: ./server
dockerfile: Dockerfile
와 같이 연결됩니다. 나중에 유그린 NAS에서 docker compose up -d 명령어 하나만 딱 치면, 도커가 이 Dockerfile 조립 설명서를 읽고 알아서 메신저 서버를 25MB 초경량 컨테이너로 뚝딱 조립해서 24시간 켜주게 됩니다!
8. docker-compose.yml 파일은 어디에 만들고 어떻게 추가하나요?
8.1 파일 위치 안내 (내 맥북 vs 유그린 NAS)
5번에서 본 폴더 구조는 server/ 안의 순수 Go 코드 구조였기 때문에 docker-compose.yml이 보이지 않았던 것입니다.
배포 및 인프라 파일들은 다음과 같이 배치합니다:
내 맥(Mac) 프로젝트 위치: deploy/ 폴더를 만들어 관리합니다.
dingdong/ (최상위 루트)
├─ server/ # Go 백엔드 코드 (main.go, Dockerfile)
└─ deploy/ # 👈 [여기에 만듭니다!]
├─ docker-compose.yml # 통합 실행 파일
├─ .env # 환경변수 파일
└─ caddy/
└─ Caddyfile # Caddy 설정 파일
유그린 NAS 위치: 나중에 NAS로 배포할 때는 SSD 작업 폴더(/volume1/docker/dingdong/)에 그대로 복사해 넣게 됩니다.
8.2 맥북 VS Code에서 파일 만들기
- VS Code 좌측 탐색기 빈 공간을 우클릭 ➡️ [새 폴더] ➡️ 이름으로
deploy입력. deploy폴더 우클릭 ➡️ [새 파일] ➡️ 이름으로docker-compose.yml입력.
(최종 경로:deploy/docker-compose.yml)- 파일에 아래의 완성된 YAML 코드를 붙여넣고 저장(
Cmd + S)합니다.
8.3 완성된 docker-compose.yml 코드
지난 [3편]의 Caddy, PostgreSQL에 이어 방금 만든 Go API 서버와 백그라운드 큐를 위한 Redis까지 연결된 최종 파일입니다:
version: '3.8'
services:
caddy:
image: caddy:2-alpine
container_name: dingdong-caddy
restart: unless-stopped
env_file: .env
ports:
- "8080:80"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
networks:
- app-network
# [추가됨!] Go 백엔드 API 서버
api:
build:
context: ./server
dockerfile: Dockerfile
container_name: dingdong-api
restart: unless-stopped
env_file: .env
environment:
- PORT=3000
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
- REDIS_URL=redis:6379
depends_on:
- postgres
- redis
networks:
- app-network
postgres:
image: postgres:16-alpine
container_name: dingdong-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "5432:5432"
volumes:
- ./postgres/data:/var/lib/postgresql/data
networks:
- app-network
# [추가됨!] 백그라운드 작업 큐(Asynq) 및 캐시용 Redis
redis:
image: redis:7-alpine
container_name: dingdong-redis
restart: unless-stopped
volumes:
- ./redis/data:/data
networks:
- app-network
networks:
app-network:
name: dingdong-network
💡 Redis 추가 팁: Asynq를 통한 푸시 알림 발송 및 접속 상태 관리를 위해 Redis 7을 함께 구성했습니다. 포트를 외부에 열지 않고 내부 네트워크(
app-network)에서만 통신하므로 안전합니다.
8.4 [중요!] 3편에서 나스에 미리 만들어 둔 파일들도 수정해야 하나요?
네, 맞습니다! 3편을 따라 이미 유그린 나스에 세팅해 두셨다면 아래 3가지를 확인 및 업데이트해 주시면 됩니다:
1. 파일 확장자 안내: docker-compose.yml vs docker-compose.yaml
- 유그린 UGOS Pro Docker 앱이나 파일 관리자에서 확장자가
.yaml로 바뀌었더라도 걱정하지 마세요! - 도커 엔진과 Docker Compose는 기본적으로
.yml과.yaml을 100% 동일한 파일로 인식합니다. 둘 중 어느 쪽이든 정상 동작합니다.
2. 유그린 나스의 Compose 파일 갱신
- 위 8.3 코드처럼
api서비스와redis서비스 블록을 추가해 주어야 합니다. api서비스의depends_on에redis가 적혀 있기 때문에, 나스에redis컨테이너 설정이 빠져 있으면 도커가 에러를 출력하며 실행을 거부합니다.
3. 유그린 나스의 .env 파일에 3줄 추가
- 기존의 도메인/DB 설정 아래에 Go 서버, Redis 연결, 그리고 로그인 토큰 암호화용
JWT_SECRET을 추가합니다:
# [이번 4편에서 추가된 항목] PORT=3000 REDIS_URL=redis:6379 # JWT 인증 토큰 암호화용 비밀키 (최소 32자 이상의 임의 문자열 권장) JWT_SECRET=dingdong_super_secret_jwt_key_2026_change_this_to_random_string
💡 JWT_SECRET 발급 팁: 맥북이나 리눅스 터미널에서
openssl rand -base64 32명령어를 입력하면 아주 안전하고 긴 무작위 문자열을 생성할 수 있습니다. 생성된 문자열을 복사해서 붙여넣으시면 됩니다.
(※ deploy/caddy/Caddyfile은 3편에서 작성했던 설정 그대로 유지하시면 됩니다!)
9. 다음 편 예고
이제 유그린 NAS에서 메모리를 30MB도 쓰지 않는 강력한 Go 백엔드 API 엔진의 뼈대가 완성되었습니다!
다음 **[개발일지 #05]**에서는 golang-migrate와 sqlc를 사용해 메신저의 핵심 데이터베이스 스키마(회원 정보, 전화번호 해시, 채팅방, 메시지 순번)를 작성하고, 타입 안전한 SQL 쿼리 코드를 자동으로 생성하는 과정을 실습 위주로 다루겠습니다!
📂 Part 1. 기획 & 아키텍처 설계
- ✅ [개발일지 #01 | 기획] 유그린 NAS와 Flutter로 만드는 나만의 프라이빗 메신저 ‘딩동’ (기획 & 아키텍처 편)
📂 Part 2. 홈서버 & 인프라 구축 (UGREEN + Synology)
- ✅ [개발일지 #02 | 인프라] Mac에서 Flutter & VS Code 개발 환경 구축하기 (초보자 완벽 가이드)
- ✅ [개발일지 #03 | 인프라] 유그린 NAS에 Caddy & PostgreSQL 구축하기 (홈서버 443 포트 충돌 해결 꿀팁 포함)
📂 Part 3. 백엔드(API) & 실시간 통화 서버
- ✅ [개발일지 #04 | 백엔드] 유그린 NAS에 Go(Golang) 초경량 메신저 백엔드 서버 구축하기
- ⏳ [#05 | 백엔드] JWT 기반 가족 인증 및 실시간 WebSocket 채팅 구현 (예정)
- ⏳ [#06 | 통화] LiveKit 연동으로 무료 음성/영상 통화 서버 구축하기 (예정)
📂 Part 4. 모바일 앱 개발 (iOS / Android)
- ⏳ [#07 | 앱-공통] 모바일 앱 프로젝트 생성 및 로그인/채팅방 UI 구현 (예정)
- ⏳ [#08 | 앱-공통] 300MB 대용량 원본 사진/영상 전송 및 미디어 처리 (예정)
- ⏳ [#09 | 앱-푸시] 앱이 꺼져 있어도 울리는 푸시 알림(FCM/APNs) 연동 (예정)
📂 Part 5. 정식 배포 & 운영
- ⏳ [#10 | 배포] 백엔드 서버 도커 이미지 빌드 및 유그린 NAS 정식 배포 (예정)
- ⏳ [#11 | 운영] DB 자동 백업 스크립트 구성 및 가족 실사용 후기 (예정)