MySQL의 한계에서 시작된 MongoDB 딥다이브 — 왜 NoSQL인가?
데이터를 다루다 보면 이런 순간이 꼭 찾아와요. 기획자가 “이 필드 하나만 추가해줘요”라고 하는데, 그 한 마디가 ALTER TABLE 한 방으로 수백만 건의 테이블을 몇 시간째 잠가버리는 상황이요.
MySQL 같은 관계형 데이터베이스는 강력하고 안정적이에요. 근데 데이터 구조가 예측 불가능하게 바뀌거나, 규모가 폭발적으로 커지는 환경에서는 그 안정성이 되려 발목을 잡기도 해요. MongoDB는 바로 그 지점에서 출발한 데이터베이스예요.
이 글에서는 MySQL의 구조적 한계를 빌드업 삼아서, MongoDB가 어떤 철학으로 설계됐는지 — 스키마리스 구조, BSON 저장 방식, 샤딩 기반 확장, 비정형 데이터 처리까지 — 개념부터 깊게 파고들어볼게요.
MySQL의 엄격한 스키마, 어디서 막히나요?
관계형 데이터베이스의 핵심은 스키마(Schema) 예요. 테이블을 만들 때 “이 컬럼은 정수형, 저 컬럼은 문자열, 최대 길이는 255자” 이런 식으로 구조를 미리 선언해두죠.
이 구조 덕분에 데이터의 일관성이 보장되고, 잘못된 형식의 데이터가 들어오는 걸 데이터베이스 레벨에서 막을 수 있어요. 여러 테이블을 JOIN해서 복잡한 관계를 표현하는 것도 관계형 DB가 특히 잘하는 영역이고요.
근데 문제는 현실의 데이터가 그렇게 깔끔하지 않다는 거예요.
예를 들어 전자상거래 플랫폼에서 상품 데이터를 저장한다고 생각해봐요. TV는 해상도, 화면 크기, HDR 지원 여부가 필요하고, 운동화는 사이즈, 소재, 색상이 필요하고, 식품은 유통기한, 원산지, 알레르기 정보가 필요해요. 카테고리마다 완전히 다른 속성이 필요한 거죠.
MySQL에서 이걸 처리하려면 두 가지 선택지가 있어요.
첫 번째는 모든 가능한 컬럼을 하나의 테이블에 다 넣는 방식이에요. TV에는 유통기한 컬럼이, 식품에는 해상도 컬럼이 NULL로 가득 차게 되죠. 테이블은 점점 넓어지고, NULL 투성이의 희소 행렬(Sparse Matrix)이 만들어져요.
두 번째는 EAV(Entity-Attribute-Value) 패턴으로 속성을 별도 테이블로 분리하는 방식인데, 이건 조회할 때마다 JOIN이 폭발적으로 늘어나서 쿼리 성능이 급격히 떨어져요.
그리고 결정적으로, 새로운 컬럼이 필요할 때마다 ALTER TABLE을 실행해야 해요. 소규모 테이블이면 괜찮은데, 수천만 건 이상의 대용량 테이블에서 컬럼 추가는 테이블 락(Table Lock)을 유발하거나, Online DDL을 써도 상당한 리소스를 점유하는 작업이에요. 스키마 변경 하나가 서비스 배포 계획 전체에 영향을 주는 거죠.
한 줄 정리: MySQL의 고정 스키마는 일관성은 강력하지만, 유연한 데이터 구조와 빠른 스키마 변화에는 구조적인 마찰이 생겨요.
MongoDB의 Schema-less — 유연성이 어디서 오는 걸까요?
MongoDB는 문서 지향(Document-Oriented) 데이터베이스예요. 데이터를 테이블의 행(Row)이 아니라, JSON을 닮은 형태의 문서(Document) 단위로 저장해요.
// TV 상품 문서
{
"_id": "prod_001",
"category": "TV",
"name": "UHD 스마트TV 55인치",
"price": 890000,
"specs": {
"resolution": "3840x2160",
"hdr": true,
"screen_size": 55
}
}
// 식품 상품 문서 (같은 컬렉션에 저장)
{
"_id": "prod_002",
"category": "food",
"name": "제주 한라봉",
"price": 25000,
"expiry_date": "2025-03-01",
"origin": "제주도",
"allergens": []
}
같은 컬렉션(Collection, MySQL의 테이블에 해당) 안에 완전히 다른 구조의 문서가 공존할 수 있어요. TV 문서에 resolution 필드가 있고, 식품 문서에 expiry_date가 있어도 MongoDB는 아무 문제 없이 받아들여요.
MongoDB에는 미리 정해진 스키마가 없으니, 새로운 필드가 필요하면 그냥 해당 문서에 넣으면 돼요. ALTER TABLE이 필요 없고, 서비스를 중단할 필요도 없어요. 기존 문서는 기존대로, 새 문서는 새 구조대로 공존하는 거죠.
물론 오해하면 안 되는 게 있어요. Schema-less가 “아무렇게나 저장해도 된다”는 뜻은 아니에요. 애플리케이션 레벨에서 데이터 구조를 관리해야 하는 책임이 생기고, MongoDB 4.x 이후부터는 JSON Schema Validation 기능으로 컬렉션 레벨에서도 규칙을 설정할 수 있어요. Schema-less의 진짜 의미는 “스키마의 책임 위치가 DB에서 애플리케이션으로 이동했다”고 보는 게 더 정확해요.
또 하나 중요한 개념이 내장 문서(Embedded Document) 예요. 관계형 DB에서라면 별도 테이블로 분리하고 JOIN으로 연결했을 관련 데이터를, MongoDB는 한 문서 안에 중첩해서 넣을 수 있어요. 상품과 리뷰를 예로 들면, 리뷰를 별도 컬렉션으로 분리하지 않고 상품 문서 안에 배열로 내장할 수 있는 거죠. 조회할 때 JOIN 없이 문서 하나만 읽으면 끝이에요.
한 줄 정리: Schema-less는 자유방임이 아니라, 스키마 관리의 책임을 DB에서 애플리케이션으로 이동시킨 유연한 설계 철학이에요.
BSON — 그냥 JSON 아닌가요?
MongoDB가 데이터를 저장할 때 실제로 쓰는 포맷은 JSON이 아니에요. BSON(Binary JSON) 이라는 독자적인 이진 포맷을 사용해요. 겉으로 보기엔 JSON처럼 생겼는데, 내부적으로는 완전히 달라요.
JSON은 텍스트 기반 포맷이에요. 숫자 12345678도 문자열 "12345678"로 저장되고, 파싱할 때 다시 숫자로 변환해야 해요. 날짜도 JSON에는 네이티브 타입이 없어서 문자열로 표현하죠.
BSON은 이 문제를 이진(Binary) 인코딩으로 해결해요.
BSON의 핵심 특징
1. 타입 정보를 직접 포함
- int32, int64, double, boolean, Date 등 명시적 타입
- JSON처럼 "이 문자열이 숫자인지 날짜인지" 추측할 필요 없음
2. 길이 정보를 앞에 붙임 (Length-Prefixed)
- 문서/배열의 크기가 앞에 명시됨
- 특정 필드 건너뛰기(Skip)가 가능 → 파싱 속도 향상
3. JSON보다 더 많은 타입 지원
- ObjectId, BinData, Decimal128, Regular Expression 등
그리고 여기서 아주 중요한 개념이 등장해요. 바로 임피던스 미스매치(Impedance Mismatch) 문제예요.
임피던스 미스매치란, 객체 지향 언어에서 다루는 데이터 구조와 관계형 데이터베이스가 저장하는 데이터 구조 사이의 불일치를 말해요. 예를 들어 Java나 Python에서 객체 하나가 여러 테이블에 걸쳐 분산 저장되고, 불러올 때는 JOIN으로 조합해야 하는 상황이요.
그래서 ORM(Object-Relational Mapping) 도구들이 등장했는데, ORM은 이 불일치를 자동으로 번역해주는 계층이에요. 편리하지만 복잡한 쿼리에서는 N+1 문제 같은 성능 이슈를 유발하고, ORM이 생성하는 SQL이 의도와 다르게 최적화되지 않는 경우도 많아요.
MongoDB + BSON 조합은 이 미스매치를 구조적으로 줄여줘요. 애플리케이션에서 다루는 객체가 그대로 문서 형태로 저장되니까요. Python의 딕셔너리, JavaScript의 객체, Java의 Map — 이들이 BSON 문서와 자연스럽게 대응돼요. ODM(Object-Document Mapping) 도구는 ORM보다 훨씬 얇은 계층으로도 충분히 동작해요.
한 줄 정리: BSON은 단순히 JSON을 이진으로 바꾼 게 아니라, 타입 명시와 빠른 파싱을 위한 설계이고, 이 구조가 객체-데이터 임피던스 미스매치를 자연스럽게 완화해줘요.
샤딩 — 데이터가 너무 많아지면 어떻게 하나요?
단일 서버의 용량과 처리량에는 한계가 있어요. 이 한계를 넘는 방법은 두 가지예요. 더 좋은 서버로 교체하는 수직 확장(Scale-Up), 아니면 서버를 여러 대로 늘리는 수평 확장(Scale-Out).
수직 확장은 한계가 명확해요. CPU와 메모리가 물리적으로 꽂을 수 있는 최대치가 있고, 고사양 서버일수록 비용이 기하급수적으로 올라가요. 그리고 단일 서버이기 때문에 그 서버가 다운되면 전체가 멈춰요.
MongoDB는 샤딩(Sharding) 을 통한 수평 확장을 네이티브로 지원해요.
샤딩이란, 데이터를 여러 서버(샤드, Shard)에 분산 저장하는 방식이에요. 각 샤드는 전체 데이터의 일부만 담당하고, 이 샤드들이 합쳐져서 하나의 논리적 데이터베이스처럼 동작해요.
MongoDB 샤딩 아키텍처
[클라이언트]
↓
[mongos - 쿼리 라우터]
↓ (샤드 키 기반으로 어느 샤드로 보낼지 결정)
[Config Server] ─ 어떤 데이터가 어느 샤드에 있는지 메타데이터 관리
↓
[Shard 1] [Shard 2] [Shard 3]
각 샤드는 Replica Set으로 구성 → 고가용성 확보
여기서 핵심은 샤드 키(Shard Key) 예요. 어떤 필드를 기준으로 데이터를 나눌지 결정하는 키인데, 샤드 키 선택이 성능을 좌우해요.
샤드 키 선택 전략은 크게 두 가지예요.
범위 기반 샤딩(Range Sharding): 샤드 키의 값 범위를 기준으로 데이터를 나눠요. 예를 들어 user_id가 1~100만은 Shard 1, 100만~200만은 Shard 2 이런 식이에요. 범위 쿼리에 유리하지만, 특정 범위에 데이터가 몰리는 핫스팟(Hotspot) 이 생길 수 있어요. 시간 순으로 쌓이는 로그 데이터를 타임스탬프로 샤딩하면 항상 최신 시간대 샤드에만 쓰기가 집중되는 문제가 대표적이에요.
해시 기반 샤딩(Hashed Sharding): 샤드 키 값에 해시 함수를 적용해서 데이터를 균등하게 분산시켜요. 핫스팟 문제를 해결하고 쓰기 부하를 고르게 분산할 수 있어요. 대신 범위 쿼리를 날리면 모든 샤드를 다 뒤져야 하는 Scatter-Gather 문제가 발생할 수 있어요.
샤드 키는 한 번 설정하면 변경이 매우 어렵기 때문에, 처음부터 데이터의 읽기/쓰기 패턴을 충분히 분석하고 결정해야 해요. 잘못된 샤드 키는 불균형한 청크 분포, 특정 샤드 과부하, 전체 성능 저하로 이어지거든요.
한 줄 정리: 샤딩은 MongoDB가 수평 확장을 네이티브로 지원하는 핵심 메커니즘이지만, 샤드 키 설계가 전체 성능을 결정하는 가장 중요한 의사결정이에요.
비정형 데이터와 로그 파이프라인 — MongoDB가 진짜 빛나는 영역
MongoDB의 진가가 가장 잘 드러나는 영역 중 하나가 비정형 데이터 저장과 로그 파이프라인이에요.
로그 데이터를 생각해봐요. 웹 서버 접근 로그, 애플리케이션 에러 로그, 사용자 행동 이벤트 로그 — 이 데이터들은 공통점이 있어요. 구조가 제각각이에요.
HTTP 요청 로그에는 method, url, status_code, response_time이 있고, 에러 로그에는 error_code, stack_trace, user_id가 있어요. 결제 이벤트 로그에는 payment_method, amount, currency가 필요하고요. MySQL에 이걸 다 넣으려면 어마어마하게 넓은 테이블이 되거나, 여러 테이블로 쪼개야 해요.
MongoDB에서는 이런 다양한 구조의 이벤트를 하나의 컬렉션에 자연스럽게 수용해요.
// 같은 'events' 컬렉션에 저장되는 서로 다른 구조의 로그들
{ "type": "http_request", "timestamp": ISODate("2025-01-15T10:23:45Z"),
"method": "GET", "url": "/api/products", "status": 200, "latency_ms": 42 }
{ "type": "error", "timestamp": ISODate("2025-01-15T10:23:51Z"),
"error_code": "DB_TIMEOUT", "service": "product-service",
"stack_trace": "...", "user_id": "user_9821" }
{ "type": "payment", "timestamp": ISODate("2025-01-15T10:24:03Z"),
"user_id": "user_9821", "amount": 89000, "currency": "KRW",
"payment_method": "card", "status": "success" }
그리고 MongoDB의 Aggregation Pipeline이 이 로그 데이터를 분석하는 데 아주 강력한 도구예요. 파이프라인은 SQL의 GROUP BY, HAVING, JOIN을 단계별로 조합하는 방식으로, 복잡한 데이터 변환과 집계를 선언적으로 표현할 수 있어요.
// 최근 1시간 에러 로그를 서비스별로 집계하는 파이프라인 예시
[
{ "$match": {
"type": "error",
"timestamp": { "$gte": ISODate("2025-01-15T09:00:00Z") }
}},
{ "$group": {
"_id": "$service",
"error_count": { "$sum": 1 },
"error_codes": { "$addToSet": "$error_code" }
}},
{ "$sort": { "error_count": -1 } }
]
$match로 조건 필터링 → $group으로 집계 → $sort로 정렬. 각 단계가 파이프처럼 연결되고, 앞 단계의 결과가 다음 단계의 입력이 되는 구조예요. 여기에 $lookup(JOIN에 해당), $unwind(배열 펼치기), $project(필드 선택/변환) 같은 스테이지를 조합하면 꽤 복잡한 분석도 가능해요.
실시간 로그 파이프라인 맥락에서는 Change Streams도 언급할 필요가 있어요. MongoDB 3.6부터 지원되는 이 기능은 컬렉션에 발생하는 변경사항(삽입, 수정, 삭제)을 실시간으로 스트리밍해서 구독할 수 있게 해줘요. Kafka 같은 메시지 큐와 조합하면, MongoDB를 로그 수집 → 실시간 알림 → 분석 파이프라인의 중간 허브로 활용하는 아키텍처도 구성할 수 있어요.
한 줄 정리: 구조가 제각각인 비정형 로그 데이터를 유연하게 수용하고, Aggregation Pipeline과 Change Streams로 실시간 분석까지 처리하는 게 MongoDB가 로그 파이프라인에서 강한 이유예요.
그래서 MongoDB, 언제 쓰고 언제 안 쓰는 게 맞을까요?
여기까지 읽으면 “그럼 MongoDB가 MySQL보다 무조건 좋은 거 아닌가요?” 싶을 수 있는데, 그건 아니에요. MongoDB가 더 적합한 상황과 MySQL이 더 적합한 상황이 분명히 달라요.
MongoDB가 강한 상황
- 데이터 구조가 자주 바뀌거나, 도메인마다 속성이 완전히 다른 경우
- 대용량 로그, 이벤트, 비정형 데이터를 빠르게 수집·저장해야 하는 경우
- 수평 확장이 필수인 대규모 트래픽 환경
- 애플리케이션 객체 구조와 저장 구조를 최대한 일치시키고 싶은 경우
- 읽기보다 쓰기가 압도적으로 많은 워크로드
MySQL이 더 나은 상황
- 복잡한 JOIN이 많고, 데이터 간 관계가 정교하게 얽혀 있는 경우
- 금융, 결제처럼 강한 트랜잭션 일관성(ACID)이 필수인 경우
- 데이터 구조가 안정적이고, 스키마 변경이 거의 없는 경우
- 기존 팀이 SQL에 익숙하고, 빠른 쿼리 최적화가 필요한 경우
참고로 MongoDB도 4.0부터 멀티 도큐먼트 트랜잭션을 공식 지원하기 시작했어요. 하지만 트랜잭션 처리가 설계 철학의 중심인 MySQL과 비교하면, 트랜잭션 집약적인 도메인에서는 여전히 MySQL이 더 자연스러운 선택이에요.
한 줄 정리: MongoDB와 MySQL은 우열의 문제가 아니라 적합한 사용 맥락이 다른 도구예요. 데이터의 구조 변화 빈도, 확장 방식, 트랜잭션 요구 수준을 기준으로 선택해야 해요.
마무리
MySQL의 고정 스키마가 주는 안정성과, 그 안정성이 만들어내는 마찰에서 MongoDB가 어떤 문제를 해결하려 했는지를 따라가다 보면, 단순히 “NoSQL은 유연하다”는 말보다 훨씬 구체적인 설계 철학이 보여요.
Schema-less는 스키마를 포기한 게 아니라 책임을 이동시킨 것이고, BSON은 JSON의 이진 버전이 아니라 임피던스 미스매치를 줄이기 위한 타입 시스템이고, 샤딩은 단순한 분산 저장이 아니라 샤드 키 설계가 전체 성능을 결정하는 정교한 아키텍처예요.
어떤 기술이든 “이걸 왜 만들었는가”를 이해하고 나면, 언제 쓰고 언제 쓰지 말아야 하는지가 훨씬 선명하게 보이더라고요. MongoDB도 마찬가지예요.