MongoDB를 사용하다 보면 한 번쯤은 마주치게 되는 E11000 duplicate key error. 이 에러는 개발자들에게 꽤나 골치 아픈 문제입니다. 처음 보면 당황스럽지만, 원인을 정확히 파악하고 체계적으로 접근하면 충분히 해결할 수 있기도 합니다. 특히, 사용자 등록 시스템을 구축할 때 이메일 중복 체크 부분에서 자주 발생하는데요. 이번 글에서는 E11000 에러의 원인부터 해결법까지 상세히 알아보겠습니다.

 

1. E11000 에러란 무엇인가?

E11000 duplicate key error는 MongoDB에서 유니크 인덱스(unique index) 제약 조건을 위반했을 때 발생하는 에러입니다. 쉽게 말해, “이미 존재하는 값을 중복으로 저장하려고 했다”는 뜻입니다.

에러 메시지 형태

E11000 duplicate key error collection: mydb.users index: email_1 dup key: { email: "test@example.com" }

이 메시지를 분석해보면:

  • mydb.users: 데이터베이스명.컬렉션명
  • email_1: 문제가 된 인덱스명
  • { email: "test@example.com" }: 중복된 키 값

 

 

2. E11000 에러 발생 원인 분석

2-1. 유니크 인덱스 제약 조건 위반

가장 일반적인 원인으로, 이미 존재하는 값을 다시 삽입하려고 할 때 발생합니다.

// 이미 존재하는 이메일로 새 사용자 생성 시도
db.users.insertOne({
  name: "홍길동",
  email: "hong@example.com"  // 이미 존재하는 이메일
});

2-2. null 값 중복 문제

유니크 인덱스가 설정된 필드에 null 값이 여러 개 있을 때도 에러가 발생할 수 있습니다.

// 스키마에서 unique: true 설정
const userSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  phone: { type: String, unique: true }  // phone 없는 사용자들은 null 값
});

2-3. 기존 인덱스와의 충돌

스키마를 변경했지만 기존 인덱스가 남아있을 때 발생하는 문제입니다.

 

 

3. 해결 방법별 상세 가이드

3-1. 기본적인 중복 데이터 해결법

Step 1: 중복 데이터 확인 먼저 어떤 데이터가 중복되었는지 확인해야 합니다.

// 중복된 이메일 찾기
db.users.aggregate([
  { $group: { _id: "$email", count: { $sum: 1 } } },
  { $match: { count: { $gt: 1 } } }
]);

Step 2: 중복 데이터 정리 중복된 데이터 중 하나만 남기고 나머지를 삭제합니다.

// 중복된 문서 중 가장 오래된 것만 남기고 삭제
db.users.aggregate([
  { $group: { 
    _id: "$email", 
    docs: { $push: { id: "$_id", createdAt: "$createdAt" } },
    count: { $sum: 1 }
  }},
  { $match: { count: { $gt: 1 } } }
]).forEach(function(doc) {
  // 가장 최근 문서를 제외한 나머지 삭제
  doc.docs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  for (let i = 1; i < doc.docs.length; i++) {
    db.users.deleteOne({ _id: doc.docs[i].id });
  }
});

3-2. 인덱스 문제 해결법

인덱스 확인하기 현재 컬렉션에 어떤 인덱스가 있는지 확인합니다.

// 모든 인덱스 조회
db.users.getIndexes();

문제 인덱스 삭제하기 불필요하거나 문제가 되는 인덱스를 삭제합니다.

// 특정 인덱스 삭제
db.users.dropIndex("email_1");

// 또는 인덱스 스펙으로 삭제
db.users.dropIndex({ email: 1 });

// _id를 제외한 모든 인덱스 삭제
db.users.dropIndexes();

3-3. null 값 중복 문제 해결법

Sparse 인덱스 사용 null 값이 있는 문서들을 인덱스에서 제외하려면 sparse 옵션을 사용합니다.

// Mongoose 스키마에서 sparse 옵션 사용
const userSchema = new mongoose.Schema({
  email: { 
    type: String, 
    unique: true, 
    sparse: true  // null 값은 인덱스에서 제외
  }
});

