MongoDB를 왜 메인 데이터베이스로 사용하고있나요?

Multi-Tenancy 환경에서 MongoDB를 선택한 이유, 그리고 활용하는 방식

배경

필자는 현재 B2B SaaS 신사업팀에서 백엔드 엔지니어로 일 하고있다.

메인 데이터베이스로 MongoDB를 사용하고있는데, 그 이유와 사용 방법에 대해 소개해보려한다. (실제로 질문도 많이 받았었다)

왜 MongoDB를 메인 데이터베이스로 채택했나요?

MongoDB는 세부적인 보안 설정을 컬렉션 별로 지원하기때문이다.

왜 세부적인 보안 설정이 필요하고, 이를 어떻게 활용한 계획인지 설명하기 위해선 우선 Multi-Tenancy에 대해 간략히 설명이 필요할것같다.

Multi-Tenancy가 뭔가요?

Multi-Tenancy는 단일 인스턴스에서 여러 테넌트(고객 또는 조직)를 독립적으로 지원하는 아키텍처이다.

위 이미지처럼 Multi-Tenancy는 소프트웨어 개발과 유지보수 비용을 공유하기 때문에 경제적이다. 따라서, 공급자는 업데이트를 한 번만 하면 다중 테넌트에게 업데이트 된 서비스를 제공할 수 있다.

(Single-Tenant라면, 공급자는 여러 소프트웨어의 인스턴스에 모두 업데이트가 필요하다.)

그런데 각 테넌트마다 독립적인 Database Server를 사용하면, 고객사가 10000개라면, 10000개의 DB Server를 관리해야하는 비용이 발생한다. 그래서 서비스 목적성에 맞추어 Multi-Tenancy Model을 채택하여 설계한다.

내가 속한 팀의 Multi-Tenancy 구조는 3번이다. 하나의 인스턴스에서 논리적으로 테넌트를 분리하고 Database Server는 한대만 기동하고있다.

물론, 엔터프라이즈 고객의 요구에 따라 Database Server와 어플리케이션 인스턴스를 독립적으로 분리할 수 있는 인프라 세팅도 준비되어있다.

어떻게 Tenant 별로 데이터를 관리하고있나요?

위에서 말했듯이, 어플리케이션에서 논리적으로 테넌트들을 분리하고있다. 논리적으로 분리된 테넌트는 각 독립된 MongoDB Collection에 저장이된다.

이를 코드로 나타내면 아래와 같다.

class ReservationRepositoryImpl(
    database: MongodbConfiguration,
) : PurchaseSummaryRepository {
    private val entityType: Class<ReservationDataModel> = ReservationDataModel::class.java
    private val collection: MongoTemplate = MongoTemplate(database.mongoDatabaseFactory)
    //...

    // tenantId는 Bson의 ObjectId 값이고, String 형태로 핸들링하고있다.
    fun create(tenantId: String, entity: ReservationDataModel) {
        val collectionName = "reservations_$tenantId"
        collection.insert(entity, collectionName)
    }

    fun findById(tenantId: String, reservationId: ObjectId): ReservationDataModel? {
        val collectionName = "reservations_$tenantId"
        return collection.findById(reservationId, entityType, collectionName)
    }
}

최초 고객이 로그인할 때, 테넌트를 설정할 수 있고 접속하면 URL에서 tenantId를 관리하고있고, 도메인 서비스에게 보내는 요청의 Request Param에 tenantId가 모두 포함되어있는 형태이다.

따라서, Domain Service들의 API Endpoint에는 .../tenants/{tenant}/...가 붙어있다. 이러한 요청을 통해 구분된 TenantId는 MongoDB의 CollectionName에 Suffix에 붙여 사용한다.

e.g. collection name

  • reservations_{tenantId}

  • products_{tenantId}

