[실전 Django - database 정복기 3부] 대용량 데이터 처리: "for문 돌리지 말고 Bulk를 믿으세요"

2025. 12. 12. 17:04·ServerDev/DJango

 

1편에서 데이터의 정합성(동시성 제어)을 다뤘고, 2편에서 데이터의 조회 속도(N+1 문제)를 최적화했습니다.

 

이제 시스템은 안전하고 빨라졌습니다. 하지만 마케팅 팀에서 "엑셀로 정리한 회원 10만 명 정보를 한 번에 업데이트해주세요"라는 요청이 온다면 어떨까요? 혹은 로그 데이터를 DB에 대량으로 쌓아야 한다면요?

 

반복문(for)을 돌며 한 땀 한 땀 save()를 호출하다가 타임아웃으로 서버가 뻗어버린 경험이 있다면, 이번 3편이 그 해결책이 될 것입니다.


대용량 데이터 처리: "for문 돌리지 말고 Bulk를 믿으세요"

1. 엑셀 업로드 버튼만 누르면 서버가 죽어요

백오피스(Admin) 개발을 하다 보면 가장 흔하게 받는 요청 중 하나가 대량 등록/수정 기능입니다.
"상품 5만 개 가격을 일괄 변경하고 싶어요.", "신규 회원 1만 명을 엑셀로 업로드할게요."

처음에는 별생각 없이 코드를 짭니다. 파일을 읽고, for 문을 돌면서 하나씩 저장하죠. 로컬에서 데이터 10개로 테스트할 땐 순식간에 끝납니다. 하지만 운영 서버에 배포하고 실제 데이터를 넣는 순간, 브라우저는 뱅글뱅글 돌기만 하다가 504 Gateway Timeout을 뱉으며 장렬히 전사합니다.

범인은 바로 데이터베이스 왕복 비용(Round Trip Time)입니다. 오늘은 10만 번 왔다 갔다 해야 할 작업을 단 한 번으로 줄여주는 Django의 강력한 무기, Bulk 연산에 대해 알아보겠습니다.

 

 


 

 

2. 최악의 시나리오: for 문 안의 save()

 

가장 직관적이지만 성능상 최악인 코드입니다.

# views.py (나쁜 예시)

def import_users(data_list):
    # data_list: 엑셀에서 읽어온 10,000명의 데이터 딕셔너리 리스트

    for row in data_list:
        # 1. 객체 생성 (메모리)
        user = User(
            name=row['name'],
            email=row['email'],
            age=row['age']
        )
        # 2. DB 저장 (네트워크 통신 발생!)
        user.save() 

데이터가 10,000개라면?

  1. DB 연결 획득
  2. INSERT INTO user ... 쿼리 전송
  3. DB 처리 및 응답 수신
  4. DB 연결 반환

이 과정을 10,000번 반복합니다. 쿼리 자체의 속도보다, 네트워크를 타고 DB에 다녀오는 시간이 훨씬 더 걸립니다. 마치 이삿짐을 나르는데 박스 하나 들고 트럭에 갔다가, 다시 올라와서 하나 들고 가는 꼴입니다.

 

 


 

3. 해결책 1: bulk_create (한 방에 넣기)

이삿짐을 트럭에 한 번에 싣고 가는 방법입니다. Django의 bulk_create는 여러 개의 객체를 리스트로 만들어 넘기면, 단 1번의 INSERT 쿼리로 모든 데이터를 저장합니다.

코드 적용

# views.py (개선된 예시 - 생성)

def import_users_bulk(data_list):
    user_objects = []

    # 1. 객체들을 메모리에 리스트로 쌓습니다. (DB 접근 X)
    for row in data_list:
        user = User(
            name=row['name'],
            email=row['email'],
            age=row['age']
        )
        user_objects.append(user)

    # 2. 한 번에 저장합니다. (쿼리 1방)
    User.objects.bulk_create(user_objects)

동작 원리 & 성능 차이

Django는 내부적으로 다음과 같은 멀티 로우(Multi-row) INSERT SQL을 생성합니다.

INSERT INTO user (name, email, age) VALUES 
('철수', 'a@a.com', 20),
('영희', 'b@b.com', 22),
...
('민수', 'z@z.com', 25);

 

 

