def get_record_detail(self, pk: int) -> TastedRecord:
"""기록 상세 조회"""
return (
TastedRecord.objects
.select_related("author", "bean", "taste_review")
.prefetch_related(
Prefetch("photo_set", queryset=Photo.objects.only("photo_url")),
)
.get(pk=pk)
)
위와 같은 방식으로 기록물에 대해 작성자(사용자), 원두, 시음 리뷰 테이블은 왜래키로 참조하고 있었기 때문에 select_related를 통해 한번에 Join을 해놓은 상태이다.
해당 시음기록이 사진 테이블을 역참조 하고 있기 때문에 prefetch_related를 사용해서 메모리에서 결합하여 사용하도록 최적화를 하였다.

그런데 로깅을 해보니 photo를 가져오는 부분에서 N+1 쿼리 문제가 발생하고 있었다.
prefetch_related를 통해서 해당 게시물의 사진 url 만 가져오는것이 목적이었다.
결과적으로 말하자면 Prefetch의 쿼리셋에서 only로 photo_url만 가져오도록 했던게 문제가 되었다.
쿼리를 하나씩 살펴보면

사진들을 역참조하여 데이터를 미리 가져오는 쿼리이다.
이때 only 매서드에 적었던 photo_url과 photo의 id와 함께 가져온다.

위에서 only("photo_url")는 id와 photo_url만 로드해왔다.
하지만 ORM이 데이터를 결합하거나 객체를 초기화 하려면 기본적으로 필요한 추가 필드인 tasted_record_id (왜래키)가 없기 때문에 개별적으로 가져오기 위해 추가 쿼리를 실행하게 되었다.
TastedRecord의 photo_set은 Photo 테이블의 tasted_record_id 필드를 통해 역참조가 되고있다.
즉 Photo 객체를 통해 TastedRecord와의 관계를 확인할려면 tasted_record_id가 필요한것이다.
이렇게 쿼리를 photo.id 만 다른 형태로 동일한 쿼리가 3번 실행되는 문제가 발생했다.
이는 only() 메서드를 사용하면서 발생한 지연 로딩(Lazy Loading) 문제로 인해, 필요한 필드를 가져오기 위해 매번 추가 쿼리를 실행한 것이 근본적인 원인이었다.
그렇다면 역참조 하고 있는 왜래키(tasted_record_id)로 only에서 같이 가져오도록 하면 N+1 문제를 해결할 수 있지 않을까?
Prefetch(
"photo_set", queryset=Photo.objects.only("photo_url", "tasted_record_id")
),

실행해본 결과 왜래키를 함께 가져오도록 하면서 문제가 해결된것을 볼 수 있었다.
저렇게 필요한 필드만 가져오도록 하는것은 객체가 너무 많은 정보를 가지고 있을때 좀 더 적합한 방식이라고 생각한다.
나에게 지금 상황에서는 해당 객체의 모든 필드를 가져오더라도 상관 없을 정도라고 생각했고 이와 같이 역참조 하도록 수정하였다.
def get_record_detail(self, pk: int) -> TastedRecord:
"""기록 상세 조회"""
return (
TastedRecord.objects
.select_related("author", "bean", "taste_review")
.prefetch_related("photo_set")
.get(pk=pk)
)
(코드 가독성에서도 좀 더 편한거 같다 ㅎㅎ)