# Shardines - Rails에서 SQLite로 테넌트별 데이터베이스 관리하기

> Clean Markdown view of GeekNews topic #20587. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=20587](https://news.hada.io/topic?id=20587)
- GeekNews Markdown: [https://news.hada.io/topic/20587.md](https://news.hada.io/topic/20587.md)
- Type: GN+
- Author: [xguru](https://news.hada.io/@xguru)
- Published: 2025-04-29T10:31:29+09:00
- Updated: 2025-04-29T10:31:29+09:00
- Original source: [blog.julik.nl](https://blog.julik.nl/2025/04/a-can-of-shardines)
- Points: 1
- Comments: 1

## Topic Body

- Rails에서 **테넌트마다 별도 데이터베이스**를 사용하는 구조를 구축하는 방법과 그 도전 과정을 설명  
- **ActiveRecord**는 기본적으로 단일 DB 연결을 전제로 설계되어 있어, 테넌트별 연결 전환이 복잡하고 까다로움  
- Rails 6 이상의 **connected_to** 기능을 활용해 **런타임에 연결을 동적으로 전환**하는 방법을 제안함  
- **SQLite3**는 소규모, 다수의 독립 DB를 다루기에 적합하여 **백업, 디버깅, 삭제** 등 관리가 용이함  
- 대규모 시스템 최적화 중심으로 발전한 Rails 인프라와 달리, **작고 독립적인 데이터베이스** 중심 아키텍처가 가능함을 강조함  
  
---  
  
### 테넌트마다 별도 데이터베이스를 사용하는 이유  
  
- 데이터 모델 안에서 독립적으로 동작하는 테넌트(`Site`) 단위로 분리하면, 데이터 격리와 관리가 수월해짐  
- 테넌트별로 데이터를 별도 DB에 저장하면, 대규모 사이트 확장이나 보안 이슈에도 유리함  
- SQLite를 활용하면 서버 설정 없이 파일 하나만으로 데이터베이스를 운용할 수 있어 간편하고 유연함  
  
### Rails에서 어려운 점  
  
- SQLite의 기본 `open/close` 작업은 매우 간단하지만, **ActiveRecord**는 내부적으로 복잡한 커넥션 관리 구조를 가짐  
- ActiveRecord는 연결을 모델에 고정해서 사용하는 구조로 설계되어 있어, 런타임에 테넌트 전환이 어려움  
- 커넥션 풀, 쿼리 캐시, 스키마 캐시 등이 모두 연결에 종속되어 있어, 매번 연결 변경이 부담스러움  
  
### Rails 다중 데이터베이스 관리의 역사  
  
- Rails 1: `ActiveRecord::Base` 단위로 DB 지정 가능  
- Rails 3: 커넥션 풀 도입  
- Rails 4: `connection_handling` 추가  
- Rails 6: `connected_to` 도입  
- Rails 7: `connected_to` 기능 확장 및 샤딩 지원  
- 하지만 여전히 "런타임에 동적으로 테넌트 추가/삭제" 같은 시나리오는 기본 지원되지 않음  
  
### 테넌트별 데이터베이스의 장점  
  
- 테넌트별 파일만 백업하거나 복원할 수 있어, **운영과 디버깅이 간단**해짐  
- 테넌트 제거는 단순히 파일 삭제(`unlink`)로 가능  
- 대규모 데이터베이스 서버는 수십 테라바이트 규모의 DB를 최적화하지만, SQLite는 수천 개의 소규모 DB에 최적화되어 있음  
- 실제로 **iCloud**도 수백만 개의 작은 SQLite DB를 Cassandra 위에 저장하는 구조를 채택함  
  
### 문제 해결 과정  
  
- 기존 방식(수동 `establish_connection`)은 다중 접속 환경에서 **ConnectionNotEstablished** 오류를 유발  
- Rails 6 이후의 방식에 맞춰, 커넥션 풀을 수동 관리하는 대신 **Rails에게 맡기는 구조**로 변경  
- 각 테넌트마다 동적으로 **connection pool**을 만들고, `connected_to` 블록으로 작업을 감쌈  
- 미들웨어를 활용해 요청 시점에 필요한 DB 연결을 동적으로 준비하고 해제하는 방식으로 개선  
  
### 핵심 코드 패턴  
  
- 커넥션 풀 체크 후 없으면 생성  
  
```ruby  
MUX.synchronize do  
  if ActiveRecord::Base.connection_handler.connection_pool_list(role_name).none?  
    ActiveRecord::Base.connection_handler.establish_connection(database_config_hash, role: role_name)  
  end  
end  
```  
  
- 연결 후 `connected_to` 블록 내에서 안전하게 쿼리 수행  
  
```ruby  
ActiveRecord::Base.connected_to(role: role_name) do  
  pages = Page.order(created_at: :desc).limit(10)  
end  
```  
  
### Rack 스트리밍 처리  
  
- Rack 응답이 스트리밍일 경우, 연결 관리를 위해 `Rack::BodyProxy`와 `Fiber`를 활용하여 안전하게 커넥션을 닫음  
  
```ruby  
connected_to_context_fiber = Fiber.new do  
  ActiveRecord::Base.connected_to(role: role_name) do  
    Fiber.yield  
  end  
end  
connected_to_context_fiber.resume  
  
status, headers, body = @app.call(env)  
body_with_close = Rack::BodyProxy.new(body) { connected_to_context_fiber.resume }  
  
[status, headers, body_with_close]  
```  
  
### 최종 미들웨어 구조  
  
- 요청마다 적절한 DB 연결을 찾아 `connected_to`로 전환하고, 응답이 끝나면 정리하는 미들웨어 `Shardine::Middleware`를 작성함  
- Rails 프로젝트의 `config.ru` 파일에 다음처럼 적용 가능  
  
```ruby  
use Shardine::Middleware do |env|  
  site_name = env["SERVER_NAME"]  
  {adapter: "sqlite3", database: "sites/#{site_name}.sqlite3"}  
end  
```  
  
### 남은 과제  
  
- ActiveRecord 6에서는 아직 `shard` 기능을 활용하지 않았지만, 이후 버전에서는 읽기/쓰기 분리도 가능함  
- 테넌트 삭제 시 커넥션 풀 정리 기능은 아직 필요하지 않아 구현하지 않음  
- 앞으로 "작은 데이터베이스 다수"를 다루는 아키텍처가 더 주목받을 가능성이 큼

## Comments



### Comment 37945

- Author: neo
- Created: 2025-04-29T10:31:29+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=43811400) 
* "database-per-tenant" 방식을 약 100만 명의 사용자와 함께 사용 중임
  - 이 방식은 읽기 중심의 앱에 적합하며, 대부분의 테넌트는 작고 테이블에 많은 레코드가 없어 복잡한 조인도 매우 빠름
  - 주요 문제는 개별 데이터베이스를 하나씩 마이그레이션해야 하므로 릴리스 시간이 크게 증가할 수 있음
  - 스키마나 데이터 드리프트가 발생하면 릴리스가 중단되고, 일부 테넌트에서 기능이 작동하지 않는 이유를 찾아야 함

* SQLite를 좋아하지만, 기존 OLTP 데이터베이스가 인덱스의 일부를 메모리에서 언로드할 필요가 있는지 궁금함
  - 사용자별 데이터베이스를 사용하면 비활성 사용자나 다른 인스턴스에서만 활성화된 사용자를 위해 메모리에 아무것도 유지하지 않음
  - 이는 Mongo의 JSON 상황과 유사하며, Postgres가 Mongo보다 두 배 빠름

* 대부분의 사람들은 테넌트별 데이터베이스가 필요하지 않으며, 이는 일반적인 방식이 아님
  - 마이그레이션과 스키마 드리프트와 같은 단점을 상쇄해야 하는 특정 사례가 있음
  - 사용할 수 있다고 해서 반드시 사용해야 하는 것은 아님
  - 주의해서 진행하고 테넌트별 데이터베이스가 필요하다는 사실을 알아야 함

* 중간 접근 방식으로 다음을 고려할 수 있음
  - 상위 N개의 테넌트를 식별함
  - 이 테넌트를 위한 DB를 분리함
  - 상위 N개는 IOPS, 중요도(수익 측면) 등을 기준으로 결정됨
  - 데이터 모델은 각 테넌트에 해당하는 행을 추출할 수 있도록 설계되어야 함

* 우연히도 Elixir를 위한 FeebDB를 작업 중임
  - 이는 Ecto의 대체물로 볼 수 있으며, 수천 개의 데이터베이스가 있을 때 잘 작동하지 않음
  - 주로 재미있는 실험으로 시작했지만, 과거에 일했던 모든 곳에서 이러한 아키텍처가 큰 도움이 될 것임
  - 데이터베이스-테넌트 접근 방식의 일반적인 문제점을 제거하거나 줄이는 것이 목표임
  - 각 데이터베이스에 단일 작성자 보장
  - 모든 테넌트에 대한 향상된 연결 관리
  - 필요 시 마이그레이션 및 백업 지원
  - 여러 DB에 대한 맵/리듀스/필터 작업 지원
  - 클러스터 배포 지원

* Forward Email은 각 메일박스/사용자별로 암호화된 sqlite db를 사용하여 유사한 작업을 수행함
  - 사용자별 보호를 차별화하는 훌륭한 방법임

* 이름이 매우 훌륭함. Sean Connery를 연상시킴

* "database per tenant" 워크플로우는 이제 시작임
  - James Edward Gray가 2012년 RailsConf에서 이에 대해 이야기함

* 과거에 유사한 것을 사용했으며, 매우 만족했음
  - 사용자가 데이터를 원하면 전체 데이터베이스를 제공할 수 있음
  - 사용자가 계정을 삭제하면 `rm username.sql`로 간단히 처리 가능
  - 컴플라이언스가 매우 쉬워짐

* 데이터가 서로 격리되고 단일 테넌트 내에서 확장 문제가 없을 때 잘못된 설계를 하기 어려움
  - 거의 모든 것이 작동할 것임