실제 벤치마크를 돌려보면, 데이터 1만 개 기준 save() 반복이 30초 걸린다면 bulk_create는 0.5초 미만으로 끝납니다. 속도 차이가 수십 배에서 수백 배까지 납니다.

 

 


 

4. 해결책 2: bulk_update (한 방에 수정하기)

"생성은 알겠는데, 수정은 어떻게 하죠?"
과거 Django 버전(2.2 미만)에서는 bulk_update가 없어서 별별 꼼수(삭제 후 재생성 등)를 썼지만, 이제는 공식 지원합니다.

코드 적용

# views.py (개선된 예시 - 수정)

def increase_user_points(user_ids):
    # 1. 수정할 객체들을 가져옵니다.
    users = User.objects.filter(id__in=user_ids)

    updates = []
    for user in users:
        # 2. 메모리 상에서 값을 변경합니다.
        user.point += 100
        updates.append(user)

    # 3. 변경할 필드('point')를 명시하여 한 번에 업데이트합니다.
    User.objects.bulk_update(updates, ['point'])

동작 원리

bulk_update는 SQL의 CASE ... WHEN 구문을 사용하여 마법을 부립니다.

UPDATE user 
SET point = CASE
    WHEN id = 1 THEN 1100
    WHEN id = 2 THEN 2500
    ...
    ELSE point
END
WHERE id IN (1, 2, ...);

단 한 번의 쿼리로 수천 명의 포인트가 각기 다른 값으로 업데이트됩니다.

 


 

5.  주의사항: Bulk의 치명적인 함정 (필독)

 

Bulk 연산은 빠르지만, ORM의 편리한 기능 일부를 포기해야 합니다. 이걸 모르고 썼다가 "데이터가 왜 이러지?" 하며 밤을 새우게 됩니다.

1) save() 메서드가 호출되지 않습니다.

모델의 save() 메서드를 오버라이딩해서 "저장될 때마다 로그를 남긴다"거나 "이미지를 리사이징한다"는 로직을 넣어뒀다면? Bulk 연산 시에는 이 로직이 무시됩니다. 순수 SQL 레벨에서 처리하기 때문입니다.

2) Signal(pre_save, post_save)이 발생하지 않습니다.

"회원 가입(post_save) 시 환영 이메일 발송" 같은 로직을 시그널(Signal)로 구현했다면? bulk_create로 만든 회원에게는 이메일이 가지 않습니다.

해결책으로는 Bulk 연산 후 필요한 로직을 수동으로 실행해주거나, 성능을 포기하고 save()를 써야 합니다. 트레이드오프를 잘 고려해야 합니다.

3) auto_now (updated_at) 필드 갱신 주의

bulk_update를 할 때 updated_at 같은 자동 갱신 필드가 DB 종류나 Django 버전에 따라 갱신되지 않을 수 있습니다. 안전하게 하려면 업데이트할 필드 목록에 updated_at을 포함하고, 파이썬 코드에서 timezone.now()로 직접 값을 넣어주는 것이 좋습니다.

 


6. 심화: 메모리 터짐 방지 (chunk_size)

 

"100만 개를 bulk_create 했더니 서버가 뻗었어요!"

한 번에 쿼리를 보내는 건 좋지만, 100만 개의 객체를 리스트([])에 담으면 파이썬 메모리(RAM)가 버티지 못합니다. 또한, SQL 패킷 크기 제한에 걸릴 수도 있습니다. 이때는 batch_size 옵션을 사용하여 데이터를 적절히 쪼개서 보내야 합니다.

# 100만 개 데이터라 가정
large_data_list = [...] 

# 1000개씩 끊어서 INSERT 쿼리를 날립니다. (총 1000번의 쿼리)
# 100만 번(save) vs 1번(bulk) vs 1000번(batch) -> 안정성과 속도의 균형
User.objects.bulk_create(large_data_list, batch_size=1000)

 

 


 

7. 마치며: 속도와 안정성 사이에서

 

Bulk 연산은 대용량 데이터 처리의 구세주입니다. 하지만 save() 메서드와 시그널을 우회한다는 점을 반드시 기억해야 합니다.

 

