6P by GN⁺ 4일전 | ★ favorite | 댓글 2개
  • SQLite는 성능, 호환성, 적은 의존성, 안정성 때문에 초기(2000년)부터 C 언어로 개발되었음
  • C는 거의 모든 OS와 언어에서 사용할 수 있으며, 특히 저수준 라이브러리로 빠른 동작을 지원함
  • 객체지향 언어 대신 C를 택한 이유는 확장성, 다양한 언어에서의 호출, 그리고 개발 당시 C++과 Java의 미성숙 때문임
  • SQLite는 의존성이 거의 없는 단일 파일 구조로, C 표준 라이브러리의 최소한의 함수만 사용함
  • Rust와 Go와 같은 "안전한 언어" 에서 재작성 논의가 있으나, 퀄리티 관리, 성능, 라이브러리 호출성 등에서 여전히 C가 우위

1. C가 최적의 선택인 이유

  • SQLite는 2000년 5월 29일 최초 개발 이후 지금까지 C 언어로 유지되고 있음
    • 현재로선 다른 언어로 재작성할 계획은 없음
  • C는 하드웨어에 가까운 제어력을 가지면서도 이식성이 뛰어나, “휴대 가능한 어셈블리 언어”로 불림
  • 다른 언어들이 “C만큼 빠르다”고 주장할 수는 있지만, C보다 빠르다고 주장하는 언어는 없음

1.1. 성능

  • SQLite 같은 저수준 라이브러리는 빈번히 호출되므로 매우 빠르게 동작해야 하는 요구가 있음
  • C 언어는 빠른 코드를 작성하는 데 적합하며, 이식성이 높으면서도 하드웨어에 밀접하게 접근 가능함
  • 다른 현대 언어들도 ‘C만큼 빠르다’고 주장하지만, 범용 프로그래밍에서는 C보다 빠르다고 확신하는 언어는 없음
  • C는 메모리와 CPU 자원을 세밀히 제어할 수 있어 파일 시스템보다 35% 빠른 성능을 보이기도 함

1.2. 호환성

  • 거의 모든 시스템이 C로 작성된 라이브러리를 호출할 수 있음
  • 예를 들어, Android(Java 기반)에서도 adaptor를 통해 SQLite를 사용할 수 있음
  • 만약 SQLite를 Java로 작성했다면 iPhone(Objective-C, Swift)에선 사용 불가하여 범용성이 크게 떨어짐

1.3. 낮은 의존성

  • C 라이브러리로 개발했기 때문에 런타임 의존성이 매우 적음
  • 최소 구성에서는 표준 C 라이브러리의 아주 기본 함수(memcmp(), memcpy(), memmove(), memset(), strcmp(), strlen(), strncmp())만 사용
  • 보다 완전한 빌드에서도 malloc(), free(), 파일 입출력 등 소수의 의존성만 가짐
  • 현대 언어들은 다수의 대형 런타임수천 개의 인터페이스가 요구되는 경우가 많음

1.4. 안정성

  • C는 오래되고 변화가 적은 지루한 언어이지만, 이는 곧 예측 가능성과 안정성을 의미함
  • SQLite처럼 작고, 빠른, 신뢰할 수 있는 데이터베이스 엔진을 만들 때는 규격이 자주 바뀌지 않는 언어가 적합함
  • 언어의 사양이나 구현이 빈번히 바뀌면 SQLite 안정성에 불리함

2. 왜 객체지향 언어로 작성되지 않았는가

  • 일부 개발자들은 객체지향이 아니라면 복잡한 시스템 SQLite를 구현하기 어렵다고 생각하지만, C에 비해 C++나 Java로 라이브러리를 만들면 다른 언어에서 부르기 어려움
  • Haskell, Java 등 다양한 언어 지원을 위해 C 라이브러리 선택이 타당함
  • 객체지향은 언어가 아니라 설계 패턴이므로 특정 언어에 한정되지 않음
    • C에서도 구조체와 함수 포인터로 객체지향 패턴 구현 가능
  • 객체지향이 항상 최적의 구조는 아니며, 절차적 코드가 더 명확하고 관리가 쉬우며, 더 빠른 결과를 얻을 때도 있음
  • SQLite 개발 초기(2000년경)에는
    • Java는 미성숙했고
    • C++은 컴파일러 간 호환성 문제가 심각했음
      → 당시에는 C가 가장 실용적이고 안전한 선택이었음
  • 현재도 SQLite를 재작성할 만한 이점은 부족함

