추상화는 왜 복잡한 계층과 구조를 숨기는가?
by
gg582 · 2026-08-13 02:33:00 · 78 views
Table of contents
가장 먼저 볼 것
가장 먼저, 실제 이 블로그 서버의 main함수를 통해 CWIST의 HTTPS 활성화가 단순화되어있는 패턴을 보자.
int main(void) {
/* SQLite must be configured before any connection is opened. Use the
* serialized threading mode so a single connection can be safely shared
* across multiple worker threads. */
sqlite3_config(SQLITE_CONFIG_SERIALIZED);
setenv("CWIST_C1M_MODE", "0", 1);
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_jwt_init("data/.jwt_secret")) {
FLY_LOG_ERROR("JWT secret initialization failed");
fly_crypto_cleanup();
return 1;
}
CWIST_LOG_INFO("JWT secret initialized");
if (!engine_settings_load()) {
fly_crypto_cleanup();
return 1;
}
image_inline_cache_build();
CWIST_LOG_INFO("Inline image cache built");
if (!engine_pool_init()) {
fly_crypto_cleanup();
return 1;
}
if (!engine_nats_init()) {
engine_pool_shutdown();
fly_crypto_cleanup();
return 1;
}
cwist_app *app = cwist_app_create();
if (!app) {
FLY_LOG_ERROR("Failed to create app");
engine_nats_stop();
engine_pool_shutdown();
fly_crypto_cleanup();
return 1;
}
CWIST_LOG_INFO("CWIST app created");
cwist_db *db = NULL;
if (!engine_db_init(app, &db)) {
engine_nats_stop();
engine_pool_shutdown();
cwist_app_destroy(app);
fly_crypto_cleanup();
return 1;
}
db_file_cleanup_duplicates(db);
if (startup_media_backfill_enabled()) {
media_preview_backfill(db);
media_preview_backfill_static_assets();
} else {
CWIST_LOG_INFO("Startup media backfill skipped; set FLYBOARD_MEDIA_BACKFILL_ON_START=1 for maintenance");
}
db_cleanup_orphaned_files(db);
CWIST_LOG_INFO("Orphaned files cleanup completed");
page_cache_init();
reqshare_init();
page_cache_warmup(db);
g_cleanup_wake_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (g_cleanup_wake_fd < 0) {
FLY_LOG_ERROR("Failed to create cleanup wake event");
} else {
atomic_store_explicit(&g_cleanup_running, true, memory_order_release);
}
if (g_cleanup_wake_fd >= 0 &&
pthread_create(&g_cleanup_thread, NULL, cleanup_worker, NULL) != 0) {
cleanup_stop();
FLY_LOG_ERROR("Failed to start cleanup worker");
} else if (g_cleanup_wake_fd >= 0) {
g_cleanup_thread_started = true;
}
cwist_app_set_max_memspace(app, CWIST_MIB(512));
cwist_app_configure_bdr(app, CWIST_MIB(256), 600, 250000);
if (g_config.use_http2) {
cwist_app_use_https2(app, true);
CWIST_LOG_INFO("HTTP/2 enabled");
}
if (g_config.use_http3) {
cwist_app_use_https3(app, true);
CWIST_LOG_INFO("HTTP/3 enabled");
if (app->h3_ctx && app->h3_ctx->ssl_ctx) {
#if defined(SSL_CTX_set_early_data_enabled)
SSL_CTX_set_early_data_enabled(app->h3_ctx->ssl_ctx, 1);
#elif defined(SSL_CTX_set_max_early_data)
SSL_CTX_set_max_early_data(app->h3_ctx->ssl_ctx, 0xFFFFFFFF);
#endif
SSL_CTX_set_session_cache_mode(app->h3_ctx->ssl_ctx, SSL_SESS_CACHE_SERVER);
SSL_CTX_set_num_tickets(app->h3_ctx->ssl_ctx, 2);
CWIST_LOG_INFO("HTTP/3 0-RTT early data and session resumption enabled");
}
}
if (g_config.use_tls) {
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");
cleanup_stop();
cleanup_join();
engine_nats_stop();
engine_pool_shutdown();
cleanup_close_wake_fd();
cwist_app_destroy(app);
return 1;
}
CWIST_LOG_INFO("HTTPS initialized");
} else {
CWIST_LOG_INFO("TLS disabled, running plain HTTP");
}
const char *ech_key = getenv("BLOG_ECH_KEY");
const char *ech_dir = getenv("BLOG_ECH_DIR");
if (ech_key || 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");
cleanup_stop();
cleanup_join();
engine_nats_stop();
engine_pool_shutdown();
cleanup_close_wake_fd();
cwist_app_destroy(app);
return 1;
}
CWIST_LOG_INFO("ECH initialized");
}
/* Do not apply HTTP content compression here. The current transport path
* can deliver a compressed body without a matching Content-Encoding header
* to Firefox, which renders the page as binary data. Keeping responses in
* identity encoding is the safe compatibility mode for every client. */
cwist_compress_unregister_all();
cwist_compress_register_backend(cwist_compress_backend_brotli());
cwist_compress_register_backend(cwist_compress_backend_zstd()); // enabled zstd compression
cwist_compress_register_backend(cwist_compress_backend_gzip());
g_standard_compress_middleware = cwist_mw_compress(1024);
cwist_app_use(app, compatibility_compression_middleware);
CWIST_LOG_INFO("Compression middleware registered (brotli > zstd > gzip, Firefox identity compatibility mode, min 1 KiB)");
engine_routes_register(app);
if (g_config.use_tls) {
if (g_config.use_http3) {
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);
} else if (g_config.use_http2) {
CWIST_LOG_INFO("Starting server on port %d (HTTP/2)", g_config.port);
printf("Docker Blog: https://localhost:%d (HTTP/2)\n", g_config.port);
} else {
CWIST_LOG_INFO("Starting server on port %d (HTTPS)", g_config.port);
printf("Docker Blog: https://localhost:%d\n", g_config.port);
}
} else {
if (g_config.use_http2) {
CWIST_LOG_INFO("Starting server on port %d (HTTP/2, plain)", g_config.port);
printf("Docker Blog: http://localhost:%d (HTTP/2)\n", g_config.port);
} else {
CWIST_LOG_INFO("Starting server on port %d (HTTP/1.1, plain)", g_config.port);
printf("Docker Blog: http://localhost:%d\n", g_config.port);
}
}
int rc = cwist_app_listen(app, g_config.port);
cleanup_stop();
cleanup_join();
engine_nats_stop();
engine_pool_shutdown();
cleanup_close_wake_fd();
db_comment_close();
db_board_tree_close();
cwist_app_destroy(app);
fly_crypto_cleanup();
return rc;
}
구간 추리기
코드를 읽은 후 아래 구간을 추려 보자.
if (g_config.use_http2) {
cwist_app_use_https2(app, true);
CWIST_LOG_INFO("HTTP/2 enabled");
}
if (g_config.use_http3) {
cwist_app_use_https3(app, true);
CWIST_LOG_INFO("HTTP/3 enabled");
if (app->h3_ctx && app->h3_ctx->ssl_ctx) {
#if defined(SSL_CTX_set_early_data_enabled)
SSL_CTX_set_early_data_enabled(app->h3_ctx->ssl_ctx, 1);
#elif defined(SSL_CTX_set_max_early_data)
SSL_CTX_set_max_early_data(app->h3_ctx->ssl_ctx, 0xFFFFFFFF);
#endif
SSL_CTX_set_session_cache_mode(app->h3_ctx->ssl_ctx, SSL_SESS_CACHE_SERVER);
SSL_CTX_set_num_tickets(app->h3_ctx->ssl_ctx, 2);
CWIST_LOG_INFO("HTTP/3 0-RTT early data and session resumption enabled");
}
}
이 구간에서 HTTP/2, HTTP/3를 쓰면서 TLS 인증까지 활성화하겠다는 구문은 압축되어 있다.
CWIST
| HTTP 버전 | 구문 |
|---|---|
| 1.1 | cwist_app_use_https |
| 2 | cwist_app_use_https2 |
| 3 | cwist_app_use_https3 |
만약 이를 순차적으로 적용하면, 'HTTP/1.1 활성화 및 TLS 활성화->HTTP/2 활성화 및 TLS는 이미 활성화되어 있음->HTTP/3 활성화, TLS는 이미 활성화되어 있음->HTTP/2와 HTTP/3를 활성화하며 TLS 인증을 함'과 같은 순서가 된다.
이제 그 함수들의 실체를 보자. CWIST 프레임워크의 src/sys/app/app.c에 있다.
/**
* @brief Enable TLS for the application using the supplied certificate pair.
* @param app Application being configured.
* @param cert_path PEM certificate chain path.
* @param key_path PEM private key path.
* @return Tagged CWIST error describing success or failure.
*/
cwist_error_t cwist_app_use_https(cwist_app *app, const char *cert_path, const char *key_path) {
cwist_error_t err = make_error(CWIST_ERR_INT16);
if (!app || !cert_path || !key_path) {
err.error.err_i16 = -1;
return err;
}
app->use_ssl = true;
if (app->cert_path) cwist_free(app->cert_path);
if (app->key_path) cwist_free(app->key_path);
app->cert_path = cwist_strdup(cert_path);
app->key_path = cwist_strdup(key_path);
if (!app->cert_path || !app->key_path) {
if (app->cert_path) {
cwist_free(app->cert_path);
app->cert_path = NULL;
}
if (app->key_path) {
cwist_free(app->key_path);
app->key_path = NULL;
}
err.error.err_i16 = -1;
return err;
}
if (app->use_https3 || app->use_http3) {
cwist_app_refresh_http3_context(app);
}
return cwist_app_refresh_https_context(app);
}
cwist_error_t cwist_app_use_https2(cwist_app *app, bool enabled) {
cwist_error_t err = make_error(CWIST_ERR_INT16);
if (!app) {
err.error.err_i16 = -1;
return err;
}
app->use_https2 = enabled;
cwist_app_refresh_https_request_handler(app);
err.error.err_i16 = 0;
if (!app->use_ssl || !app->cert_path || !app->key_path) {
return err;
}
return cwist_app_refresh_https_context(app);
}
cwist_error_t cwist_app_use_https3(cwist_app *app, bool enabled) {
cwist_error_t err = make_error(CWIST_ERR_INT16);
if (!app) {
err.error.err_i16 = -1;
return err;
}
app->use_https3 = enabled;
err.error.err_i16 = 0;
if (!app->use_ssl || !app->cert_path || !app->key_path) {
return err;
}
cwist_app_refresh_http3_context(app);
return cwist_app_refresh_https_context(app);
}
막상 이 구간을 추려도 HTTPS 컨텍스트를 초기화하는 것은 다른 곳에 들어 있다.
*/
static cwist_error_t cwist_app_refresh_https_context(cwist_app *app) {
cwist_error_t err = make_error(CWIST_ERR_INT16);
if (!app || !app->cert_path || !app->key_path) {
err.error.err_i16 = -1;
return err;
}
if (app->ssl_ctx) {
cwist_https_destroy_context(app->ssl_ctx);
app->ssl_ctx = NULL;
}
cwist_https_options options = {
.enable_http2 = app->use_https2,
.enable_http3 = app->use_https3
};
return cwist_https_init_context_with_options(&app->ssl_ctx,
app->cert_path,
app->key_path,
&options,
app);
}
이것은 존재하는 컨텍스트를 소멸시킨 후, 새로운 HTTPS 컨텍스트를 옵션과 함께 시작한다.
다음 명령은 안 봐도 뻔하다. cwist_https_init_context_with_options를 색인하자. 이는 src/net/http/https.c에 있다.
cwist_error_t cwist_https_init_context_with_options(cwist_https_context **ctx,
const char *cert_path,
const char *key_path,
const cwist_https_options *options,
cwist_app *app) {
cwist_error_t err = make_error(CWIST_ERR_INT16);
bool enable_http3 = options && options->enable_http3;
bool enable_http2 = options && (options->enable_http2 || enable_http3);
if (!ctx || !cert_path || !key_path) {
err.error.err_i16 = -1;
return err;
}
// Initialize OpenSSL
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
const SSL_METHOD *method = TLS_server_method();
SSL_CTX *ssl_ctx = SSL_CTX_new(method);
if (!ssl_ctx) {
return make_ssl_error("Unable to create SSL context");
}
if (SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION) != 1) {
SSL_CTX_free(ssl_ctx);
return make_ssl_error("Unable to enforce TLS minimum version");
}
cwist_https_apply_base_tls_defaults(ssl_ctx);
if (!cwist_tls_apply_pqc_layer(app, ssl_ctx)) {
SSL_CTX_free(ssl_ctx);
return make_ssl_error("PQC layer configuration failed");
}
if (enable_http2) {
err = cwist_https_apply_http2_tls_profile(ssl_ctx);
if (err.errtype != CWIST_ERR_INT16 || err.error.err_i16 != 0) {
SSL_CTX_free(ssl_ctx);
return err;
}
}
// Load Cert and Key
if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) <= 0) {
SSL_CTX_free(ssl_ctx);
return make_ssl_error("Unable to load certificate");
}
if (cwist_tls_autoload_intermediates(ssl_ctx) < 0) {
SSL_CTX_free(ssl_ctx);
return make_ssl_error("Unable to complete certificate chain");
}
if (SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
SSL_CTX_free(ssl_ctx);
return make_ssl_error("Unable to load private key");
}
// Verify key matches cert
if (!SSL_CTX_check_private_key(ssl_ctx)) {
SSL_CTX_free(ssl_ctx);
return make_ssl_error("Private key does not match certificate");
}
*ctx = (cwist_https_context*)cwist_alloc(sizeof(cwist_https_context));
if (!*ctx) {
SSL_CTX_free(ssl_ctx);
err.error.err_i16 = -1;
return err;
}
(*ctx)->ctx = ssl_ctx;
(*ctx)->http2_enabled = enable_http2;
(*ctx)->http3_enabled = enable_http3;
SSL_CTX_set_alpn_select_cb(ssl_ctx,
cwist_https_alpn_select_cb,
(void *)*ctx);
err.error.err_i16 = 0; // Success
return err;
}
앱 로직에는 추상화된 로직만 들어가고, 실질적인 SSL 처리는 모두 https.c에 몰려 있다.
왜 프레임워크 설계는 이와 같이 수행되며, 디버깅이 어려움에도 이렇게 진행하는가?
기능 구현과 버그 수정의 함정
우리가 이와 같이 순서대로 파일을 색인하며 버그를 수정할 때에는 일일이 상위, 하위 로직을 IDE에서 찾아 가며 알아 보는 작업이 매우 비효율적으로 느껴진다.
하지만, 조직 내 자체 프레임워크로 웹 서버를 작성 후 HTTPS 기능에 문제가 생겼다고 쳐 보자.
1안은 추상적인 계층은 모두 프레임워크의 앱 파트에 몰려 있고, HTTPS는 별도의 파일에 들어가 있다.
2안은 추상적인 계층과 HTTPS 모두 한 파일에 들어가 있고, 내부 호출이 분리되지 않아 흐름대로 작성되어 있다.
만약 통시적인 버그를 잡아 해결하려 한다면 의외로 2안이 도움될지 모르지만, HTTPS에만 국한된 버그를 잡고 싶을 경우 1안대로 분리되어 있는 것이 로직이 순수하게 분리된다.
통시적인 버그에서의 비용은 싸다
분리되지 않은 파일에서 국소적인 버그에서의 비용은 수천, 수만 줄의 거대 파일을 읽으며 무엇이 무엇인지 노트에 모두 일일이 옮겨 적거나, LLM 코더에게 대규모 분류 작업을 시켜 줄 번호를 추적하게 하는 것이다.
잘 분리된 파일에서 통시적인 버그에서의 수정은 차례대로 import나 include 등의 구문을 추적하며 파일의 상하위 관계만 적당히 트리로 만들면, 문제가 생겼을 때 그 문제의 원인을 찾기 위해서는 상향식 혹은 하향식으로 단계적 색인만 하면 된다.
파일을 찾는 비용 역시 싸지는 않지만, IDE에서는 단축키가 너무 잘 되어 있고, CLI/TUI 개발 환경에서도 grep만으로 특정 함수를 정의하는 파일을 찾을 수 있다.
그렇기 때문에 어느 한 쪽 설계가 완벽하게 정당화되지 않더라도 기회 비용을 덜 날리는 차악을 선택하여 개발하게 되는 것이다.
이와 같은 관점은...
이와 같은 관점을 UX에 적용해 보면, 거대한 파사드 패턴을 반복하더라도 사용자가 '함수 한 개'만 알면 개발할 수 있게 하느냐, 혹은 개발할 때 조금 피로해지더라도 사용자가 각 기능을 명확하게 이해하고, 나아가 버그 제보를 쉽게 할 수 있게 하느냐의 문제로 귀결된다.
이 경우에도 절대적인 정답은 없다. 다만 사용자의 특성을 고려하여 두 전략 중 잘 골라 향후 배움이 되게 하는 것이 중요하다.