오늘의 요약

  1. 반복문 속 save()는 죄악이다. (데이터가 많을 때)
  2. bulk_create, bulk_update를 쓰면 수십 배 빨라진다.
  3. 단, Signal과 save() 오버라이딩 로직은 실행되지 않는다.
  4. 데이터가 너무 많으면 batch_size로 쪼개서 보내라.

데이터 정합성(1편), 조회 성능(2편), 그리고 대량 처리 성능(3편)까지 챙겼습니다. 이제 우리 서버는 꽤 튼튼해졌습니다.

하지만, "분명히 트랜잭션 안에서 데이터를 저장했는데, 비동기 작업(Celery)에서 데이터를 못 찾는 유령 같은 현상"을 겪어보신 적 있나요? 코드는 완벽한데 간헐적으로 에러가 나는 이 상황, 정말 미치게 만듭니다.

다음 포스팅 [ 트랜잭션과 Celery의 미묘한 관계: "왜 알림이 안 가죠?"] 에서는 트랜잭션 커밋 시점과 비동기 작업의 실행 타이밍이 어긋나서 생기는 문제와, transaction.on_commit을 이용한 우아한 해결책에 대해 알아보겠습니다.

 

긴 글 읽어주셔서 감사합니다!

'ServerDev > DJango' 카테고리의 다른 글

[실전 Django - database 정복기 5부] 인덱스(Index) 설계: "쿼리가 느리다면 답은 정해져 있다"  (0) 2025.12.12
[실전 Django - database 정복기 4부] 트랜잭션과 Celery의 미묘한 관계: "왜 알림이 안 가죠?"  (0) 2025.12.12
[실전 Django - database 정복기 2부] 기능은 똑같은데 왜 쿼리가 100방이나 나가죠? (N+1 문제 해결)  (0) 2025.12.12
[실전 Django - database 정복기 1부 ] ORM의 배신, 그리고 select_for_update로 해결한 동시성 이슈  (0) 2025.12.12
'ServerDev/DJango' 카테고리의 다른 글
  • [실전 Django - database 정복기 5부] 인덱스(Index) 설계: "쿼리가 느리다면 답은 정해져 있다"
  • [실전 Django - database 정복기 4부] 트랜잭션과 Celery의 미묘한 관계: "왜 알림이 안 가죠?"
  • [실전 Django - database 정복기 2부] 기능은 똑같은데 왜 쿼리가 100방이나 나가죠? (N+1 문제 해결)
  • [실전 Django - database 정복기 1부 ] ORM의 배신, 그리고 select_for_update로 해결한 동시성 이슈
yeseul-kim01
yeseul-kim01
  • yeseul-kim01
    슬 개발일지
    yeseul-kim01
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 자격증 (1)
        • 정보보안기사 (0)
      • DevOps (17)
        • Docker (6)
        • Kubernetes (1)
        • GitHub Actions (0)
        • AWS (4)
        • Monitoring (1)
        • Nginx (1)
        • GCP (3)
      • ServerDev (34)
        • SpringBoot (13)
        • DJango (5)
        • FastAPI (14)
        • Next (0)
        • Flask (0)
        • Database (2)
      • Algorithm (2)
        • BFS (0)
        • DFS (1)
        • 다익스트라 (0)
      • CS (8)
      • Data Engineering (1)
      • AI&MLOps (2)
      • Architecture (6)
      • Software Engineering (0)
        • Library Packaging (0)
      • Project (5)
        • docx-generator (0)
        • speak-note (2)
        • ms-serving (1)
        • keyshield (2)
      • ProgrammingLanguages (3)
        • Python (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    동시성제어
    multipartfile
    grpc
    프로젝트기록-speaknote
    프로젝트기록-KeyShield
    백엔드
    FastAPI - CORS 마스터
    실시간시스템
    FastAPI
    Django
    비동기처리
    KeyShield
    멀티모듈
    STT
    KServe
    SpringBoot
    Kubernetes
    MLops
    NLP부트캠프
    docker
    rag
    di
    실무일기-인프라편
    depends
    SpeakNote
    트러블슈팅
    아키텍처
    아키텍처설계
    하이브리드아키텍처
    실무일기-백엔드편
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yeseul-kim01
[실전 Django - database 정복기 3부] 대용량 데이터 처리: "for문 돌리지 말고 Bulk를 믿으세요"
상단으로

티스토리툴바