Django on_commit, 작업 발행 누락까지 막아줄까?

DB에 저장한 직후 백그라운드 작업을 보낼 때는 먼저 실패 지점을 나눠야 합니다. Django의 on_commit()은 커밋보다 작업이 먼저 시작되는 순서 문제를 줄여줍니다. 하지만 커밋 뒤 프로세스가 멈추거나 큐 전달이 실패하는 경우까지 복구해 주지는 않습니다. 후속 작업의 누락을 다시 찾아야 한다면, 업무 데이터와 발행할 이벤트를 함께 저장하는 아웃박스(outbox)를 검토할 때입니다.

확인일: 2026년 10월 10일 · 문서 검토 기반 판단 가이드 · Django 6.1 문서 기준. 아래 예제는 실행하지 않았으며 문법과 문서를 대조했습니다. 실제 장애 경험이나 운영 실험 결과가 아닙니다.

1. 먼저 “언제 실패했는가”를 구분합니다

예를 들어 보고서 생성 요청을 DB에 기록하고 작업 큐로 전달한다고 합시다. 트랜잭션 안에서 바로 발행하면 워커가 커밋 전에 조회를 시도해 새 레코드를 찾지 못할 수 있습니다. Celery 공식 문서도 이 순서 문제를 설명합니다. 반대로 발행 이후 DB가 롤백되면, 외부로 나간 작업만 남을 수 있습니다. Celery의 커밋 후 작업 발행 설명

Django의 기본값은 자동 커밋입니다. 필요한 구간은 atomic()으로 감싸거나, ATOMIC_REQUESTS=True로 각 뷰의 실행을 트랜잭션에 넣을 수 있습니다. 후자의 경우에도 미들웨어와 템플릿 응답 렌더링은 트랜잭션 밖입니다. 호출 위치에 활성 트랜잭션이 없다면 on_commit() 콜백도 즉시 실행됩니다. “이 함수를 썼으니 항상 나중에 실행된다”는 가정부터 확인해야 합니다. Django 기본 트랜잭션 동작

2. on_commit이 기다리는 범위

  • 정상 커밋: 해당 DB 트랜잭션이 성공한 뒤 콜백을 실행합니다. 롤백되면 등록한 콜백은 버립니다.
  • 중첩 atomic: 안쪽 블록이 끝나도 바깥 트랜잭션까지 기다립니다. 안쪽 저장점으로 롤백하면 그 안에서 등록한 콜백은 실행하지 않습니다.
  • 콜백 예외: 이미 커밋된 DB는 되돌아가지 않습니다. 기본 robust=False에서 처리하지 않은 예외가 나면 뒤에 등록한 콜백도 실행되지 않습니다.
  • robust=True: Exception 계열 오류를 기록하고 뒤쪽 콜백을 계속 실행합니다. 실패한 콜백의 재시도는 제공하지 않습니다.

서로 독립적인 캐시 갱신 두 개와, 앞 단계의 성공이 꼭 필요한 작업 두 개는 다르게 다뤄야 합니다. 후자에 robust=True만 붙여 계속 진행시키는 것은 복구 설계가 되지 않습니다. 위 동작의 근거는 Django 커밋 후 처리 문서입니다.

3. 미실행 예제: DB 롤백과 발행 기록을 따로 보기

파일명은 django_on_commit_docs_only.py입니다. Python 3.12~3.14와 Django 6.1.2가 이미 설치된 별도 실습 환경을 전제로 합니다. 6.1.2는 2026년 10월 6일 발표된 정식 보안 릴리스이며, 이 예제 때문에 새 기능으로 소개하는 것은 아닙니다. 릴리스 확인 · Python 호환성

검증 한계: 작성 환경에는 Django가 없어 런타임 실행을 하지 않았습니다. 아래 코드는 메모리 SQLite와 합성 발행 목록만 사용하도록 작성했습니다. 실제 큐, 워커의 동시 조회, 네트워크 전송과 장애 복구는 검증하지 않습니다.

"""Document-reviewed example, NOT executed with Django as of 2026-10-10.

Prerequisite: a disposable Python environment with Django 6.1.2 already installed.
Run: python django_on_commit_docs_only.py
Only an in-memory SQLite database and a synthetic dispatch list are used.
This illustrates rollback/order semantics, not a real worker race or delivery.
"""
from functools import partial

import django
from django.conf import settings

settings.configure(
    INSTALLED_APPS=[],
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
)
django.setup()

from django.db import connection, transaction

with connection.cursor() as cursor:
    cursor.execute("CREATE TABLE demo_job (id TEXT PRIMARY KEY)")

sent = []  # Synthetic dispatch record, not a durable queue or an outbox.


def dispatch(job_id):
    sent.append(job_id)


def scenario(job_id, *, deferred, abort):
    sent.clear()
    try:
        with transaction.atomic():
            with connection.cursor() as cursor:
                cursor.execute("INSERT INTO demo_job (id) VALUES (%s)", [job_id])
            if deferred:
                transaction.on_commit(partial(dispatch, job_id))
                assert sent == []
            else:
                dispatch(job_id)
            if abort:
                raise ValueError("synthetic rollback")
    except ValueError:
        pass
    with connection.cursor() as cursor:
        cursor.execute("SELECT COUNT(*) FROM demo_job WHERE id = %s", [job_id])
        exists = bool(cursor.fetchone()[0])
    print(job_id, exists, sent)