3. 왜 "안전한 언어"로 작성되지 않았는가

  • 최근 Rust, Go와 같은 안전한 프로그래밍 언어에 대한 관심이 높지만, SQLite가 처음 개발될 당시(처음 10년간)는 존재하지 않았음
  • Go나 Rust로 다시 작성할 경우 버그가 더 많이 발생하거나, 성능이 저하될 가능성 있음
  • 이러한 언어들은 메모리 체크 등 추가 분기(branch) 코드를 삽입하는데, SQLite의 품질 전략상 100% 브랜치 커버리지가 중요한데 이 부분이 충족되지 않음
  • 안전한 언어들은 out-of-memory 상황에서 주로 프로그램을 중단시키지만, SQLite는 메모리 부족 상황에서도 복구 가능하도록 설계
  • Rust, Go 등은 여전히 신생 언어이며 지속적인 개발이 필요함
  • 그러므로 SQLite 개발진은 안전한 언어 발전을 응원하지만, SQLite 구현에선 여전히 검증된 C의 안정성을 중시함

그럼에도 불구하고, 언젠가 Rust로 재작성될 가능성은 있음. Go는 assert()를 싫어하기 때문에 Go로 작성될 가능성은 낮음

  • 하지만 Rust로 작성되기 위에서는 전제 조건이 있음:
    • Rust가 더 성숙하고, 변화 주기가 느려져서 “오래되고 지루한 언어”가 될 것
    • 여러 언어에서 호출 가능한 범용 라이브러리를 만들 수 있음이 입증되어야 함
    • 임베디드 등 OS 없는 장치에서도 동작하는 오브젝트 코드를 생산 가능해야 함
    • 컴파일된 바이너리에 대해 100% 브랜치 커버리지 테스트 도구가 마련되어야 함
    • OOM(메모리 부족) 오류에서 복구 가능해야 함
    • SQLite에서 C가 처리하는 모든 작업을 성능 저하 없이 Rust가 수행할 수 있어야 함
  • 만약 Rust 애호가(rustacean)가 위의 조건들이 이미 갖춰졌고, SQLite를 Rust로 다시 코딩해야 한다고 생각한다면, SQLite 개발자에게 직접 연락해서 의견을 주장해보기를 권함
