- 발행일
없는 pk로 들어가면 404가 아니라 500이 났다 — get_object를 덮으면 예외 처리도 같이 덮인다
없는 pk로 들어가면 404가 아니라 500이 났다
관리자 조직 상세 주소에 없는 번호가 들어오면 "페이지를 찾을 수 없습니다"가 아니라 500이 났습니다. 조직·조직 구성원·권한 그룹·사용자의 상세와 수정, 여덟 개 화면이 전부 같았습니다.
원인은 이렇게 생긴 코드였습니다.
def get_object(self):
logger.info(f'OrganizationDetailView.get_object()')
# 최적화된 쿼리: 관련 객체들을 미리 로드
organization = Organization.objects.prefetch_related(
'organizationmember_set__user', # OrganizationMember -> User
).get(pk=self.kwargs.get('pk'))
logger.info(f"Organization object loaded with optimized prefetch_related")
return organization
의도는 분명합니다. 상세 화면이 구성원 목록을 그리니 prefetch_related 로 쿼리를 줄이고 싶었던 거예요. 그 의도 자체는 맞습니다.
문제는 그걸 얹으려고 get_object() 를 통째로 덮었다는 데 있습니다.
덮으면 같이 없어지는 것들
Django 제네릭 뷰의 get_object() 는 원래 이런 일을 합니다.
get_queryset()을 불러 대상 쿼리셋을 정한다- URL의
pk나slug로 좁힌다 - 없으면
Http404를 던진다
세 번째가 핵심이에요. 안에서 queryset.get() 이 던지는 DoesNotExist 를 받아 Http404 로 바꾸기 때문에 없는 pk는 404가 됩니다. 그런데 위 코드는 .get(pk=...) 를 직접 부릅니다. 매니저의 .get() 은 없으면 DoesNotExist 를 던지고, 뷰가 그걸 안 받으니 그대로 500으로 나갑니다.
즉 가져오는 방법을 바꾸려고 덮었는데, 같이 딸려 있던 예외 처리까지 덮었습니다. 겸사겸사 get_queryset() 도 안 거치게 됩니다. 지금 이 뷰들은 쿼리셋에 걸린 조건이 없어서 결과가 같았지만, 나중에 쿼리셋 단에 조건이 하나라도 붙으면 이 여덟 화면만 그 조건을 안 타는 상태가 됩니다.
덮어야 했던 건 다른 자리였다
하고 싶었던 일은 "무엇을 가져오나"가 아니라 "어떤 쿼리셋에서 가져오나"입니다. 그건 get_queryset() 자리예요.
def get_queryset(self):
return super().get_queryset().prefetch_related("organizationmember_set__user")
super() 가 준 쿼리셋에 prefetch_related 만 얹고 돌려줍니다. pk로 좁히는 것도, 없을 때 404를 내는 것도 제네릭 뷰가 원래 하던 대로 합니다. 성능 의도는 그대로 살아 있고요.
여덟 뷰를 같은 모양으로 옮기니 78줄이 26줄이 됐습니다. 줄어든 52줄의 상당 부분은 요청마다 찍히던 logger.info 와, "최적화된 쿼리"라고만 적혀 있어 무엇이 왜 최적화됐는지는 알려 주지 않는 주석이었습니다.
옮기면서 중복도 하나 걷어냈습니다.
# 전
prefetch_related(
'user_permissions',
'groups',
'user_permissions__content_type',
'groups__permissions__content_type',
)
# 후
prefetch_related(
"user_permissions__content_type",
"groups__permissions__content_type",
)
user_permissions__content_type 를 prefetch 하면 user_permissions 는 어차피 같이 가져옵니다. 앞의 두 줄은 같은 걸 한 번 더 적은 것이었어요.
버린 대안
get_object() 안에서 get_object_or_404 만 쓰기. 한 줄만 바꾸면 되니 제일 짧습니다. 안 골랐어요. 500은 사라지지만 get_queryset() 을 건너뛰는 구조는 그대로 남습니다. 여덟 벌의 비슷한 코드도 그대로고요. 증상만 막고 같은 형태를 남기면 다음 사람이 이 모양을 보고 새 뷰에 복사합니다. 실제로 이 여덟 벌이 그렇게 늘어난 모양이었습니다.
검증
- 없는 pk로 여덟 화면에 들어가 404가 뜨는 것을 확인했습니다.
- 정상 pk로 상세·수정을 열어 화면이 같고, 구성원·권한 목록이 그대로 그려지는지 봤습니다.
회귀 테스트는 안 붙였습니다. 이게 이 작업의 가장 약한 부분이에요. "없는 pk면 404"는 뷰 여덟 개에 대해 한 줄짜리 assertEqual(response.status_code, 404) 로 잠글 수 있는 조건인데 안 했습니다. 지금 상태는 같은 실수가 다시 들어와도 아무것도 빨개지지 않아요.
남은 것 · 한계
- 같은 함정이 네 곳 남아 있습니다. 물리 아키텍처 서비스·시나리오의 상세·수정 뷰가 아직
get_object()안에서.get(pk=self.kwargs["pk"])를 직접 부릅니다. 이번 커밋은 사용자·조직 묶음만 훑었고 거기서 멈췄어요. 찾는 건 검색 한 번이면 되는데 범위를 넓히지 않았습니다. get_object()를 덮는 게 늘 틀린 건 아닙니다. 이 저장소에도get_object()override가 수십 곳 있고, 대부분은 pk가 아닌 다른 키로 찾거나 여러 모델 중에 고르는 등 제네릭 뷰가 못 하는 일을 합니다. 이번에 문제였던 건 override 자체가 아니라, 제네릭 뷰가 이미 하던 일을 다시 하면서 일부를 빠뜨린 override 입니다.- 쿼리 수를 재지 않았습니다. 원래 주석이 "최적화된 쿼리"라고 주장하고 있었는데, 그게 실제로 몇 개를 줄였는지는 그때도 지금도 숫자로 남아 있지 않습니다. prefetch 대상을 그대로 옮겼으니 나빠지진 않았겠지만, 옮기면서
assertNumQueries로 한 번 박아 뒀으면 다음에 이 줄을 지워도 되는지 판단할 근거가 생겼을 겁니다.
관련 글: 관리자 화면 12개가 익명에 열려 있었다2026-08-12관리자 화면 12개가 익명에 열려 있었다 — 그중 둘은 GET으로 저장하고 있었다로그아웃 상태로 관리자 공지 목록 주소를 열었더니 화면이 그대로 떴습니다. 같은 파일 안의 분류 뷰 다섯은 막혀 있고 공지 뷰 다섯은 안 막혀 있었어요. 게다가 그중 둘은 GET 요청으로 정렬 순서를 저장하고 있었고, except 절의 bare return 때문에 500까지 났습니다. 믹스인을 붙이면서 붙인 권한 선언이 실은 아무도 안 읽는 값이었다는 것도 같이 적었습니다. · 없는 관계를 prefetch하고 있었다2026-07-20없는 관계를 prefetch하고 있었다 — 다운로드 4종을 죽인 한 단어카탈로그 PNG 다운로드가 500을 뱉어서 들어갔는데, 원인은 성능 최적화라고 믿고 있던 prefetch_related의 인자 하나였습니다. Dataset에 없는 관계명이라 QuerySet 평가 시점에 AttributeError가 났고, 지연 평가 때문에 선언부가 아니라 버튼 클릭에서 터졌어요. 같은 파일 안에서 따옴표 표기가 갈려 grep이 한쪽만 잡은 이야기와, AST로 인자 478건을 전수 대조한 기록입니다. · Django ORM 정리 — 그리고 실무에서 안 통했던 지점2025-04-25Django ORM 정리 — 그리고 실무에서 안 통했던 지점Django ORM의 핵심 원리와 자주 쓰는 메서드를 표로 정리한 레퍼런스입니다. 뒤에 실무에서 실제로 걸린 지점들을 덧붙였습니다 — 지연 평가가 에러 위치를 속이는 문제, update()가 시그널과 auto_now를 건너뛰는 문제, only()가 오히려 쿼리를 늘리는 경우까지. 정리할 때는 몰랐고 운영하면서 배운 것들입니다. · 같은 표준데이터를 두 화면이 198건과 134건으로 세고 있었다2026-08-06같은 표준데이터를 두 화면이 198건과 134건으로 세고 있었다 — 집계 모집단 일원화홈 "데이터 현황" 타일은 198건, 그 타일을 눌러 도착하는 목록 화면은 134건이었습니다. 닷새 전에 "타일이 목록으로 링크하니까 목록에 맞춘다"고 결정하고 주석까지 박아뒀는데도 회귀가 샜습니다. 원인은 결정이 아니라 그 결정을 지킨다던 테스트였어요. 기대값을 상수로 박아둔 정합 테스트는 목록 뷰가 조건을 하나 더 붙여도 계속 초록이었습니다.