모델을 읽을 때 삭제 관계부터 확인한 이유

블로그의 카테고리 모델은 칼럼 목록뿐 아니라 “카테고리를 지웠을 때 글을 어떻게 할 것인가”도 결정합니다. 이전 예제는 카테고리 삭제 시 글의 연결을 비우는 SET_NULL을 소개했지만, 현재 win-j의 Post는 PROTECT로 삭제를 막습니다. 예제 모델을 실제 모델처럼 복사하면 운영 행동이 달라지는 부분입니다.

2026년 9월 22일 src/blog/models.py를 기준으로 실제 필드와 migration 확인 순서를 정리합니다. 아래 조회 예제는 모델을 다시 정의하거나 테이블을 생성하지 않습니다.

현재 모델에서 중요한 선택

항목 현재 코드 의미
Category.parent 자기 자신을 참조, nullable, SET_NULL 상위 카테고리 삭제 시 하위의 parent를 비울 수 있음
Post.category 필수 ForeignKey, PROTECT 연결된 글이 있으면 해당 카테고리 삭제 차단
Post.summary 최대 280자, blank 허용 폼에서 빈 요약 허용, 별도 NULL 의미를 만들지 않음
Post.slug 고유 문자열 글 URL 식별자 충돌 제한
Post.status 기본 published 발행 상태의 기본값
Post.indexing_status 기본 noindex 생성 직후 검색 검토 완료로 보지 않음

Category에는 원문 예제에 있던 is_active, created_at이 없습니다. Post의 정렬은 고정 여부와 작성일을 사용하며 원문의 published_at 필드도 없습니다. 관리자 list_display에 없는 필드를 적으면 모델 설명은 그럴듯해도 Django 검사에서 문제가 생길 수 있습니다.

ForeignKey의 삭제 정책과 null·blank의 역할은 Django 모델 필드 문서를 참고할 수 있습니다. 특히 blank=True가 DB의 NOT NULL 제약을 자동 해제하는 것은 아닙니다.

삭제하지 않고 관계를 확인하는 코드

다음 코드는 Django shell에서 메타데이터만 읽습니다. 운영 글을 삭제하는 실험이 아닙니다.

from blog.models import Category, Post

for model, name in ((Post, "category"), (Category, "parent")):
    field = model._meta.get_field(name)
    print(model.__name__, name, field.null,
          field.remote_field.on_delete.__name__)

현재 출력은 Post category False PROTECT, Category parent True SET_NULL입니다. 예를 들어 부모 아래 자식 카테고리가 있고 부모에 연결된 글이 없다면, ORM으로 부모를 삭제할 때 자식이 남고 parent가 비워집니다. 반면 부모에 글이 연결되어 있으면 PROTECT 때문에 부모 삭제가 거부됩니다. 실제 관리자 화면의 사전 검사와 오류 안내는 이 ORM 동작 위에 별도로 구현됩니다.

models.py, migration 파일, 실제 DB를 나눠 본다

makemigrations는 현재 모델과 migration 파일의 상태를 비교해 변경 파일을 만듭니다. 운영 DB의 테이블을 보고 모델을 자동으로 맞춰 주는 명령은 아닙니다. migrate는 migration 의존 관계와 적용 기록을 따라 DB 변경을 수행합니다. 이 차이는 Django migration 안내에서 확인할 수 있습니다.

개발 web이 실행 중인 상태에서 다음처럼 조회·점검할 수 있습니다.

docker compose -f .\docker\dev\docker-compose.dev.yml exec web python src/manage.py makemigrations blog --check --dry-run
docker compose -f .\docker\dev\docker-compose.dev.yml exec web python src/manage.py showmigrations blog
docker compose -f .\docker\dev\docker-compose.dev.yml exec web python src/manage.py migrate --plan

첫 명령의 성공은 blog 모델 변경에 대해 새 migration이 필요하지 않다는 검사입니다. 두 번째의 [X]는 적용 기록이며, 세 번째는 앞으로 수행할 계획입니다. 운영 DB의 모든 테이블과 인덱스가 파일과 완벽히 일치한다는 검사는 아닙니다. SQL을 더 확인하려면 존재하는 migration 이름으로 sqlmigrate를 사용하고, 파일 생성과 적용은 검토 뒤 별도로 진행합니다.

필드를 추가할 때 생기는 데이터 문제

비어 있지 않은 테이블에 필수 필드를 추가한다면 기존 행의 값을 어떻게 채울지 먼저 결정합니다. 무조건 같은 임시 값을 넣으면 업무 의미가 틀릴 수 있고, unique 필드라면 중복 제약에 걸릴 수도 있습니다. 필요하면 nullable 필드 추가 → 데이터 보정 → 제약 적용처럼 여러 단계로 나눠 검토합니다.

column does not exist가 나왔다고 운영 서버에서 새 migration을 즉석 생성하지 않습니다. 먼저 올바른 DB에 연결됐는지, 배포한 파일에 필요한 migration이 있는지, 적용 기록이 어떤지 확인합니다. --fake나 migration 파일 삭제도 원인을 확인하지 않은 해결책으로 쓰지 않습니다. 코드 되돌리기만으로 삭제된 데이터가 복구되지 않으므로 스키마 변경 전에는 백업과 복원 절차가 필요합니다.

구조가 준비되면 공개 글을 조회하고 화면에 연결하는 흐름으로 이어집니다. 모델에 필드가 존재하는 것과 방문자가 해당 데이터를 볼 수 있는지는 뷰의 조회 조건에서 다시 결정됩니다.