Hacker News 의견
  • 안전한 프로그래밍 언어들이 처음 10년 동안은 존재하지 않았음에도, SQLite를 Go나 Rust로 다시 구현하면 오히려 고칠 수 있는 버그보다 더 많은 버그를 만들어낼 가능성이 높고, 속도가 느려질 수도 있다고 생각함. 이미 엄청난 시간과 테스트를 거쳐 버그 없는 코드가 완성됐다면, 변화율이 낮은 상황에서는 어떤 언어로 짜여져도 상관없음. 심지어 어셈블리어라 해도 상관없는 수준임
    • “변화율이 낮으면 문제가 적다"는 점이 Google Security Blog에서 설명한 바 있음. 여기서 메모리 안전 문제는 대부분 새로운 코드에서 발생하고, 코드는 시간이 지날수록 더 안전해진다는 주장임 관련 링크
    • Rust 쪽에서는 Turso 같은 프로젝트가 꽤 활발하게 움직임 Turso
    • Linux의 기본 유틸리티를 rust로 다시 작성하면 안 된다고 주장하는 사람들도 있음. 수십 년간 쓰였고 대부분의 버그가 이미 제거된 소프트웨어를 굳이 다시 쓸 필요가 없다고 봄
    • Zig가 일부 C코드 대체에 좋다고 생각함. Python, 기존 C 바이너리와도 잘 어울림. Go 철학이 좋긴 한데, 최적화가 어렵고 실력 좋은 개발자들이 필요하다는 한계가 있었음. Rust도 쓸 수 있겠지만, 기존 C를 계속 쓰면서 Zig를 점진적으로 도입하는 것이 훨씬 쉬웠음. 우리가 C코드에서 버그를 완전히 제거하지 못하지만, Rust로 전환하는 게 현실적으로 힘들다고 느낌
    • 이미 Go로 포팅된 sqlite 구현체가 존재함 cznic/sqlite
  • SQLite가 처음 개발될 당시 C가 최적의 선택이었던 이유나 현재의 장점 외에도, 굳이 다른 언어로 SQLite를 다시 쓰는 특별한 이유가 없다고 생각함. 경량 SQL 데이터베이스를 구현하는 건 누구나 가능하니, Rust, C++, Go, Lisp 등 원하는 언어로 새로운 구현체를 만들 수 있음. 기존 C로 잘 동작하는 구현을 쓸데없이 버리고, 25년 넘게 C로 SQLite를 유지해 온 개발자들에게 억지로 새 언어를 배우고 처음부터 다시 만들라고 할 필요가 없다고 봄
    • 많은 언어 팬덤에서 남들에게 자신이 원하는 걸 강요하는 경향이 있고, 언어 채택이 일종의 제로섬 싸움처럼 변질된 부분이 있다고 느낌. 어떤 프로젝트가 특정 언어로 개발되고 있으면, 그 언어를 안 쓰는 것만으로도 다른 언어의 필요성에 의문을 제기하게 됨. 사실상 선택지는 훨씬 다양하고, 다른 언어로 다시 짠다고 해도 메뉴에는 Rust, Go, D, Lisp, Julia 등 여러 언어가 오름
    • 실제로 SQLite 개발자들은 Rust로의 리라이트에 열려있음. Rust가 필요한 전제 조건을 충족하면 다시 만들 가능성도 있음. Rust 팬이라면 SQLite 개발자에게 직접 연락해보라는 안내도 있음
    • 이미 Rust로 구현된 rqlite와 turso 같은 프로젝트가 있음
    • Go로 짜여진 어댑터가 있어서, golang에서 cgo 없이 sqlite를 사용할 수 있음. 이제 sqlite는 그 자체로서 C 라이브러리일 뿐만 아니라, 데이터베이스 파일 포맷이기도 함. 앞으로 pure rust 구현체가 나오고, 언젠가 그게 메인 구현이 될 수도 있다고 생각함
    • 요즘 5년 이상 된 기술을 구식이라며 무시하는 풍조에 답답함을 느낌. 오랫동안 다듬어진 기술에 대한 존중이 더 필요하다고 생각함
  • 안전한 언어들은 배열 접근 시 경계를 검사하는 추가 브랜치를 생성하는데, 실제로 올바른 코드에서 그런 브랜치는 실행되지 않음. 즉, 100% 브랜치 테스트가 어렵고, 이 점이 SQLite의 품질 전략과 관련됨. 이런 새로운 논리를 듣게 되어 흥미로움
    • 만약 그 코드 브랜치가 절대 실행되지 않을 것이라 확신할 수 있다면, 굳이 그 부분을 테스트할 필요가 없는 것 아님? 테스트 커버리지 100%를 위해 안전성을 희생하는 느낌이 듦
    • 안전한 언어에서 컴파일러가 자동으로 if (i >= array_length) panic("index out of bounds") 방어 코드 같은 걸 추가하는데, 그 코드 자체는 Rust 컴파일러가 잘 테스트했으니 걱정할 필요 없다고 봄. 이 논리를 내가 제대로 이해하고 있는 것인지 궁금함
    • Dr Hipp와 같은 전문가, sqlite 같은 프로젝트라면 이런 논거에서도 일리가 있다고 봄
    • Rust의 get_unchecked() 같은 방법을 이용하면 bounds check 없는 접근도 가능한데, 이를 통해 안전하면서도 성능을 높일 수 있음 get_unchecked 문서
    • 조건적으로 panic으로 빠지는 브랜치는 커버리지를 의무로 하지 않게 해서 이 문제를 줄일 수 있진 않을까 하고 궁금함
  • SQLite가 someday Rust로 다시 작성될 가능성을 열어두고 있고, Go는 assert() 관련 제약으로 가능성이 희박함. Rust로 이전하려면 아래와 같은 전제 조건이 필요하다고 봄: Rust가 더 오랜 시간 변화가 적어져야 하고, 범용 라이브러리 작성에 적합해야 하며, OS 없는 임베디드에도 돌아가야 하고, 100% 브랜치 커버리지 툴링이 필요하며, OOM 오류 처리 메커니즘이 갖춰져야 하고, 성능 저하 없이 C의 역할을 대체해야 함
    • Rust 1.0 이후 10년 넘게 호환성 있게 변화해 옴. 완전히 변화가 멈추길 바라는 사람과, 변화해도 되는 사람의 차이가 있음. 범용 라이브러리 개발은 이미 증명됐고, OS 없는 임베디드 지원도 명확히 가능함. 브랜치 커버리지는 내가 비전문가라 잘 모르지만, Ferrocene 등에서 관련 작업을 진행하고 있음. Rust 언어 자체는 메모리 할당을 안 하므로, OOM 처리는 표준 라이브러리 수준에서 결정할 수 있음. 성능 문제는 정의 방식에 따라 해석이 달라질 수 있음
    • Go에서도 if condition { panic(err) } 구문을 assert 함수처럼 쓸 수 있는 것 아닌지 궁금함
  • 대부분의 주장이 처음엔 그럴듯하지만, 꼼꼼히 따져 보면 완벽하지 않다고 느낌. C를 2000년쯤에 골랐던 이유만 분명히 소명하면, 현재는 잘 다듬어진 코드베이스를 받아들이면 되는 것 아닌가 생각함. 부가적 논거들은 반박이 가능한 부분이 있음
    • 구체적으로 어떤 주장이 반박 가능한지 듣고 싶음
    • 제시한 논거는 과거 코드베이스 유지에는 쓸 수 있지만, 새로운 개발자들이 복잡한 언어 대신 C를 채택하게 하려면 더 많은 근거가 필요함
    • (해당 문서는 2017년에 작성됨)
    • 오랜 기간 수많은 “왜 X로 다시 쓰지 않나” 질문에 대응하려고 디테일하고 긴 문서를 쓴 것이라 추정함
  • SQLite를 자동화로 Go로 옮긴 프로젝트가 이미 여러 해 전부터 있으면서 활발히 배포되고 있음 modernc.org/sqlite. 같은 테스트 슈트도 잘 통과함. 다만 Go판이 많이 느리긴 하며, 속도 자체보다 Go 네이티브 포팅의 편의성이 더 중요한 상황이 많음. 결론적으로 SQLite를 Go나 Rust, Zig, Nim, Swift 등으로 다시 쓰는 것보다, C에서 자동 번역해 내는 방식이 현실적이라고 생각함
    • 공개 테스트 슈트는 통과하지만, SQLite에는 훨씬 더 강도 높은 사내 테스트 슈트도 있다고 들음
    • 테스트 슈트 통과 = 버그가 없다는 의미는 아니라, 새로운 에지케이스나 성능 문제는 남을 수 있다고 봄
  • “왜 SQLite는 C로 개발됐는가”는 공식 문서에 잘 설명되어 있지만, “왜 Rust가 아닌가”란 질문을 들으면 오히려 “왜 Rust여야만 하는가?”가 먼저 생각남
    • 이 질문이 덧씌워진 제목 때문이라는 의견임
    • 이미 이런 Rust 리라이트 프로젝트가 있음: tursodatabase/turso 그리고 블로그 포스팅에서 Why에 대한 논의도 있었음
    • SQLite를 왜 BASIC이 아니라 C로 짰냐고 물을 수도 있다는 논조임
  • 많은 코딩, 소프트웨어 사용, 그리고 리라이트 관련 글을 볼수록 한 가지 문제는 “기능 동등성”만을 목적으로 리라이트를 하면, 그동안 누적된 수많은 예외처리와 패치들이 빠지기 쉬움. 결국 다시 소프트웨어가 깨지거나, 예전에 잘 동작하던 것들이 망가질 수 있음. 이런 식의 리라이트엔 충분한 강조와 신중함이 필요하며, 100% 복원은 어렵다고 생각함. SDL 같은 중요 라이브러리도 마찬가지. 반복적으로 망가지는 릴리즈와 사용자 불만이 예상됨. C는 Rust가 대세가 된 후에도 오래 살아남을 것이라고 봄. 리라이트는 기본선택이 되어선 안 된다고 생각함
  • DuckDB가 Rust가 아닌 C++로 작성됐다는 점이 더 흥미로운데, DuckDB는 2019년에 등장한 신생 프로젝트라 Rust를 채택했을 법한데 결국 C++를 선택함. DuckDB는 새롭고, 코드베이스도 SQLite보다 훨씬 작음
    • DuckDB 개발진이 C++에 자신이 있었고, 컴파일러의 오토벡터화 기능을 믿어서 선택했다고 들음. 당시(2019년) Rust에는 명확한 고수준 SIMD 지원이 없었음. 핸드롤 SIMD 코드를 유지보수하고 싶지 않았음
    • 최대 성능이 목표라면, C++가 더 빠른 바이너리를 더 적은 코드로 만들 수 있다고 생각함. 최신 C++도 컴파일타임 안전성이 커서 DB 같은 코드에 적합함
    • 현대적 C++로 쓴다면 괜찮다고 생각함
  • 이전에도 SQLite 리라이트 논쟁은 많았음 2021년, 2018년
    • tptacek의 코멘트가 흥미로운데, 이전 문서에는 보안(security)에 관한 단락이 있었으나, 최신 문서에는 사라짐. C는 SQLite에도 명확한 보안 취약점임. 이전 버전엔 ‘SQLite는 그리 보안 민감한 라이브러리가 아니다’라는 설명이 있었음. 신뢰할 수 없는 SQL 실행 자체가 이미 더 큰 이슈라며, 외부 파일 임포트에 대해선 방어코드와 강력한 테스트로 문제를 막고, 사전 검증 루틴도 존재한다고 되어 있었음 2021년 웹아카이브 문서

c는 sqlite에도 보안 위험이라는 구절, 충분히 테스트를 잘 작성하고, 충분히 숙련된 개발자여도 마찬가지 일까요. 로직과 개발 프로세스가 문제 일 수는 있을텐데, 언어 자체가 보안취약점이라는 건 이해하기 어려워서요. 사실 c로 짜인 인프라에 의존하지 않는 프로그램이 거의 없을텐데요.