메뉴
HN
Hacker News • 50일 전

웹 서버 배포 모델은 취미 수준에서 한계에 부딪힌다

IMP
6/10
핵심 요약

사용자가 직접 자신의 서버에 호스팅할 수 있도록 배포되는 웹 애플리케이션은 정적 파일 서빙 및 캐싱 측면에서 큰 비효율성을 겪습니다. 엔터프라이즈 환경과 달리, 개인 서버 환경에서는 시스템 구성 요소가 단편화되어 있어 개발자가 최적의 성능을 내기 어렵고 잦은 호환성 문제를 감당해야 합니다.

번역된 본문

2026-08 웹 서버 배포 모델은 취미 수준(개인 서버 규모)에서 한계에 부딪힌다.

만약 당신이 다른 사람들이 자신의 서버에 호스팅할 수 있도록 웹 기반 무언가를 만들고자 한다고 가정해 보자. 안타깝게도 이러한 생각을 하는 순간, 사설로 호스팅되는 다른 소프트웨어들이 활용할 수 있는 여러 효율적인 기법들을 당신의 코드베이스는 사용할 수 없게 된다. 그리고 다른 사람들이 이미 더 잘 만들어둔 여러 기능들을 당신이 직접 처음부터 다시 만들어야 하는 짐을 지게 된다.

요구 사항이 변경됨에 따라 추가할 수 있는 기본적인 구성부터 시작해 보자:

  • TLS 처리: 애플리케이션과 분리하여 개인 키가 필요 이상으로 노출되지 않도록 보호한다. 이를 통해 인증서 획득 방식을 변경하더라도 애플리케이션 코드에 영향을 주지 않는다.
  • 애플리케이션 자체

대부분의 경우 첫 번째 항목(TLS 처리)은 리버스 프록시(Reverse proxy)를 포함한 여러 기능을 갖춘 성능이 꽤 괜찮은 웹 서버일 가능성이 높다. 아주 흔한 기능 중 하나가 정적 파일(static files)을 제공하는 것인데, 현재 이는 당신의 애플리케이션이 처리하고 있다.

정적 파일 (Static files)

정적 파일 제공을 리버스 프록시로 오프로드하는 것은 나쁜 생각이 아니다. 리버스 프록시의 구현이 당신이 선택한 최신 힙한 프레임워크에 내장된 기능보다 훨씬 성능이 좋을 확률이 높다. 설령 그렇지 않더라도, 정적 파일 요청이 파이썬이나 노드(Node) 웹 서버가 동시에 처리할 수 있는 적은 양의 요청을 막아버리는 일(병목 현상)은 더 이상 발생하지 않을 것이다.

이 시도를 해본다고 가정하자. 당신의 앱이나 리버스 프록시 중 하나라도 컨테이너화되어 있거나 다른 방식으로 격리되어 있다면, 이제 당신의 앱을 설치하는 모든 관리자는 리버스 프록시가 앱에 포함된 정적 파일에 접근할 수 있도록 하나 또는 두 컨테이너 모두에 구멍(포트)을 뚫어야 한다.

앱을 마침내 출시한 후, 리버스 프록시로 오직 리버스 프록싱만 전담하고 쿠버네티스(Kubernetes)의 일부로만 배포되는 이른바 "클라우드 네이티브(cloud native)" 제품을 사용하는 사람으로부터 이슈 리포트를 받게 된다. 당신은 풀이 죽어 앱이 파일을 직접 제공하게 하는 옵션을 다시 추가하고, 사람들은 그 기능이 존재한다는 사실을 깨닫자마자 즉시 그것을 켠다. 당신이 타겟으로 하는 오디언스가 접근할 수 있는 저사양 하드웨어의 잠재력을 끌어내리는 비효율성의 단면에 기여했다는 사실을 깨닫고 눈물을 흘린다.

반면 "프로페셔널(전문가)" 규모에서는 어떨까? 정적 파일은 완전히 분리되어 배포된다. 십중팔구 S3 버킷이나 비슷한 곳에 배포되고 실제 CDN™ 의 뒷단에 위치하므로 이러한 문제가 애초에 발생하지 않는다.

미인증 캐싱 (Unauthenticated caching)

