1. 이슈 개요

Django 환경에서 transaction.on_commit()을 활용하여 Celery 태스크를 호출하는 구조에서,
ManyToMany 필드의 데이터가 Celery 태스크 내부에서는 비어 있는 현상이 발생.


2. 현상

  • post.categories.add(...), post.tags.add(...) 를 모두 완료한 후
  • transaction.on_commit() 콜백으로 translate_post.delay() 호출
  • Celery 태스크 내부에서 post.categories.all(), post.tags.all()을 조회했더니 빈 리스트([])가 반환됨

3. 환경 구성

  • WRITE: GCP VM의 PostgreSQL 마스터
  • READ: Raspberry Pi의 PostgreSQL 레플리카 (streaming replication)
  • Django DB router를 통해 읽기 요청은 레플리카로 보내도록 설정
  • ManyToMany 관계 필드를 사용하는 Post 모델

4. 로그 분석

✅ 순서 요약 (예시 시간 기준)

  1. 13:13:56.728 — Post 생성
  2. 13:14:00.922 — 마지막 TaggedItem (ManyToMany) 쿼리
  3. 13:14:01.688on_commit() 실행 → Celery 호출
  4. 13:14:01.772translate_post() 내 조회 결과:
    • categories: []
    • tags: []

즉, 순서는 맞지만 내용이 반영되지 않음


5. 원인 분석

✅ PostgreSQL 레플리카 지연

  • PostgreSQL의 기본 replication은 비동기적(async)으로 작동
  • Master DB의 변경 사항이 Replica에 수 ms ~ 수백 ms 지연되어 반영됨
  • add()로 연결되는 ManyToMany의 중간 테이블 기록이 아직 레플리카에 반영되지 않았던 것

✅ Django의 on_commit() 작동 방식

  • on_commit()은 Django 트랜잭션이 커밋된 직후 실행
  • 하지만 Celery는 별도 프로세스이고, 읽기는 레플리카 DB를 사용
  • 결과적으로 on_commit 시점에는 레플리카가 아직 업데이트 전 상태

6. 검증 포인트

  • Post 생성 로그는 존재
  • add() 쿼리 로그도 정상 실행
  • 실제 쿼리 순서와 태스크 호출 순서는 옳음
  • 문제는 조회 시점의 DB가 레플리카였다는 점

7. 해결 방안

1. Celery 태스크에서 마스터 DB로 강제 읽기

# ORM 방식
post = Post.objects.using('default').get(id=post_id)
tags = post.tags.using('default').all()
categories = post.categories.using('default').all()

2. Celery 전용 DB Router 구성

settings.py에 Celery 태스크일 경우 무조건 master 사용하도록 설정 가능


3. 레플리카가 반드시 최신 상태여야 하는 경우:

  • PostgreSQL의 synchronous_commit = on 설정 필요 (성능 저하 가능)

8. 결론

이 문제는 Django의 문제가 아닌, 비동기 replication 환경에서의 지연과 ORM의 읽기 우선 정책이 충돌한 결과다.
해결의 핵심은 Celery 태스크에서 강제로 마스터 DB를 사용하도록 보장하는 것이다.


9. Jesse의 코멘트

"on_commit()은 분명히 정확한 타이밍에 실행되었지만, 읽은 DB가 레플리카였다는 사실 하나가 문제의 본질이었다.
Django와 Celery는 아무 잘못이 없다. 결국 시스템 아키텍처를 이해하고 조율해야 하는 것은 개발자의 몫이다."