HOWTO · Django

Django의 Select_related 메서드

이 기사에서는 QuerySet이 무엇인지와 이를 사용하는 방법을 설명하고, Django의 `select_related()`로 관련 객체를 쿼리와 함께 불러오는 방법을 보여줍니다. 이 메서드는 행을 필터링하지 않습니다.

이 페이지의 내용

Django QuerySet에서 직원의 부서처럼 값이 하나인 관련 객체를 읽을 때 select_related()를 사용하세요. 쿼리와 함께 해당 객체를 불러오므로 반복해서 접근할 때 관련 객체를 별도로 가져오는 일을 줄일 수 있습니다. 행을 필터링하는 기능은 아닙니다.

select_related()의 동작

select_related()는 QuerySet의 객체와 관련 객체를 같은 데이터베이스 쿼리에서 가져오도록 Django에 지시하는 QuerySet 메서드입니다. 직원의 부서처럼 값이 하나인 관계를 코드에서 읽을 때 유용합니다. Django는 관련 테이블을 조인하고 관련 객체를 채워 두므로, 직원마다 별도의 쿼리로 가져오지 않고도 해당 객체에 접근할 수 있습니다.

이 메서드는 관련 데이터를 불러오는 방식을 바꿀 뿐, 직원이나 부서를 필터링하지 않습니다. 가져올 행을 고를 때는 filter() 같은 일반 QuerySet 필터를 사용하세요. select_related()라는 이름은 어떤 관련 행이 조건에 맞는지 제한한다는 뜻이 아니라, 객체를 선택할 때 모델 관계를 따라간다는 의미입니다.

관련 객체를 읽을 때 발생하는 쿼리 문제 이해하기

Django 프로젝트에 Department와 Employee 모델이 있고, Employee.department가 Department를 참조하는 ForeignKey라고 가정해 보겠습니다. Employee.objects.all() 같은 QuerySet은 먼저 직원들을 가져옵니다. 그런 다음 반복문에서 employee.department.name을 읽으면, 해당 모델 인스턴스에 부서가 이미 로드되어 있지 않은 경우 Django가 부서를 가져오기 위한 쿼리를 추가로 실행할 수 있습니다.

이렇게 반복해서 접근하면 N+1 쿼리 패턴이 생길 수 있습니다. 직원들을 가져오는 쿼리 하나에 더해, 관련 부서에 접근할 때마다 추가 쿼리가 실행되는 방식입니다. 정확한 쿼리 수는 어떤 관계에 접근하는지, 객체가 이미 로드되었거나 재사용되는지, 그리고 주변의 쿼리 동작이 어떤지에 따라 달라집니다. 직원이 100명이라고 해서 항상 쿼리 101개가 실행된다고 단정할 수는 없습니다. 여러 직원이 같은 부서를 참조할 수도 있으며, QuerySet과 객체를 어떻게 사용하는지에 따라서도 쿼리 수가 달라집니다.

앞서 가정한 외래 키 관계에서는 관계 필드 이름을 QuerySet에 추가합니다. Django 뷰에서 사용할 표현식은 Employee.objects.all().select_related("department")입니다. 그런 다음 반환된 직원을 반복하면서 필요할 때 employee.department.name을 읽습니다. Django는 SQL 조인을 통해 각 직원 행과 함께 부서 데이터도 가져옵니다.

모델에 해당 관계가 있어야 하며, select_related()에 전달하는 문자열은 department.name 같은 표시 속성이 아니라 관계 필드여야 합니다. 사용자 정의 ForeignKey 필드 이름을 쓴다면 그 필드 이름을 전달하세요. 이 메서드는 QuerySet을 반환하므로 다른 QuerySet 메서드와 연결해 필터링이나 정렬을 함께 구성할 수도 있습니다.

다음 독립 실행형 Python 예시는 직원 4명과 부서 2명을 두고 조회 횟수 차이를 보여 줍니다. 개념을 설명하기 위한 예시일 뿐 Django를 실행하거나 데이터베이스 쿼리를 측정하지는 않습니다. 다음 예시를 실행해 보세요.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

employees = [
    {"name": "Jane", "department_id": 1},
    {"name": "John", "department_id": 2},
    {"name": "Sam", "department_id": 1},
    {"name": "Lee", "department_id": 2},
]
departments = {1: "HR", 2: "Accounts"}

separate_lookups = 1 + len(employees)
joined_lookups = 1
print(f"Separate related lookups: {separate_lookups}")
print(f"Related data loaded with employee query: {joined_lookups}")
print(f"First employee's department: {departments[employees[0]['department_id']]}")

실제 애플리케이션에서는 이 메모리 기반 예시 대신 Django의 QuerySet 메서드를 사용하세요. 예시에서 별도로 로드하는 경우 직원마다 관련 객체를 한 번씩 조회하도록 의도적으로 계산했습니다. 실제 쿼리 수는 Django의 관계 캐시와 코드의 접근 방식에 따라 달라질 수 있습니다.

쿼리 동작과 쿼리 수 확인하기

QuerySet의 query 속성을 사용하면 생성된 SQL을 확인할 수 있습니다. 예를 들어 print(Employee.objects.select_related("department").query)를 실행합니다. 이 방법으로 쿼리에 관련 테이블이 포함되었는지 확인할 수 있지만, 표시되는 것은 SQL 표현이며 요청 전체에서 실제로 실행된 쿼리 수는 아닙니다.

개발 프로젝트에서 요청의 쿼리 수를 측정하려면 Django Debug Toolbar를 사용하거나 테스트 대상 코드 주위에 assertNumQueries() 같은 Django 테스트 도구를 사용하세요. 데이터베이스 내용과 접근 방식을 동일하게 유지한 채 select_related()를 추가하기 전과 후의 같은 작업을 비교합니다. 총계를 해석할 때는 데이터베이스 설정, 미들웨어, 요청에서 실행되는 다른 쿼리도 고려하세요. 코드가 선택한 단일 값 관계를 따라갈 때 관련 객체를 가져오는 별도 쿼리가 줄어드는 것이 기대되는 개선입니다. 그렇다고 요청 전체가 쿼리 하나만 사용한다고 보장되는 것은 아닙니다.

적절한 관계 로딩 방식 선택하기

select_related()는 ForeignKey와 OneToOneField 경로처럼 값이 하나인 관계를 위한 메서드입니다. 해당 관계의 행을 SQL 쿼리에 조인합니다. 모델에 해당 필드가 있다면 select_related("department__manager")처럼 경로를 지정해 값이 하나인 여러 관계를 따라갈 수도 있습니다.

역방향 외래 키나 다대다 필드처럼 여러 객체를 포함하는 관계에는 대신 prefetch_related()를 사용하세요. 이 메서드는 별도 쿼리로 관련 행을 가져온 다음 Python에서 연결하므로, 부모 데이터가 반복되는 큰 조인 결과를 피할 수 있습니다. 관계의 종류와 코드에서 읽을 데이터를 기준으로 선택하세요. 직원 한 명의 부서를 읽을 때는 select_related()를, 직원과 관련된 여러 프로젝트 같은 컬렉션을 읽을 때는 prefetch_related()를 사용합니다. 두 메서드 모두 비효율적인 반복 로딩을 줄이는 데 도움이 되지만, 쿼리 전략은 서로 다릅니다.