따라서 각 테넌트별로 독립적인 컬렉션을 갖고있는 형태가된다. 독립적이기때문에 얻을 수 있는 이점은 컬렉션 별 보안 설정을 할 수 있다. 물론, 특정 테넌트에 잘못된 데이터가 Hard Updating 되었을 때 다른 테넌트들은 장애 전파가 안되기도하지만 이는 매우 특별한 케이스이긴하다.

그래서 중요한 보안은 어떻게 설정을 하고있나요?

MongoDB는 컬렉션 수준 보안 설정 기능을 제공한다.

역할 기반 액세스 제어 (RBAC) 기능

  • 역할 기반 액세스 제어를 통해 DB의 특정 컬렉션에 대한 권한을 세밀하게 부여할 수 있다.

    • 관리자는 사용자 정의 역할(User Defined Role)을 생성하여 각 테넌트의 컬렉션에 대한 읽기, 쓰기, 업데이트 등의 권한을 개별적으로 설정할 수 있다.

    • 권한의 수준 범위는 아래와 같다.

      • 데이터 베이스 수준

        • 특정 테넌트 전용의 DB에만 접근 권한만 부여할 수 있다 (하나의 테넌트가 N개의 DB에 접근할 수도 있다)
      • 컬렉션 수준

        • 테넌트의 컬렉션만 접근 가능하도록 제한이 가능하다
      • 필드 수준

        • 특정 필드에도 read, write의 제한이 가능하다

사용자 정의 역할(User Defined Role)

  • MongoDB 관리자(제품팀)는 특정 컬렉션이나 DB에 대해 사용자 정의 역할을 생성할 수 있다.

  • 각 역할 별 read, write, update와 같은 세부 권한을 커스텀 할 수 있고, 특정 테넌트의 컬렉션에만 적용되도록 제한할 수도 있다.

    • e.g.

      • reservation_{tenantA}에서 병원 IT 관리자1 에게 해당 컬렉션의 read 권한을 줄 수 있다.

      • 제품팀 개발자들의 책임에 따라 관리자가 책임별로 세부 권한을 나누어 관리할 수 있다.

        • SaaS 제품 특성 상 병원의 데이터를 제품 개발자라고 모두 볼 수 있는것은 법적으로 안된다.

위의 MongoDB RBAC 특성을 통해, 잘못된 쿼리나 운영 실수로 인해 다른 테넌트에 데이터가 노출되거나, 수정되는것을 방지할 수 있게되어 데이터 격리 수준을 한층 강화할 수 있다.

db.createRole({
  role: "tenant_admin_reader",
  privileges: [
    {
      resource: { db: "myDatabase", collection: "reservations_tenantA" },
      actions: ["find"]
    }
  ],
  roles: []
});

// 2. 사용자에게 역할 할당
db.createUser({
  user: "tenantAUser",
  pwd: "pw1234",
  roles: ["tenant_admin_reader"]
});

위 스크립트처럼 RBAC 설정들을 모두 DB Query를 제공하고 있어서, 백오피스를 구축하면 쉽게 권한 분리도 가능하다.

마치며

속한 SaaS팀에서 Multi-Tenancy 환경을 구축하면서 MognoDB를 메인 데이터베이스로 사용하고있는 이유에 대해서 다뤄보았다. 포스트에서 유의할점은 RDBMS에서도 RBAC에 준하는 권한 관리가 가능하다.

그러나 MongoDB는 동적 데이터 구조 관리(Dynamic Data Structure Management) + 스키마 리스(Schema-less)하여 보다 현재 속한 신사업 팀이 보다 빠르고 간단하게 Multi-Tenancy를 구축하는데 도움이 되었던 내용을 공유한것이니 MongoDB 라서 가능했다!는 아니라는것을 유의바란다.

실제로, Mongo Atlas를 사용하고 있었기때문에 Atlas Search 기능을 통해 보다 쉽게 검색 엔진을 도입할 수 있었던 사례도 있는데 이것도 추후 기회가 되면 작성해보도록 하겠다.