CWIST 프레임워크 개발: 프레임워크는 왜 비대해지는가? 1장
by
gg582 · 2026-05-27 04:04:51 · 52 views
CWIST 프레임워크
CWIST 프레임워크는 C 언어로 고수준의 추상화를 제공하는 현대적인 SSR과 HTTP/3+TLS 1.3을 지원하는 프레임워크입니다. CWIST는 저전력 임베디드 보드(예: STM32F769i-DISCO)와 같은 것을 목표로 하기보단, 라즈베리 파이 2 이후 세대, RAM 2-3GB VPS 등의 환경을 가정하고 적당히 무겁고, 적당히 빠른 프레임워크를 제공함으로 C언어 기반의 생태계를 그대로 누리면서, Axum 등의 러스트 생태계 못지 않은 반응성을 보여 주기 위하여 개발되었습니다. 이 프레임워크는 학습용과 실제 개발용 목적을 모두 가지고 있습니다.
CWIST 프레임워크의 HTTP/3: 실제 블로그 엔진 코드의 메인 함수를 통하여 알아 봅시다.
#define _POSIX_C_SOURCE 200809L
#include "handlers/handlers.h"
#include "auth/auth.h"
#include "crypto/fly_crypto.h"
#include "db/db.h"
#include "nats/fly_nats.h"
#include "config/config.h"
#include <cwist/sys/app/app.h>
#include <signal.h>
#if defined __has_include
# if __has_include (<cwist/security/tls/ech.h>)
# define HAVE_ECH 1
# include <cwist/security/tls/ech.h>
# endif
#endif
#include <cwist/core/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/timerfd.h>
#include <limits.h>
#define BLOG_CERT "server.crt"
#define BLOG_KEY "server.key"
#define DB_PATH "data/blog.db"
static volatile bool g_nats_running = false;
static bool dir_exists(const char *path) {
struct stat st;
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}
static bool ensure_workdir_with_public(const char *root) {
if (!root || !root[0]) return false;
char public_path[PATH_MAX];
int result = snprintf(public_path, sizeof(public_path), "%s/public", root);
if (result < 0 || result >= (int)sizeof(public_path)) return false;
if (!dir_exists(public_path)) return false;
return chdir(root) == 0;
}
static bool ensure_asset_workdir(void) {
if (dir_exists("public")) return true;
const char *env_root = getenv("BLOG_ROOT");
if (ensure_workdir_with_public(env_root)) return true;
#if defined(__linux__)
char exe_path[PATH_MAX];
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
if (len <= 0 || len >= (ssize_t)(sizeof(exe_path) - 1)) return dir_exists("public");
exe_path[len] = '\0';
char *slash = strrchr(exe_path, '/');
if (!slash) return dir_exists("public");
*slash = '\0';
if (ensure_workdir_with_public(exe_path)) return true;
#endif
return dir_exists("public");
}
static void *nats_worker(void *arg) {
(void)arg;
while (g_nats_running) {
fly_nats_dispatch();
}
return NULL;
}
static int create_daily_3am_timer(void) {
int fd = timerfd_create(CLOCK_REALTIME, 0);
if (fd < 0) return -1;
time_t now = time(NULL);
struct tm *tm_now = localtime(&now);
struct tm target = *tm_now;
target.tm_hour = 3;
target.tm_min = 0;
target.tm_sec = 0;
time_t target_time = mktime(&target);
if (target_time <= now) target_time += 24 * 3600;
struct itimerspec its;
its.it_value.tv_sec = target_time - now;
its.it_value.tv_nsec = 0;
its.it_interval.tv_sec = 24 * 3600;
its.it_interval.tv_nsec = 0;
if (timerfd_settime(fd, 0, &its, NULL) < 0) {
close(fd);
return -1;
}
return fd;
}
static void *cleanup_worker(void *arg) {
cwist_db *db = (cwist_db *)arg;
int tfd = create_daily_3am_timer();
if (tfd < 0) {
CWIST_LOG_ERROR("Failed to create cleanup timerfd");
return NULL;
}
uint64_t exp;
while (1) {
ssize_t s = read(tfd, &exp, sizeof(exp));
if (s != sizeof(exp)) break;
db_cleanup_orphaned_files(db);
}
close(tfd);
return NULL;
}
int main(void) {
signal(SIGPIPE, SIG_IGN);
fly_log_init();
if (!ensure_asset_workdir()) {
FLY_LOG_ERROR("Public assets not found; set BLOG_ROOT or run from project root");
return 1;
}
CWIST_LOG_INFO("Workdir verified");
if (!fly_crypto_init()) {
FLY_LOG_ERROR("PQC crypto init failed");
return 1;
}
CWIST_LOG_INFO("Crypto initialized");
if (!auth_admin_load("admin.settings")) {
FLY_LOG_ERROR("Failed to load admin.settings");
return 1;
}
CWIST_LOG_INFO("Admin settings loaded");
blog_config_load("blog.settings");
CWIST_LOG_INFO("Blog config loaded");
const char *nats_url = getenv("NATS_URL");
if (nats_url) {
if (fly_nats_init(nats_url)) {
g_nats_running = true;
pthread_t ntid;
pthread_create(&ntid, NULL, nats_worker, NULL);
} else {
FLY_LOG_ERROR("NATS init failed, continuing without messaging");
}
}
cwist_app *app = cwist_app_create();
if (!app) {
FLY_LOG_ERROR("Failed to create app");
fly_crypto_cleanup();
return 1;
}
CWIST_LOG_INFO("CWIST app created");
cwist_error_t dberr = cwist_app_use_db(app, DB_PATH);
if (dberr.errtype != CWIST_ERR_INT16 || dberr.error.err_i16 != 0) {
FLY_LOG_ERROR("Failed to open database");
cwist_app_destroy(app);
return 1;
}
CWIST_LOG_INFO("Database opened: %s", DB_PATH);
cwist_db *db = cwist_app_get_db(app);
if (!db_init(db)) {
FLY_LOG_ERROR("Failed to initialize database schema");
cwist_app_destroy(app);
return 1;
}
CWIST_LOG_INFO("Database schema initialized");
if (!db_comment_init("data/comments.db")) {
FLY_LOG_ERROR("Failed to initialize comments database");
cwist_app_destroy(app);
return 1;
}
CWIST_LOG_INFO("Comments database initialized");
db_file_cleanup_duplicates(db);
db_cleanup_orphaned_files(db);
CWIST_LOG_INFO("Orphaned files cleanup completed");
pthread_t cleanup_tid;
pthread_create(&cleanup_tid, NULL, cleanup_worker, db);
cwist_app_set_max_memspace(app, CWIST_MIB(512));
cwist_app_configure_bdr(app, CWIST_MIB(256), 600, 250000);
cwist_app_use_https2(app, true);
cwist_app_use_https3(app, true);
cwist_error_t tls = cwist_app_use_https(app, BLOG_CERT, BLOG_KEY);
if (tls.errtype != CWIST_ERR_INT16 || tls.error.err_i16 != 0) {
FLY_LOG_ERROR("HTTPS init failed; run ./keygen.sh first");
cwist_app_destroy(app);
return 1;
}
CWIST_LOG_INFO("HTTPS initialized");
const char *ech_key = getenv("BLOG_ECH_KEY");
const char *ech_dir = getenv("BLOG_ECH_DIR");
cwist_error_t ech = cwist_app_use_ech(app, ech_key, ech_dir);
if (ech.errtype != CWIST_ERR_INT16 || ech.error.err_i16 != 0) {
FLY_LOG_ERROR("ECH init failed");
}
cwist_app_use(app, global_middleware);
cwist_app_get(app, "/assets/img/:filename", handler_asset_img);
cwist_app_get(app, "/assets/uploads/:filename", handler_asset_upload);
cwist_app_get(app, "/assets/profile/:filename", handler_asset_profile_upload);
cwist_app_get(app, "/assets/tasfa/:scope/:filename/handshake", handler_asset_tasfa_handshake);
cwist_app_get(app, "/assets/tasfa/:scope/:filename/chunk/:chunk_index", handler_asset_tasfa_chunk);
cwist_app_static(app, "/assets/images", "public/images");
cwist_app_static(app, "/assets/js", "public/js");
cwist_app_static(app, "/js", "public/js");
cwist_app_static(app, "/assets/media", "public/media");
cwist_app_get(app, "/sw.js", handler_sw_js);
/* Routes */
cwist_app_get(app, "/", handler_home);
cwist_app_get(app, "/theme.json", handler_theme_json);
cwist_app_get(app, "/themes.json", handler_themes_json);
cwist_app_get(app, "/rss.xml", handler_rss_xml);
cwist_app_get(app, "/login", handler_login_get);
cwist_app_post(app, "/login", handler_login_post);
cwist_app_get(app, "/logout", handler_logout);
cwist_app_get(app, "/register", handler_register_get);
cwist_app_post(app, "/register", handler_register_post);
cwist_app_post(app, "/unregister", handler_unregister_post);
cwist_app_get(app, "/profile", handler_profile_get);
cwist_app_post(app, "/profile", handler_profile_post);
cwist_app_get(app, "/account/settings", handler_account_settings_get);
cwist_app_post(app, "/account/settings", handler_account_settings_post);
cwist_app_get(app, "/account/password", handler_password_change_get);
cwist_app_post(app, "/account/password", handler_password_change_post);
cwist_app_get(app, "/user/:id", handler_user_profile_get);
cwist_app_get(app, "/boards", handler_board_list);
cwist_app_get(app, "/board/new", handler_board_new_get);
cwist_app_post(app, "/board/new", handler_board_new_post);
cwist_app_get(app, "/board/:id/edit", handler_board_edit_get);
cwist_app_post(app, "/board/edit", handler_board_edit_post);
cwist_app_get(app, "/board/:id/delete", handler_board_delete);
cwist_app_get(app, "/board/:id/perms", handler_board_perms_get);
cwist_app_post(app, "/board/perms", handler_board_perms_post);
cwist_app_post(app, "/board/perms/revoke", handler_board_perms_revoke_post);
cwist_app_get(app, "/search", handler_post_list);
cwist_app_get(app, "/board/:slug", handler_post_list);
cwist_app_get(app, "/post/:slug", handler_post_get);
cwist_app_get(app, "/post/new", handler_post_new_get);
cwist_app_post(app, "/post/new", handler_post_new_post);
cwist_app_get(app, "/post/:id/edit", handler_post_edit_get);
cwist_app_post(app, "/post/:id/edit", handler_post_edit_post);
cwist_app_get(app, "/post/delete/:id", handler_post_delete);
cwist_app_get(app, "/files", handler_file_repo);
cwist_app_get(app, "/file/:id", handler_file_detail_get);
cwist_app_get(app, "/file/download/:id", handler_file_download);
cwist_app_get(app, "/file/download/:id/handshake", handler_file_download_handshake);
cwist_app_get(app, "/file/download/:id/chunk/:chunk_index", handler_file_download_chunk);
cwist_app_post(app, "/file/download/complete", handler_file_download_complete);
cwist_app_post(app, "/file/upload/init", handler_file_upload_init);
cwist_app_post(app, "/file/upload/status", handler_file_upload_status);
cwist_app_post(app, "/file/upload/renegotiate", handler_file_upload_renegotiate);
cwist_app_post(app, "/file/upload", handler_file_upload);
cwist_app_post(app, "/file/upload/complete", handler_file_upload_complete);
cwist_app_post(app, "/file/upload/cancel", handler_file_upload_cancel);
cwist_app_post(app, "/file/delete", handler_file_delete);
cwist_app_post(app, "/comment/new", handler_comment_new_post);
cwist_app_post(app, "/comment/edit", handler_comment_edit_post);
cwist_app_get(app, "/comment/:id/delete", handler_comment_delete_get);
cwist_app_get(app, "/admin/users", handler_admin_users);
cwist_app_post(app, "/admin/user/role", handler_admin_user_role);
cwist_app_post(app, "/admin/files/drop", handler_admin_files_drop);
cwist_app_post(app, "/api/preview", handler_api_preview);
cwist_app_post(app, "/api/upload", handler_api_upload);
cwist_app_get(app, "/api/boards", handler_api_boards_json);
cwist_app_get(app, "/api/my-files", handler_api_my_files);
cwist_app_post(app, "/post/vote", handler_post_vote);
/* Enable io_uring-based async reactor for C1M scale mode.
* cwist_reactor will use Linux io_uring direct syscalls on Linux,
* falling back to epoll/kqueue on other platforms. */
setenv("CWIST_C1M_MODE", "1", 1);
CWIST_LOG_INFO("Starting server on port %d (HTTP/3 on UDP %d)", g_config.port, g_config.port);
printf("Docker Blog: https://localhost:%d (HTTP/3 on UDP %d)\n", g_config.port, g_config.port);
int rc = cwist_app_listen(app, g_config.port);
g_nats_running = false;
fly_nats_close();
db_comment_close();
cwist_app_destroy(app);
fly_crypto_cleanup();
return rc;
}
이것은 지금 여러분이 보고 있는 fly.board의 1개 워커에서 실제 사용되는 메인 함수입니다. 이 경우, HTTP/3의 활성화를 어떻게 하고 있나요? 외부적으로 드러나는 것은 이것 뿐입니다.
cwist_app_use_https3(app, true);
그러나, cwist_app 구조체가 처리하는 것은 어떤 것들이 있나요?
슬러그 등록, HTTP/2, 환경 변수 설정 시 ECH 등록 등의 다양한 기능들을 포함하고 있습니다. (첨언하자면 프레임워크는 ECH를 지원하며, 운영 환경에서는 DNS 레코드 설정이 필요합니다)
이 모든 것이 몇 줄의 함수만으로 처리되기 위해서는 자원의 등록과 관리가 추상화될 수밖에 없습니다.
실제 app.h
실제 app.h의 참조 관계를 보면, 매우 복잡한 헤더 포함과 의존성이 단 하나의 헤더에 모여 있기 때문에 미관 상 보기에 좋지 못합니다.
그러나, 사용자 입장에서의 코드를 비교해 보세요...
#include <cwist/sys/app/app.h>
/* ... */
cwist_app *app = cwist_app_create();
cwist_app_use_https3(app, true);
#include <cwist/sys/app/app.h>
#include <cwist/net/http/http3.h>
/* ... */
cwist_app *app = cwist_app_create();
cwist_http3 *http3 = cwist_http3_create();
ret = cwist_register_http3_to_app(app, http3);
if(ret != 0) {
ret = cwist_destroy_http3(http3);
if(ret != 0) cwist_app_destroy(app);
}
코드의 구조적 분리와 별개로, 프로그래머 입장에선 전자가 압도적으로 외우기 편합니다, 그리고, 이런 단순화된 구조는 필연적으로 퍼사드 패턴을 불러 옵니다.
다른 블로그의 글을 잠깐 인용해 봅시다. 퍼사드 패턴 이 블로그의 글을 보면, 퍼사드 패턴의 유지보수성, 그리고 사용자 입장에서의 압도적인 편리함에 대하여 역시 논의하고자 합니다.
만약 사용자가 숙련된 임베디드 전문가라면 심한 경우 메모리 주소에 대한 직접 액세스를 노출해도 아무런 문제가 되지 않습니다. 그러나, 사용자가 C언어를 처음 배우는 고등학생이라고 생각하면 이것은 매우 학습 곡선을 가파르게 만들 수 있습니다.
따라서 CWIST 역시 목표 사용자가 달랐다면, 노출 범위가 달라졌을 것입니다.
CWIST의 예상 사용자
CWIST의 예상 사용자는 C언어를 다 배웠지만, C++이나 Rust에 비해 생태계가 빈약해 퀵 정렬만 100번 구현하는 학생들입니다. 이러한 학생들이 C언어로 유입되지 못하는 가장 큰 까닭은, 보다 어려운 생태계를 진입하기 전 단계로 웹 개발, 애플리케이션 개발 등이 없기 때문이라는 것 역시 부정하기 힘듭니다.
따라서 이러한 사용자의 숙련도를 고려하면 세부적인 함수 역시 추가 학습을 위해 노출은 하되, 기본적으론 쉬운 래퍼들이 많은 방향으로 조정할 수밖에 없습니다.
CWIST의 메모리 관리 전략
이러한 문제점들에 대하여 인지한 것과 별개로, C의 메모리 관리는 지나치게 자유로워서 프로그래머의 판단은 물론이고, LLM의 판단에도 맡기기 곤란합니다.
따라서, 난해할 수 있는 패턴들을 관리하기 위해 기존에 작성해 둔 메모리 모델인 LibTTAK을 꺼내 와서 도입했습니다.
이 때, LibTTAK은 비동기 처리, GC, 통신 등에 대하여 작성된 시스템 라이브러리이며, CWIST의 비즈니스 로직에서는 이러한 것들을 빌려 옵니다.
따라서 비즈니스 로직과 LibTTAK은 철저하게 분리되며, 메모리가 더러워질 수 있는 통신이나 메모리 관리는 LibTTAK에 격리합니다.
자동 할당기 변경을 도입
그때그때 새로운 힙 공간을 임기응변으로 할당해 오느니, 사전에 구현한 아레나, VMA 등의 자동 폴백을 가져오는 것이 좋습니다. LibTTAK의 할당기는 새로 할당하고자 하는 자료의 크기와 정적 생명 주기를 활용하여 VMA 공간을 쓸지, 작은 아레나를 두어 빌려줄지 등을 자동적으로 결정합니다.
또한, Non-stop GC 계열인 LibTTAK의 EpochGC를 통하여서 기존 작업을 멈추지 않고도 약한 가비지 컬렉터를 걸어서 C의 고질병인 고아 메모리(Orphaned Memory Block) 등이 덜 발생하도록 하였습니다.
CWIST의 래핑 전략
CWIST의 래핑 전략은 LibTTAK이 비동기 처리 등을 1차로 래핑해 둔 것을 웹 개발이라는 맥락에 맞게 다시 래핑하는 것입니다. 따라서 CWIST의 코어 모듈에 해당하는 LibTTAK은, 단 1바이트라도 아쉬울 것 없이 정합성을 지켜야 하며, 또 그러하다고 가정하고 진행됩니다.
그러나...
그러나 IT 분야의 종사자들은 익히 알고 있는 left-pad 사건과 같이, 많은 경우 이렇게 어정쩡하게 얹어진 의존 관계의 경우 하위 모듈의 정합성은 지속적으로 관찰되어야 합니다.
맺으며...
C언어로 웹 프레임워크를 새롭게 작성해 보면서, 사람이나 LLM의 규칙 파악 능력의 한계로 높은 복잡도를 기피할 수 없는 순간이 온다는 것을 체감하였습니다. 그러나, 이러한 코드의 의존 관계가 더러워지는 것, 그리고 어디서 문제가 생긴 지 파악하는 것은 LLM을 이용한 코드 생성이 늘어난 현 시대에는 더 중요해졌습니다.
다음 시간
다음 시간에는 Doxygen을 통한 문서화, 그리고 문서를 통한 구현 역추적을 해 볼 것이며, 이를 통해 TLS 1.3이 어떻게 붙었으며, OpenSSL과 BoringSSL을 동시에 사용하는 통합 아키텍처가 어떻게 가능하였는지 다루려고 합니다. 시간이 된다면, PQC 레이어를 어느 정도까지 통합하고 어디까지 타협했는지도 짧게 서술하겠습니다.