PyMongo의 insert_many() 사용 시 중복 키 오류를 피하는 법

Dev Smile·2025년 3월 5일
1

FastAPI

목록 보기
9/10

pymongo의 insert_many를 이용하여 여러 개의 문서를 삽입하는 과정에서 중복키 에러가 발생했던 상황을 공유하려고 합니다.

문제 상황

아래와 같이, 동일한 내용의 document를 여러 번 insert 하려는 코드가 있을 때, 어떠한 문제가 발생할 수 있을까요?

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
db = client["mydatabase"]
collection = db["mycollection"]

document = {
    "data": "example"
}
documents_to_insert = [document] * 5

result = collection.insert_many(documents_to_insert)

위 코드를 실행하면 다음과 같은 오류가 발생할 수 있습니다.

# pymongo.errors.BulkWriteError: batch op errors occurred, full error:
{
    'writeErrors': [
        {
            'index': 1,  # 두 번째 문서(index=1)에서 오류 발생
            'code': 11000,  # MongoDB의 Duplicate Key Error 코드
            'keyPattern': {'_id': 1},  # 중복된 키가 '_id' 필드임을 의미
            'keyValue': {'_id': ObjectId('67c7f9e56674ed3dfa230c9f')},  # 충돌된 ObjectId 값
            'errmsg': "E11000 duplicate key error collection: OIT.slot index: _id_ dup key: { _id: ObjectId('67c7f9e56674ed3dfa230c9f') }",
            'op': {  # 실패한 문서의 내용
                "data": "example",
                '_id': ObjectId('67c7f9e56674ed3dfa230c9f')
            }
        }
    ],
    'writeConcernErrors': [],
    'nInserted': 1,  # 1개의 문서만 삽입됨
    'nUpserted': 0,
    'nMatched': 0,
    'nModified': 0,
    'nRemoved': 0,
    'upserted': []
}

원인 분석

  • 정확한 원인 분석을 위하여, 우선 문제 상황 코드를 살펴본 후에 insert_many 코드(pymongo → collection → insert_many)를 살펴보겠습니다.
    • 문제 상황 코드 확인
      • mongodb collection을 선언하고, document를 삽입하는 부분을 제외하고 볼 부분은 다음 부분 밖에 없습니다.
        document = {
            "data": "example"
        }
        documents_to_insert = [document] * 5 
      • documents_to_insert라는 리스트를 생성하는데, document를 5번 반복해서 리스트에 넣습니다.
      • 이때 리스트 내부의 요소들은 모두 동일한 객체에 대한 참조(레퍼런스)입니다.
      • 즉, documents_to_insert리스트의 모든 요소는 동일한 document 객체를 가리키게 됩니다.
      • 이 상황을 파악하고, insert_many가 어떻게 동작하는지 확인해봅시다.
    • insert_many 코드 확인
      • PyMongo github을 확인해보니 collection을 처리하는 동기와 비동기, 두 가지 버전을 확인할 수 있었습니다. 확인해보니 코드 구현은 동일하게 되어 있으므로 동기 버전의 코드만 가지고 확인을 해보겠습니다.
      • _id를 처리하는 부분의 코드입니다.
        # L960
        inserted_ids: list[ObjectId] = []
        
        def gen() -> Iterator[tuple[int, Mapping[str, Any]]]:
            """A generator that validates documents and handles _ids."""
            for document in documents:
                common.validate_is_document_type("document", document)
                if not isinstance(document, RawBSONDocument):
                    if "_id" not in document:
                        document["_id"] = ObjectId()  # type: ignore[index]
                    inserted_ids.append(document["_id"])
                yield (message._INSERT, document)
        • inserted_ids 리스트를 만들어, 삽입된 문서의 _id를 저장.
        • 문서 유효성을 검사 (common.validate_is_document_type(...)).
        • 문서에 _id가 없으면 자동으로 ObjectId()를 생성해서 추가.
        • yield를 통해 삽입할 데이터를 제너레이터(generator)로 반환.
    • 종합 해석
      1. 리스트 내부 요소가 동일한 객체를 참조

        document = {"data": "example"}
        documents_to_insert = [document] * 5

        위 코드에서 documents_to_insert 리스트의 모든 요소는 동일한 객체를 참조합니다. 즉, 리스트에 5개의 개별 객체가 있는 것이 아니라 하나의 document 객체가 5번 반복된 상태입니다.

      2. insert_many()의 동작 방식

        PyMongo는 _id 필드가 없는 문서에 대해 자동으로 ObjectId()를 생성합니다. 하지만 모든 요소가 동일한 객체를 가리키므로 _id가 하나만 생성되고, 이 _id가 리스트의 모든 요소에 적용됩니다.

        [{"data": "example", "_id": ObjectId('604a8f032d23d2e9c3a2c1e1')},
         {"data": "example", "_id": ObjectId('604a8f032d23d2e9c3a2c1e1')},
         {"data": "example", "_id": ObjectId('604a8f032d23d2e9c3a2c1e1')},
         {"data": "example", "_id": ObjectId('604a8f032d23d2e9c3a2c1e1')},
         {"data": "example", "_id": ObjectId('604a8f032d23d2e9c3a2c1e1')}]

        결과적으로 _id가 중복되어 Duplicate Key Error(11000)가 발생합니다.

해결 방법

얕은 복사가 문제가 된다는 것을 알았으니 해결 방법은 간단합니다.

1. dict()를 사용한 깊은 복사(Deep Copy)

dict(document)를 사용하면 개별 객체를 새로 생성할 수 있습니다.

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
db = client["mydatabase"]
collection = db["mycollection"]

document = {"data": "example"}

documents_to_insert = [dict(document) for _ in range(5)]

result = collection.insert_many(documents_to_insert)
print("Inserted IDs:", result.inserted_ids)

이 방식은 _id가 각 문서에 대해 개별적으로 생성되도록 보장하여 중복 오류를 방지할 수 있습니다.

2. _id 필드를 명시적으로 추가

각 문서에 수동으로 _id를 할당하면 충돌을 방지할 수 있습니다.

from bson import ObjectId

documents_to_insert = [{"_id": ObjectId(), "data": "example"} for _ in range(5)]
result = collection.insert_many(documents_to_insert)

이 방법을 사용하면 PyMongo의 자동 _id 생성이 아닌, 우리가 직접 지정한 _id가 사용됩니다.

결론

PyMongo에서 insert_many()를 사용할 때, 같은 객체를 복제하는 방식은 _id 중복 오류를 유발할 수 있습니다. 이를 방지하기 위해서는 아래와 같은 방법을 사용하면 됩니다.

  1. dict()를 사용하여 개별 객체를 생성
  2. _id 필드를 직접 추가

PyMongo의 내부 동작을 이해하면, 예상치 못한 오류를 줄이고 안전하게 데이터를 삽입할 수 있습니다. 얕은 복사(Shallow Copy)를 조심합시다!

0개의 댓글

관련 채용 정보