C로 만든 현대적인 웹 앱

A Modern Web App in C

prophitt.me ▲ 381 댓글 159 prophittcorey

요약

C 프로그램 하나와 SQLite 파일만으로 오늘날의 웹 서비스를 충분히 운영할 수 있습니다.

개발자 코리 프로핏이 유료 웹 서비스 marketyour.games를 C로 만들어 CGI(요청마다 프로그램을 새로 실행해 응답하는 옛 방식)로 띄운 경험을 정리했습니다. 프레임워크와 컨테이너 없이 OpenBSD, C, httpd, SQLite만 썼습니다.

왜 중요한가

  • 작은 서비스라면 프레임워크와 컨테이너 없이 바이너리 하나로도 충분함을 실제 유료 서비스로 보여 줍니다.
  • 의존성이 컴파일러와 라이브러리 세 개뿐이어서 공급망 위험(외부 패키지를 거쳐 들어오는 보안 위험)이 크게 줄어듭니다.
  • 저자는 CGI가 밀려난 이유가 2004년에는 타당했지만 지금은 맞지 않는다고 봤습니다.

핵심 내용

  • 회원 계정, 콘텐츠 제작자 검색 대시보드, Stripe 결제, Postmark 메일이 자바스크립트 없이 동작합니다.
  • 정적 파일까지 넣은 바이너리 하나만 바꾸면 배포가 끝나서 재시작이나 중단이 없습니다.
  • 요즘 커널은 프로세스 생성 비용이 낮고, 병목이 생기면 FastCGI로 옮기면 된다고 했습니다.
  • OpenBSD의 pledge·unveil과 httpd의 chroot로 프로그램이 접근할 수 있는 범위를 좁혔습니다.
  • CGI에는 쿠키, 폼 해석, 출력 이스케이프 기능이 없어서 cgi.h라는 헤더 하나로 직접 만들었습니다.

HN 반응

  • busybox와 lighttpd의 CGI 지원을 소개하거나 C와 FastCGI로 식당 주문 시스템 사업을 꾸렸다는 경험담이 이어지며 단순한 방식에 공감하는 의견이 많았습니다.
  • 반면 인터넷에 노출된 C 서버는 메모리 안전 문제를 피하기 어렵다며 CGO_ENABLED=0으로 빌드한 Go 단일 바이너리나 Rust가 낫다는 반론도 나왔습니다.

댓글