당신은 애플리케이션이 사람이나 봇에 의해 방대한 양의 '미인증(로그인 필요 없는) 방문'을 받을 것임을 인지한다. 미인증 방문은 항상 동일한 응답을 받으므로, 변경된 사항이 없다면 이 응답들을 매번 다시 계산할 실질적인 이유가 없다. 당신의 상황에서 1시간 정도면 "오래된(stale)" 페이지로 간주하기에 적절한 시간이라고 결정한다.

당신은 Varnish 캐시 웹사이트를 애정 어린 시선으로 바라보지만, 당신이 이미 존재하는 개인 사용자용 웹 서버라는 거대한 루브 골드버그 머신의 일부에 불과하기 때문에 이를 필수 요구 사항으로 둘 수 없다는 사실을 잘 알고 절망한다.

실망한 채로, 당신은 엔드포인트에 필요한 cache-control 및 vary 헤더를 추가한다. 그러다 no-vary-search 기능을 발견하지만, 그것이 크롬(Chrome)에서만 작동한다는 사실을 깨닫는다. 당신은 응답을 변경시킬 정확한 쿼리 매개변수 세트를 알고 있지만, 현재 위치에서는 아무것도 할 수 없다. 만약 당신이 Varnish에 의존할 수만 있었다면, 사용되지 않는 쿼리 매개변수들을 제거할 수 있었을 것이다. 그래야 링크 미리보기에 관한 죽은 농담을 되살리려는 짜증나는 사람들이 링크 끝에 ?qweqweawe를 추가하여 당신의 캐시를 우회하려는 시도를 막을 수 있을 텐데 말이다.

당신은 사용 중인 웹 서버 라이브러리에 캐싱 미들웨어가 있다는 것을 발견한다. 기능 목록을 읽어보니, 운이 좋으면 cache-control의 TTL과 vary 정도를 지원할 뿐 그 이상은 지원하지 않는다는 것을 깨닫는다. 당신은 아무도 이상한 짓(캐시 우회 등)을 시도하지 않기를 바라는 마음 한 스푼의 희망만을 품은 채 그것을 추가한다. 이미 좋은 캐시를 구축한 관리자들이 최고의 효율성을 위해 이를 미세 조정해주기를 바라며 구성 스위치와 문서를 제공하지만, 그들은 결코 그렇게 하지 않는다.

앱을 출시하자마자, Caddy 사용자로부터 이슈 리포트를 받게 된다. 그들이 믿고 의지했던 Caddy의 믿을 수 없을 정도로 형편없는 캐싱 플러그인이 마음대로 응답을 훼손해버렸기 때문이다. 또 다른 누군가는 엔진엑스(nginx)의 캐시를 활성화했지만 그들의 설치본은 끊임없이 지...