scenario("direct-rollback", deferred=False, abort=True)
scenario("deferred-rollback", deferred=True, abort=True)
scenario("deferred-commit", deferred=True, abort=False)
connection.close()

실행 명령: python django_on_commit_docs_only.py

문서상 예상 출력이며 실제 측정 결과가 아닙니다. 각 줄은 시나리오 이름, DB 행 존재 여부, 합성 발행 목록입니다.

direct-rollback False ['direct-rollback']
deferred-rollback False []
deferred-commit True ['deferred-commit']

이 예제를 실행한다면 먼저 세 출력이 일치하는지 확인하십시오. 일치하더라도 실제 큐의 누락·중복 전달이 해결됐다는 근거로 쓰면 안 됩니다. 리스트는 프로세스 메모리일 뿐, 재시작 뒤 복구할 발행 기록이 아닙니다.

4. DB만 남은 작업을 찾아야 하면 아웃박스를 검토합니다

커밋 성공과 콜백 발행 성공은 별개의 단계입니다. 그 사이 중단에 대비하는 아웃박스 방식은 업무 행과 이벤트 행을 같은 DB 트랜잭션에 저장하고, 별도 전달기가 미처리 이벤트를 읽어 큐로 보냅니다. 발행에 실패해도 다시 찾을 기록을 남기는 구조입니다. AWS의 transactional outbox 설명

그렇다고 “정확히 한 번 처리”가 자동으로 생기지는 않습니다. 전달은 성공했지만 완료 표시 전에 중단되면 같은 이벤트를 다시 보낼 수 있습니다. 소비자는 동일한 이벤트 ID가 반복돼도 업무 효과를 중복 적용하지 않도록 설계해야 합니다.

작은 팀을 위한 판단 기준은 다음과 같습니다. 이는 위 문서를 바탕으로 정리한 설계 제안이며 성능 비교 결과가 아닙니다.

  • on_commit부터 적용: 당장의 문제가 커밋 전 실행이고, 후속 작업 누락을 허용하거나 기존 대조·재처리 수단으로 복구할 수 있을 때.
  • 아웃박스 검토: 저장된 요청마다 후속 처리가 필요하고, 재시작 후에도 미처리 목록을 조회·재전송해야 할 때.
  • 도입 전에 정할 일: 이벤트 ID, 중복 처리 기준, 재시도 간격, 오래된 미처리 건의 알림, 완료 기록 보존 기간과 담당자.

두 방식은 함께 쓸 수도 있습니다. 이벤트 행을 먼저 저장하고 on_commit()으로 전달기를 빨리 깨우되, 깨우기에 실패해도 전달기가 미처리 행을 다시 찾게 합니다. 이 경우 복구의 근거는 콜백 자체가 아니라 저장된 이벤트입니다. 반면 단순한 부가 알림 하나에 아웃박스를 도입하면 전달기와 미처리 기록을 운영하는 부담이 더 클 수 있습니다.

5. 코드 리뷰에서는 다섯 경계를 확인합니다

  1. 등록 위치: 실제로 열린 트랜잭션 안에서 등록하는가? 여러 DB를 쓰면 using이 업무 쓰기 대상과 같은가?
  2. 롤백: 전체 롤백과 내부 저장점 롤백에서 어떤 콜백이 없어져야 하는가?
  3. 커밋 뒤 예외: DB 행은 남았는데 요청 처리나 콜백이 실패할 때, 무조건 재요청하면 업무가 중복되지 않는가?
  4. 프로세스 중단: 커밋 뒤 발행 전에 멈춘 작업을 재시작 후 무엇으로 찾는가?
  5. 중복 전달: 발행 뒤 완료 표시 전에 멈춰도 동일 이벤트의 업무 효과는 한 번만 적용되는가?

등록된 콜백 목록과 실행에 따른 효과는 TestCase.captureOnCommitCallbacks(execute=True)로 확인할 수 있습니다. 이는 커밋을 흉내 내 콜백을 실행하는 검사입니다. 실제 커밋·롤백 경계는 TransactionTestCase 등으로 따로 점검해야 합니다. 위 다섯 항목은 앞으로 적용할 테스트 계획이며, 이 글에서 통과시킨 결과가 아닙니다. Django 테스트 도구

또한 커밋 뒤 워커가 읽기 복제본을 조회하면 복제 지연이 남을 수 있습니다. on_commit()만으로 모든 조회 실패를 해결했다고 결론 내리지 마십시오. Django 다중 DB 문서의 복제 지연 주의사항

적용 순서: 먼저 발행 위치와 트랜잭션 범위를 확인하고, 누락된 작업을 재시작 뒤 찾을 수 있는지 답하십시오. 그 답이 없고 누락을 허용할 수 없다면 아웃박스와 중복 처리 설계를 함께 검토하는 편이 좋습니다.

공식 문서 조사와 AI 작성 보조를 활용해 구성한 설명입니다. 문서 검토, 미실행 예제, 설계 제안을 구분했으며 실제 서비스 운영 경험이나 테스트 성과를 주장하지 않습니다.

댓글