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. 로그 분석
✅ 순서 요약 (예시 시간 기준)
13:13:56.728
— Post 생성13:14:00.922
— 마지막TaggedItem
(ManyToMany) 쿼리13:14:01.688
—on_commit()
실행 → Celery 호출13:14:01.772
—translate_post()
내 조회 결과:- categories:
[]
- tags:
[]
- categories:
즉, 순서는 맞지만 내용이 반영되지 않음
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는 아무 잘못이 없다. 결국 시스템 아키텍처를 이해하고 조율해야 하는 것은 개발자의 몫이다."
Add a New Comment