30개 표시 · 전체 159개
  1. ramses0 HN

    제가 제일 좋아하는 잡학은 busybox에 cgi-bin 지원이 기본으로 들어 있다는 겁니다(rclone serve webdav도 글에서 언급하지 않은 게 아쉽습니다). https://elixir.bootlin.com/busybox/1.37.0/source/networking/httpd.c#L2401

    글에서 말한 내용은 전부 맞고 논리도 탄탄합니다. 특히 권한을 낮추려고 pledge/chroot 등을 넣은 부분이 그렇습니다.

    배포 방식은 정반대인 두 갈래가 있습니다.

    1. 잡동사니를 탑처럼 쌓아 올립니다. 원격 컴퓨트에서 돌아가는 하이퍼바이저 아래 k8s 클러스터의 일부인 proxmox 서버 속 VM 안의 도커 컨테이너 위에서 고작 echo "hello world" 하나를 돌리는 식이죠...

    2. 그냥 bash -c 'echo "hello world"'를 실행하고 앞에 CGI를 붙입니다(상태 없음, 바이너리 하나, 어쩌고저쩌고...).

    https://github.com/Domain/bash.cgi 를 한번 보세요... 'busybox 위에서 cgi-bin으로 bash를 돌리는 것'이 "올바른 아키텍처"라는 얘기는 아닙니다. 진짜 "프레임워크 없는" 상태에 거의 다다르는 데 발목을 잡는 코드의 양이 아주 적고 이해하기 쉽다는 걸 보여 주려는 겁니다.

    업무용 내부 도구를 만들 때는 요즘 이런 식으로 가는 편입니다.

        some-cmd [--json|--markdown] ...yet-more-args...
    

    프로세스와 셸 환경 자체를 repl처럼 쓰다 보면, [--cgi-bin]도 그저 출력 형식 하나가 되는 쪽으로 흘러갑니다. 그리고 명령이 서로 독립적이고, 재현 가능하며, 입력 인자에만 의존하게 만드는 능력(아니, 선견지명이죠!) 덕분에 "적대적인" 환경(firecracker VM이나 람다 방식)에서도, 아주 "편안한" 환경(셸, 디버거, 단위/통합 테스트)에서도 그대로 돌아가게 됩니다.

  2. rpdillon HN

    제 경험과 똑같네요. busybox를 말씀하시니 신기한데, 저도 얼마 전에 busybox가 busybox httpd -p 9000 -h . -f -v로 웹 서빙을 할 수 있다는 걸 알았습니다. 현재 디렉터리에 cgi-bin 폴더만 넣어 두면 됩니다.

    저는 이미 Bottle로 Python 서비스를 만들어서 독립 실행형 서버로도, CGI 스크립트로도 돌아가게 해 두었고, 이제 busybox의 더 제한적인 CGI 환경에 맞게 고치는 중입니다(기본적으로 .htaccess 재작성 규칙이 없는 상황을 처리할 수 있어야 합니다).

    일회용·소모성·개인용 소프트웨어가 더 많이 만들어지면서 간단한 배포 도구가 다시 뜰 가능성이 분명히 보입니다. busybox + lua 같은 조합은 시스템 요구 사양이 극히 낮은데, "DDR4면 충분하다"는 요즘 분위기와도 잘 맞습니다. 저는 오래된 Raspberry Pi 2B에서 돌리고 있는데, 정기적으로 접속하는 10명 안팎에게는 성능이 충분합니다.

  3. MomsAVoxell HN

    저는 busybox+Lua 대신 TurboLua를 씁니다.

    https://turbo.readthedocs.io/en/latest/

    이벤트 기반에 빠르고, http, mustache 템플릿, 암호화까지 네트워킹 기본기가 다 갖춰져 있습니다. 실행 파일은 가볍고 작아서 임베디드 용도로 아주 좋고, 시그널과 fd 처리도 내장돼 있고, 쓰기도 아주 쉽습니다...

    백만 달러짜리 프로젝트에 아주 성공적으로 썼고, 자원이 제한된 시스템(RAM 512메가)에서 돌아가는 과학 애플리케이션에도 썼습니다..

    웹 프런트엔드와 연결할 C 함수를 붙이는 것도 아주 쉬웠습니다. 이럴 때 libffi가 좋은데, 잘 짜인 표준 Lua/C 애플리케이션처럼 확장하는 것도 아주 쉽습니다..

  4. rpdillon HN

    제가 딱 관심 있는 분야입니다. 특히 Lua+배터리 방식의 여러 시도가 기대돼서, 직접 설계해 볼까(네트워킹, JSON, 템플릿, DB 라이브러리 바인딩을 갖춘 C 래퍼) 알아본 적도 있습니다. 그런데 Redbean이 거의 이상적이라서 전체 설계를 넘어 코드까지 파고들지는 못했습니다.

    그래도 Turbo는 근사해 보이네요! 제가 채우고 싶던 바로 그 틈새입니다. 프로토타입을 한두 개 만들어 보고 어떤지 판단해 보겠습니다. 알려 주셔서 정말 감사합니다!

  5. MomsAVoxell HN

    별말씀을요. 우리는 관심사가 같네요. 저도 Lua+배터리 방식으로 사는 걸 좋아합니다. 여러 상업 프로젝트에서 Lua를 아주 성공적으로 써 왔기 때문에, Lua를 처음 접한 초심자들이 흔히 투덜대는 흠과 가시에도 불구하고 Lua의 힘을 확신합니다.

    TurboLua가 특히 좋은 이유는 네트워크 중심 애플리케이션에 필요한 거의 모든 I/O 방식이 정말로 통합돼 있고, 시그널 핸들러와 fd 집합 같은 것에 연결해서 제대로 조율하기 쉬운 꽤 괜찮은 런타임 루프까지 있기 때문입니다. 임베디드 작업을 아주 빨리 하는 방법으로도 이만한 게 없습니다. gpio, 시그널 처리 같은 것도 아주 쉽습니다.

    행운을 빕니다. 처음 한 번 써 보시고 시간 되시면 생각을 공유해 주세요. 기꺼이 듣겠습니다..

  6. OleksandrC HN

    독특한 웹 애플리케이션에 관심 있는 분들을 위해 말씀드리면, 저는 C++로 게임 프로젝트를 만들고 있습니다. 클라이언트는 Emscripten으로 WebAssembly로 컴파일하고, 애플리케이션 서버도 C++로 구현했습니다(nginx 뒤에 둡니다).

    AI 시대 이전부터 해 온 작업입니다. 게임은 문명 같은 장르입니다. 3D 부분과 웹 처리 전부를 포함해 엔진을 맨땅에서 직접 만들었습니다.

    한번 보세요: https://statebuilder.com/

  7. hughw HN

    아름답네요.

  8. nmstoker HN

    멋지네요. 댓글이 기술적인 부분에 초점을 맞춘 건 알지만, 게임을 확실히 너무 낮춰 소개하셨어요!

  9. Teever HN

    저도 비슷한 걸 하고 있는데, C와 WebAssembly, Emscripten으로 만드는 Red Alert++ 같은 게임입니다.

    여가 시간에 조금씩 만지던 건데, 지난주에 올라온 옛날 게임을 디컴파일해서 웹 앱으로 다시 만든 글들에 자극받아 다시 손을 잡게 됐습니다. 그 게임들의 성능을 비판한 사람이 많았다는 점이 흥미롭습니다. 최근의 심시티 복제작은 제 노트북 팬이 확실히 윙윙 돌게 했는데, 제 게임에는 그런 성능 문제가 전혀 없습니다. 성능 면에서는 WebAssembly와 Emscripten이 놀랍고, 저는 아직 SIMD나 멀티코어 기능을 넣지도 않았습니다.

    이런 식으로 만들면 좋은 점 하나는 데스크톱 버전이 3D 모델/이미지 생성기와 쉽게 연동된다는 겁니다. 게임에 쓸 에셋을 만들 때 데스크톱 유닛 에디터에서 그렇게 쓸 생각입니다.

    제가 구상하는 파이프라인은 이렇습니다. 3D 모델 생성기가 모델을 Blender로 넘겨서 여러 시점의 스틸 이미지를 만들고, 그걸 stable diffusion 이미지 생성기에 넣어 채색하고 유닛 아이콘 같은 것도 만듭니다. 음성 모델은 유닛을 클릭했을 때 나오는 "출동 준비 완료" 같은 일꾼 대사를 만들고요. 밤새 아무도 지켜보지 않아도 유닛을 잔뜩 만들어 두고, 아침에 확인해서 마음에 드는 것만 게임에 넣을 수 있는 시스템을 그리고 있습니다.

  10. assimpleaspossi HN

    저는 22년 전에 제가 운영하던 식당의 온라인 메뉴 주문 시스템을 개발한 뒤 웹 개발 회사를 시작했습니다. 저 혼자, 이후엔 우리 팀이 글에서 말한 것과 거의 똑같이 했는데, 우리는 fastcgi를 썼습니다. C로 시작한 건 제가 아는 게 C였기 때문입니다. 그게 제 본업이었죠. 너무 잘 돌아가서 그 프랜차이즈가 저한테서 소프트웨어를 사 갔는데, 이후 전부 마이크로소프트 계열로 갈아엎었다가 문제에 문제가 꼬리를 물었습니다. 그건 다른 이야기고요.

    저는 극장 단체나 식당처럼 온라인 주문이 필요한 다른 업장의 사이트도 계속 만들었습니다. 전부 C, fastcgi, FreeBSD, postgresql, h2o-server로 했는데, 남의 규칙에 맞추려고 구부릴 필요 없이 늘 우리가 원하는 대로 되어 줬기 때문입니다. 변경도 빨랐고, 우리가 모르는 사이에 또는 동의 없이 바뀌는 일도 없었습니다. 우리는 아주 잘 해냈습니다.

  11. glouwbug HN

    쓰신 걸 보면 아주 오래전 일처럼 느껴지는데, 22년 전이면 사실상 2005년이네요.

  12. assimpleaspossi HN

    2004년 2월이었습니다.

  13. benlivengood HN

    Golang도 바이너리 하나로 웹사이트나 서비스를 서빙할 수 있습니다. 핵심 Go만 쓰면(CGO_ENABLED=0) libc를 링크하지 않을 수도 있고, 기존 웹 서버(nginx, apache, httpd)를 대체하는 데 필요한 http와 tls 라이브러리도 전부 들어 있습니다.

    저는 C를 좋아하고 예전에 C로 CGI도 몇 개 짜 봤지만, C로 하는 동시성 프로그래밍은 좋아하지 않습니다. fork와 exec는 .text 세그먼트의 읽기 전용 mmap을 공유할 수 있어도 .bss와 .data는 초기화하고 복사해야 하니까요. 스레드 방식 프로세스라면 피할 수 있는 시작 오버헤드가 피할 수 없이 생기고, 날것의 pthread는 좀 고통스럽습니다. 트래픽이 적은 사이트만 서빙하면 된다면 CGI도 괜찮습니다.

    제가 제일 걱정하는 건 SQLite DB를 얼마나 자주 백업하느냐입니다. 요즘 세상에 C로 만든 웹 서버에 RCE가 없을 리가 없으니까요.

  14. ncruces HN

    염치없는 홍보입니다. Go를 중시하고, 그리고 CGO_ENABLED=0을 쓰고, 그리고 SQLite를 쓰면서, 웹 서버의 C 코드와 RCE가 걱정되신다면 제 SQLite 드라이버에 관심을 가져 보시겠어요?

    https://github.com/ncruces/go-sqlite3

    C를 Go로 번역하는 데 쓰는 도구의 보안 목표를 방금 글로 정리했습니다: https://github.com/ncruces/wasm2go/blob/main/SECURITY.md

    개인 취미 프로젝트일 뿐이라 당연히 무엇도 약속할 수는 없지만, 이것이 목표이고 실제로 메모리 손상 버그도 잡아냅니다: https://sqlite.org/bugs/forumpost/543f94984b

  15. Levitating HN

    CGI는 훌륭합니다. 아쉽게도 lighttpd와 달리 nginx는 CGI를 기본으로 지원하지 않습니다.

    nginx에 CGI를 추가해 주는 이 프로젝트가 마음에 듭니다: https://github.com/pjincz/nginx-cgi

    passenger1를 통해 nginx에서 Rack 애플리케이션을 바로 띄우는 방법도 있습니다. Rack은 루비 웹 애플리케이션을 위한 CGI 비슷한 인터페이스입니다: https://github.com/rack/rack 루비 몇 줄이면 애플리케이션과 미들웨어를 쉽게 짤 수 있습니다.

  16. synergy20 HN

    저는 그냥 lighttpd를 씁니다.

  17. Levitating HN

    저도 오랫동안 썼는데, nginx는 설정으로 할 수 있는 게 더 많아서요.

    제가 만든 nginx 설정 템플릿도 있습니다: https://git.levitati.ng/Levitating/nginx.conf

  18. huijzer HN

    그런데 진짜 중요한 질문은 Caddy가 이걸 지원하느냐겠죠?

    답은, 지원은 하는데 커뮤니티 플러그인을 통해서라고요.

  19. BirAdam HN

    fastcgi wrap이요.

  20. assimpleaspossi HN

    저도 바로 그걸 썼는데 글쓴이는 왜 안 썼는지 궁금하네요. 맨 위에 댓글을 달아 두겠습니다.

  21. Levitating HN

    설정이 쉬워서 아닐까요? fcgiwrap에 뭐가 불만이었는지 정확히 기억나진 않습니다.

    써 본 적이 있고 PR도 올렸는데, 그 PR에 lighttpd 메인테이너가 답을 줬습니다: https://github.com/gnosek/fcgiwrap/pull/62

    요점은 헤더가 없는 임의의 응답 본문도 fgciwrap이 받아 주길 바랐다는 겁니다. 그러면 CGI용으로 만든 게 아닌 프로그램과도 쓸 수 있으니까요. lighttpd는 이걸 지원하지만 어쩌다 우연히 그렇게 된 것 같습니다.

  22. unixholder HN

    복잡한 웹 기술과 프레임워크를 깊이 파고들수록 단순함이 이긴다는 게 더 분명해집니다.

  23. hughw HN

    첫 문장에는 동의하지 않습니다. 199x년대에 제가 알던 웹 페이지 제작자들은 cgi-bin에 C 프로그램이 아니라 perl 스크립트를 넣었습니다.

  24. nkozyra HN

    그건 당신보다 조금 더 앞선 시기일 수도 있습니다. Perl이 자리를 잡은 건 확실히 그 직후지만, cgi-bin 초창기에는 C가 주류였고, 그러다 사람들이 웹 앱을 위해 컴파일과 씨름하는 데 질려 버렸죠.

    제 기억에는 스펙 자체에도 C 애플리케이션용으로 만들었다고 적혀 있습니다.

    물론 mod-perl은 구원이었고요.

  25. minaguib HN

    Perl의 CGI.pm은 불과 5개월 전에도 업데이트됐어요 :)

    https://metacpan.org/pod/CGI

  26. forinti HN

    저는 폼이 있는 페이지를 만들어야 해서 Perl을 배웠습니다. 폼 내용을 Postscript로 바꿔서 인쇄해야 했거든요. 선택지는 제가 잘 아는 C와 한 번도 본 적 없는 Perl뿐이었습니다. 그래서 Perl을 배우기 시작했죠. C로 하자니 그 일에는 품이 너무 많이 들어 보였습니다.

  27. ok123456 HN

    CGO=0으로 빌드하고 템플릿과 문서를 전부 내장한 Go 바이너리가 도구와 배포 경험 면에서 전반적으로 더 낫습니다. scratch 컨테이너에 넣으면 완전히 격리되고요.

  28. doublepg23 HN

    CGO=0은 예전 직장에서 큰 도움이 됐습니다. 빌더의 glibc가 실제 운영 서버보다 새 버전이었는데, 거기서 컴파일한 Go 앱을 배포하던 때였죠.

  29. Kuyawa HN

    이제 AI가 Node Express처럼 request, response, 템플릿, 쿠키, 인증, DB, 웹소켓과 몇 가지 다른 라이브러리를 갖춘 기본적인 C 웹 프레임워크를 만들어 낼 때가 됐습니다.

    AI가 직접 쓸 용도라면 엄청나게 빠를 겁니다.

    그냥 재미로요.

  30. nostrademons HN

    어차피 AI를 쓸 거면 왜 Rust가 아니라 C죠?

    C는 메모리 안전성 문제가 생길 수 있고, 인터넷에 노출된 서버 소프트웨어에서는 치명적입니다. 그리고 제가 본 바로는 AI는 메모리 버그가 전혀 없는 소프트웨어를 짜는 데 그다지 능숙하지 않습니다.

    Rust는 프로그래머가 소유권과 아키텍처를 훨씬 더 신중하게 따져 보게 하는 대가로 메모리 안전성 구멍을 막아 줍니다. 그건 바로 AI가 잘하는 일이고요. 그러니 빠른 걸 원하고 어차피 AI로 쓸 거라면, AI한테 Rust 코드를 생성시키지 않을 이유가 있나요?

Hacker News에서 보기 ↗