DRF이란 장고 안에서 RESTful API 서버를 유연하게 구축할 수 있도록 도와주는 라이브러리이다. API 생성, 요청 처리, 권한 관리, 데이터 직렬화 등 API 개발에 필요한 추가 기능을 제공한다.
Django ORM에서 objects
는 쿼리셋을 관리하는 매니저 역할을 한다.
makemigrations -> 생성된 SQL 문 항상 확인하는거 필요
Django ORM에서는 데이터베이스 질의를 QuerySet을 통해 수행한다. QuerySet은 Lazy Loading(지연 로딩)과 Caching(캐싱)을 지원하여, 불필요한 데이터베이스 호출을 줄일 수 있다.
user_queryset : QuerySet[User] = User.objects.all()
QuerySet은 바로 실행되지 않고, 실제 데이터가 필요할 때 실행된다. 아래 예제를 봐보자.
user_queryset: QuerySet[User] = User.objects.all()
위 코드는 데이터베이스에서 사용자 데이터를 가져오는 쿼리를 정의했지만, 아직 실행되지는 않는다.
if user_queryset.exists(): # select 1 from user where username=1234 limit 1;
exists()를 호출하면 즉시 실행(Eager Execution)되며, 최소한의 데이터만 조회하는 SQL이 실행된다.
user_list: List[User] = list(User.objects.all()) # 쿼리 즉시 수행
list로 변환하려면 데이터가 필요하기 때문에 QuerySet이 즉시 실행되며, 결과가 캐싱된다. 이후 같은 QuerySet을 재사용하면, 캐싱된 데이터를 활용하여 SQL 실행을 줄일 수 있다.
user1: User = user_queryset[0]
만약 캐싱이 안되어있었으면 queryset[0] 때문에 limit 1로 가져오는 SQL이 나갔을 것이다.
QuerySet 내부 구조
Django의 QuerySet은 내부적으로 여러 속성을 가지고 있다.
class QuerySet:
query = Query() # 메인 쿼리
prefetch_related_lookups = ("order_set", "menu_set") # 추가 쿼리셋 정보 저장
result_cache = [] # 쿼리 결과 캐싱
iterable_class = ModelIterable # 데이터 반환 방식 결정
next()
리스트 컴프리헨션을 사용하면 데이터가 없을 때 에러가 발생할 수 있다.
이때 next()를 활용하면 안전하게 데이터를 조회할 수 있다. None
을 기본값으로 지정하면, 데이터가 없을 경우 에러가 발생하지 않고 None을 반환한다.
next((user for user in user_list if user.first_name == "aa"), None)
get()
단일 객체를 조회할 때 사용하며, 고유한 데이터를 찾을 때만 사용해야 한다. 데이터가 없으면 DoesNotExist 예외 발생하며, 여러 개의 데이터가 존재하면 MultipleObjectsReturned 예외가 발생한다. 따라서 try except로 예외 처리를 해줘야 한다.
user = User.objects.get(username="test_user")
filter()
filter()는 QuerySet을 반환하므로 지연 로딩이 적용된다.
즉시 로딩하려면 [0]을 사용해야 하지만, 데이터가 없으면 IndexError가 발생할 수 있으므로 별도 처리가 필요하다.
user = User.objects.filter(username="test_user").first() # 안전한 조회
first()
: 데이터가 존재하지 않을 때 예외가 발생하지 않고, None이 반환되기 때문에 안전하게 처리할 수 있다.count()
QuerySet의 개수를 구할 때 count()를 사용해야 한다.
len(QuerySet)을
사용하면 지연 로딩이 풀려서, 불필요한 SQL 실행이 발생할 수 있기 때문이다.
user_count = User.objects.filter(is_active=True).count() # select count(*) from user;
Django에서는 쿼리 매니저를 직접 커스텀하여 유지보수성을 높일 수 있다.
class ContractManager(models.Manager):
def recently_expired(self, store: Store) -> QuerySet:
return self.get_queryset().filter(store=store).order_by("-end_date")
위와 같이 매니저를 정의하면, Contract.objects.recently_expired(store)와 같이 사용할 수 있다
장고에서 N+1 문제는 어떻게 해결할까? 아래 예제를 보면, menu.restaurant.name
을 참조할 때마다 추가 SQL이 실행되므로 성능이 저하된다.
menu_queryset = Menu.objects.filter(name_contains="파스타")
for menu in menu_queryset:
menu.name # 1. 첫 번째 SQL 쿼리 수행
menu.price # 2. 캐싱된 데이터 사용
menu.restarant.name # 3. 다른 엔티티의 name 은 캐싱 X -> N+1 문제 발생
따라서 아래와 같은 즉시 로딩 기법을 사용해, N+1 문제를 해결할 수 있다.
select_related()
N:1
관계에서만 사용 가능하다.Menu.objects
.select_related("a", "b") # 여러개 가능
.filter()
prefetch_related()
SQL IN
쿼리가 나간다. 즉 1 + N
이 아닌 1 + 1
이 된다.Restaurant.objects.filter.prefetch_related(
Prefetch("order_set", queryset = Order.objects.all(), # 내부에 쿼리 셋을 직접 지정 가능
Prefecth("menu_set", queryset = Menu.objects.filter(name__contains = "파스타"))
Django Rest Framework(DRF)에서는 Serializer를 활용하여 데이터를 직렬화 및 역직렬화할 수 있다.
s = UserSerializer(data=request.data)
s.is_valid(raise_exception=True)
s.save()
is_valid()
모든 유효성 검증이 수행되며, 데이터 타입을 직렬화에 선언한 필드 타입으로 변환해주는 작업이 수행된다.
request로 받은 딕녀너리 데이터가, validated_data
라는 값으로 생성된다.
save()
내부적으로 create() 또는 update() 메서드를 호출한다. POST 요청은 create, PATCH 요청은 update 메서드가 자동으로 호출되며, 각 메서드에서 위에서 검증된 validated_data
를 사용해서 데이터를 처리한다.
# 통합 커스텀 메서드 : 객체 수준이 아니라 비즈니스 수준에서 필요한 검증을 작성
def validate():
# 자동으로 유효성 검증 시 해당 메서드 수행
def validate_{필드명}():
# 여러 필드 혹은 객체 수준에서 복합적으로 유효성 검증이 필요한 경우 사용
class Meta:
validators = [UniqueTogetherValidator(
queryset = User.objects.all(),
fields = ["name", "phone"]
]
또한, 아래와 같이 커스텀 validator + SerializerField 에 부여해서 재사용 높은 코드를 만들 수 있다.
class EnglishOnlyValidator:
message = "{attr_name} 에는 숫자가 포함되면 안 됩니다."
def __init__(self, message):
self.message= message
def __call__(self, value: str, serializer_field):
if value.isalpha():
raise serializers.ValidationError(
detail = self.message.format(attr_name=serializer_field.name)
)
class SignUpSerializer(serializers.Serializer):
first_name = serailizer.CharField(
max_length=127,
validators=[EnglishOnlyValidator()]
)
장고에서 View(controller)의 경로를 설정하려면 다음과 같이 선언해주면 된다.
path(route, view, name)
다음과 같이 함수 기반 뷰를 사용하려면 GenericViewSet
을, 클래스 기반 뷰를 사용하고 싶다면 ModelViewSet
을 상속받으면 된다.
memo_list = MemoModelViewSet.as_view({"get": "list", "post": "create"})
memo_detail = MemoModelViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"})
# 경로 매핑 과정
urlpatterns = [
path("memo", memo_list), // RESTFUL O
path("memo/<int:pk>", memo_detail),
path("memo/recent", MemoModelViewSet.as_view({"get": "recent_memo"})). // RESTFUL X
]
ModelViewSet + ModelSerializer
조합은 안티 패턴이므로 사용하지 않는 것이 좋다. 아래와 같이 비즈니스 로직 없이 자동으로 테이블과 1:1 매칭되는 CRUD API 를 사용하는 것은 개발 편의성은 높더라도 추후 유지보수성이 떨어지기 때문이다.
class UserViewSet(viewsets.GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSchema
views 내부에 직렬화 / 모델 / 유틸 모듈이 존재하며, 이거를 ExceptionHandler가 밖에서 감싸고 있다. 예외는 DRF의 예외 체계인 APIException
을 사용하자.
class CustomException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = ""
위와 같이 모델 계층에서 비즈니스 로직을 처리한 후, 커스텀 예외를 생성하여 반환하면 의존성을 줄일 수 있다.
참고 자료
백엔드 개발을 위한 핸즈온 장고