원문 보기
원문 보기 (영어)
2026-08 the web server deployment model breaks at hobby scale lets say you want to make a web thing that you intend other people to host on their own servers. unfortunately, by thinking this very thought you immediately locked your codebase out of several efficiency tricks other software intended to be hosted privately can make use of, and you have now burdened yourself with reinventing several wheels others have already done better. let’s start out with a baseline, which we’ll add to as requirements change: something to take care of tls separate from the application so your private keys aren’t exposed wider than necessary lets you change how you get your certs without impacting application code the app in pretty much every case, the first bit will likely also be a relatively capable web server that includes reverse proxying as one of many features. one very common feature is to serve static files, which is currently handled by your application. # static files it’s not a bad idea to offload static file serving to the reverse proxy. it’s implementation will likely be way better than whatever comes with the hip new framework you chose, and even if not, static files will no longer clog up the mediocre amount of requests your python or node web server can handle concurrently. you attempt this. if either one of your app or the reverse proxy is containerized or otherwise compartmentalized, every admin setting up your application now has to poke a hole in one or both of these compartments to let the reverse proxy access the static files distributed with the app. once your app is finally released, you get an issue report from someone whose reverse proxy is one of those “cloud native” ones that does nothing but reverse proxying and is only distributed as part of kubernetes. you dejectedly add back in an option to let the app serve the files, which everyone turns on the instant they realize it exists. you weep as you realize you have contributed to the fair share of inefficiency bogging down the potential of the lower end hardware your target audience has access to. in “professional” scale? static files are deployed completely separately, likely to an s3 bucket or somesuch and fronted by an Actual Cdn™, so this issue never comes up. # unauthenticated caching you recognize your application will get a lot of unauthenticated visits, either by humans or bots. unauthenticated visits always get the same response, so there’s no real reason to re-compute these responses if nothing changed. you determine that an hour is an adequate amount of time for a “stale” page in your context. you look fondly at the vinyl cache website, with full knowledge you can’t actually require it as you’re just one part of the already existing rube goldberg machine of a hobbyist’s web server. dejected, you add the necessary cache-control and vary headers to your endpoints. you find out about no-vary-search and realize it’s chrome only. you know the exact set of query parameters that will change the response, but there’s nothing to do from where you are. if you could depend on vinyl, you could trim out the unused query parameters, so annoying people trying to resurrect a dead joke about link previews don’t try to bypass your cache by adding ?qweqweawe after your links. you find out your web server library has a caching middleware. you read through the features it supports and realize it doesn’t support anything other than cache-control ‘s ttl and perhaps vary if you’re lucky. you add it in, with nothing but a glimmer of hope in your heart that nobody tries anything funny. you put a configuration switch and documentation in hopes that admins who already have a good cache set up will tweak it for best efficiency. they don’t once your app is released, you get an issue from a caddy user, because they trusted caddy’s unbelievably mediocre caching plugin which will gladly corrupt responses if it feels like it. someone else has enabled nginx’s cache but their install keeps getting bogged down anyway because they forgot to enable proxy_cache_lock . someone else has put their site behind cloudflare but enabled your cache option as well because they didn’t have a cache set up on their own machine. you cringe at the amount of memory wasted. you also realize your builtin caching middleware is caching your static files. serving those are relatively trivial and likely already cached by your OSs disk caching facilities, so you’re just bloating your memory for nothing. because your middleware only looks at cache-control , you can’t do anything without preventing browsers from also caching your best-practice immutable static files on the client’s end. you weep. in “professional” scale? they control the entire machine, so they can indeed set up vinyl or something along it’s lines and tune it for the exact cache behavior they want. well, ok, they probably outsource it to a cdn which does a mediocre job at it, but they have the option to control caching this granularly. # authenticated caching you recognize your application may get quite a lot of authenticated visits as well. maybe you’re doing something federated, where you have remote instances authenticating with you. the data they fetch still won’t change that often, and certainly won’t change between different requesters. there’s no reason to re-compute those either. you determine a shorter timeout, say, a minute, is adequate for a “stale” response. this cache is only truly meant to alleviate bursts of requests. because you know you’ll have to run your code before the cache anyhow, you’re confident you can invalidate the cached data when it changes. you quickly realize if you want to support external caches, your only options are write plugins for each cache middleware or, more realistically, just one supported cache middleware and leave the rest “to the community” (nobody will bother) write a reverse proxy that handles your authentication and passes the result into the cache middleware as appropriate, with a header or something you can vary on. you go with neither, as both options complicate the deployment of your software. you are very attentive to the fact that people will not bother with deploying your software if the instructions are more than run it point your reverse proxy at it to the point where there’s an entire ecosystem of “one-click deployment” middleware and operating systems, which package up everything into a nice point and click interface for newcomers. you realize the advantages of this, letting more people control their own data is a good idea. you support the concept even if you weep at the existing implementations of it. so you take the only remaining option. you stop supporting external cache middleware, and move the authentication middleware to run earlier than the caching middleware in your application’s code. you weep, because the cache middleware in your framework sucks. in “professional” scale? again, they control the entire machine. they can pick either solution and make it work. # the best programming language for the job you have enough interactivity to warrant a single-page-application as your frontend. however, you also want people without javascript enabled to be able to read the bits that don’t require interaction. perhaps this interactivity only makes sense for logged-in users. you decide to use a server-side-rendered single-page-app framework. every single competent option is written in javascript. this makes sense, you don’t want to write the same code twice, both for the backend and for the frontend. your application, however, is not written in javascript. you briefly have a really bad idea, which is to embed nodejs into your application . this technically works, but feels very fragile. the single-threaded nature of nodejs also means you’ll want multiple frontend processes to load balance between. additionally, you want your frontend and application to be separate in various resource use metrics, so you can tell where