순수 Go로 구현한 미니멀 헤드리스 CMS: 아키텍처와 구현 방법
핵심 결정 사항은 mattn/go-sqlite3 대신 modernc.org/sqlite를 사용하는 것입니다. 이는 SQLite의 순수 Go 포트로, 호환되는 database/sql API를 제공하며 C 컴파일러가 필요하지 않습니다.
import (
"database/sql"
_ "modernc.org/sqlite"
)
func Open(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
// 동시 접근을 위한 WAL 모드
pragmas := []string{
"PRAGMA journal_mode=WAL",
"PRAGMA busy_timeout=5000",
"PRAGMA foreign_keys=ON",
"PRAGMA synchronous=NORMAL",
}
for _, p := range pragmas {
if _, err := db.Exec(p); err != nil {
return nil, fmt.Errorf("pragma %q: %w", p, err)
}
}
return db, nil
}
WAL 모드는 쓰기 중에도 병렬 읽기를 가능하게 하여, 동시에 API와 관리자 패널이 작동할 때 필수적입니다.
net/http를 활용한 라우팅과 미들웨어
Gin/Echo/Chi 대신 http.ServeMux를 사용함으로써 의존성 감소와 디버깅 간편화를 달성합니다:
mux := http.NewServeMux()
// 공개 API
mux.Handle("/api/leads", rateLimiter(http.HandlerFunc(h.CreateLead)))
mux.Handle("/api/news", http.HandlerFunc(h.ListNews))
// 관리자 패널
mux.Handle("/admin/", authMiddleware(sessionStore, adminHandler))
미들웨어 체인: logger → rateLimiter → auth → csrfCheck → handler. 각각 독립된, 테스트 커버리지가 확보된 함수입니다.
Bitrix24와 비동기 통합
CRM에 리드 제출은 Go 채널과 워커 풀을 통해 비동기적으로 처리되며, Redis나 Celery 없이도 가능합니다:
type Worker struct {
db *sql.DB
queue chan Job
webhook string
wg sync.WaitGroup
}
func NewWorker(db *sql.DB, webhook string, poolSize int) *Worker {
w := &Worker{
db: db,
queue: make(chan Job, 100),
webhook: webhook,
}
for i := 0; i < poolSize; i++ {
w.wg.Add(1)
go w.process()
}
return w
}
func (w *Worker) Enqueue(job Job) {
select {
case w.queue <- job:
default:
log.Printf("bitrix queue full, lead %d will be retried manually", job.LeadID)
}
}
100건의 작업 버퍼, 30초 타임아웃을 포함한 격리된 종료 기능이 있습니다.
HMAC-SHA256 기반 CSRF 보호
HTMX를 사용하는 관리자 폼은 동기화된 토큰으로 보호됩니다:
func generateCSRFToken(sessionID, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(sessionID))
return hex.EncodeToString(mac.Sum(nil))
}
func validateCSRFToken(r *http.Request, sessionID, secret string) bool {
token := r.FormValue("csrf_token")
if token == "" {
token = r.Header.Get("X-CSRF-Token")
}
expected := generateCSRFToken(sessionID, secret)
return hmac.Equal([]byte(token), []byte(expected))
}
토큰은 폼과 HTMX 헤더에 내장되어 있으며, hmac.Equal은 타이밍 공격을 방지합니다.
메모리 기반 요청 제한
공개 API는 외부 저장소 없이 IP당 분당 10회 요청만 허용합니다:
type IPLimiter struct {
visitors sync.Map
mu sync.Mutex
}
type entry struct {
count int
resetAt time.Time
}
func (l *IPLimiter) Allow(ip string) bool {
now := time.Now()
val, _ := l.visitors.LoadOrStore(ip, &entry{resetAt: now.Add(time.Minute)})
e := val.(*entry)
l.mu.Lock()
defer l.mu.Unlock()
if now.After(e.resetAt) {
e.count = 0
e.resetAt = now.Add(time.Minute)
}
if e.count >= 10 {
return false
}
e.count++
return true
}
5분마다 정리되는 sync.Map 사용.
설정 없이 첫 실행
서버는 데이터베이스를 자동 초기화하고 관리자 비밀번호를 생성해 콘솔에 출력합니다. 플래그나 환경 변수를 통해 구성 가능:
./cms— 기본값 (포트=8080, db=./cms.db)CMS_PORT=8080 CMS_DB_PATH=./data.db ./cms
보안 조치
| 위협 | 대응 방안 |
|--------|------------|
| SQL 인젝션 | 파라미터화된 쿼리 |
| XSS | 자동 에스케이프를 지원하는 html/template |
| CSRF | HMAC 토큰 |
| 브루트 포스 | 분당 10회 요청 제한 |
| 경로 탐색 | 샌드박스 내에서 http.FileServer 사용 |
주요 특징:
embed.FS를 통해 정적 자산 포함, 바이너리 크기 약 20MB, 런타임 의존성 0개- 잠금 없이 동시성 보장 가능한 WAL SQLite
- Redis/Celery 대체용 Go 채널
- HMAC CSRF +
html/template로 강력한 보안 구현 CGO_ENABLED=0— 윈도우에서 리눅스로 크로스 컴파일 가능
— Editorial Team
아직 댓글이 없습니다.