// 또는 직접 인덱스 생성
db.users.createIndex(
  { email: 1 }, 
  { unique: true, sparse: true }
);

3-4. 애플리케이션 레벨 해결법

upsert 연산 사용 데이터 삽입 전에 존재 여부를 확인하고 업데이트하는 방식입니다.

// Node.js + Mongoose 예제
async function createOrUpdateUser(userData) {
  try {
    // findOneAndUpdate with upsert 사용
    const user = await User.findOneAndUpdate(
      { email: userData.email },  // 찾을 조건
      userData,                   // 업데이트할 데이터
      { 
        upsert: true,            // 없으면 생성
        new: true,               // 업데이트된 문서 반환
        setDefaultsOnInsert: true // 기본값 설정
      }
    );
    return user;
  } catch (error) {
    if (error.code === 11000) {
      console.log('중복 키 에러 발생:', error.message);
      // 에러 처리 로직
    }
    throw error;
  }
}

사전 중복 체크 데이터 삽입 전에 미리 존재 여부를 확인합니다.

async function createUser(userData) {
  // 1. 이메일 중복 체크
  const existingUser = await User.findOne({ email: userData.email });
  if (existingUser) {
    throw new Error('이미 존재하는 이메일입니다.');
  }
  
  // 2. 새 사용자 생성
  const newUser = new User(userData);
  return await newUser.save();
}

 

 

4. 실제 자주 사용하는 해결방법 패턴

4-1. 에러 핸들링 패턴

// Express.js + Mongoose 에러 처리
app.post('/users', async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    if (error.code === 11000) {
      // E11000 에러 처리
      const field = Object.keys(error.keyPattern)[0];
      return res.status(400).json({
        error: `${field} 값이 이미 존재합니다.`,
        field: field
      });
    }
    res.status(500).json({ error: '서버 에러' });
  }
});

4-2. 대량 데이터 처리 시 해결법

// 대량 삽입 시 ordered: false 옵션 사용
try {
  await db.users.insertMany(usersData, { ordered: false });
} catch (error) {
  // 일부 중복 에러는 무시하고 나머지는 삽입
  console.log(`${error.result.nInserted}개 문서 삽입 완료`);
  console.log(`${error.writeErrors.length}개 중복 에러 발생`);
}

 

 

5. E11000 에러 예방 방법

5-1. 스키마 설계 시 고려사항

필드 타입 권장 인덱스 설정 주의사항
이메일 { unique: true, sparse: true } null 값 허용 시 sparse 필수
사용자명 { unique: true } 항상 값이 있어야 함
전화번호 { unique: true, sparse: true } 선택 입력 시 sparse 사용

5-2. 개발 과정에서의 예방법

1. 개발 환경에서 충분한 테스트

// 테스트 코드 예제
describe('User 생성 테스트', () => {
  it('중복 이메일로 사용자 생성 시 에러 발생', async () => {
    await User.create({ email: 'test@example.com', name: '사용자1' });
    
    await expect(
      User.create({ email: 'test@example.com', name: '사용자2' })
    ).rejects.toThrow(/duplicate key error/);
  });
});

2. 데이터베이스 마이그레이션 전략

// 안전한 인덱스 추가 방법
async function addUniqueIndex() {
  try {
    // 1. 먼저 중복 데이터 정리
    await cleanupDuplicateData();
    
    // 2. 인덱스 생성
    await db.users.createIndex(
      { email: 1 }, 
      { unique: true, background: true }
    );
    
    console.log('인덱스 생성 완료');
  } catch (error) {
    console.error('인덱스 생성 실패:', error);
  }
}

 

E11000 duplicate key error는 MongoDB의 데이터 무결성을 보장하는 중요한 기능이기도 합니다. 에러가 발생했다고 해서 무조건 제거하려고 하지 말고, 왜 발생했는지 원인을 파악한 후 적절한 해결책을 적용하는 것이 중요합니다. 실무에서는 사전 예방이 가장 중요하므로, 스키마 설계 단계부터 신중하게 접근하고 충분한 테스트를 거치시기 바랍니다. 특히 프로덕션 환경에서 인덱스를 변경할 때는 반드시 백업을 먼저 수행하고, 단계적으로 진행하시길 바랍니다.

 

댓글 남기기