Django
를 쓰다보면 많은 Controller(View)를 작성해야하고 그 때마다 이걸 class로 만드나? 단순한 function으로 만드나? 고민하게 된다. 최근에 이 선택의 기준에 관한 질문을 받기도 했고, 개인적인 생각을 글로 정리해보는게 좋을 거 같다는 생각이 들었다.
결론부터 말하자면 나는 CBV
를 더 선호한다. 이유는 다음과 같다.
1.
을 바탕으로 코드의 재사용성을 높일 수 있다.물론, 짜고 다시 보지 않을 코드라면 무엇을 쓰든 상관이 없다. 이럴 때는 그냥 적당히 본인이 짜기 편한 쪽으로 골라잡는게 좋을 것이다. 하지만 코드베이스가 오래 살아남으면 살아남을수록 CBV
가 주는 이점이 커진다고 생각한다.
다음과 같은 경우를 생각해보자.
@api_view(['GET', 'POST'])
def hello_world(request):
if request.method == 'POST':
return Response({"message": "Got some data!", "data": request.data})
return Response({"message": "Hello, world!"})
DRF API Guide - @apiview에서 발췌
함수형 뷰의 경우 위처럼 request.method
를 이용해 한 함수 내에서 분기하게 되는 경우가 잦다. 둘은 사실 완전히 다른 일을 하게 되고 수정하게 되면 어느 한 쪽만 수정하게 되는 경우가 잦다. 이 함수가 좀 더 발전한다면, 아마 다음과 같은 모습을 띄게 될 것이다.
@api_view(['GET', 'POST'])
def hello_world(request):
if request.method == 'POST':
if name not in request.data:
raise ValidationError('Name is required')
return Response({
"message": "Got some data!",
"data": request.data
}, status=201)
name = request.GET.get('name', 'world')
return Response({"message": f"Hello, {name}!"})
... 시간이 지날수록 이 함수를 유지보수하기는 힘들어진다. 각기 다른 일(조회와 생성)을 한 곳에서 if
, else
를 나누어서 하게 되기 때문이다. 시간이 지나고 엔드포인트가 아주 많아지면 단순히 url을 찾는 것조차 힘들어지기 시작한다.
이제 조금 다른 예시를 생각해보자. Database 조회 기능이 있는 ListUsers
와 GetUser
기능이 있는 엔드포인트를 상상해보자. 어떤 모습이어야하나? FBV로 구현하면 대략 다음과 같겠다.
def validate_admin(secret_key):
return secret_key == 'admin'
@api_view(["GET"])
def article_list_create_api_view(request):
is_admin = validate_admin(request.headers.get("X-Api-Key"))
if is_admin:
articles = Article.objects.all()
else:
articles = Article.objects.filter(is_public=True)
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
@api_view(["GET"])
def article_detail_api_view(request, pk):
try:
article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
return Response({"error": {
"code": 404,
"message": "Article not found"
}}, status=status.HTTP_404_NOT_FOUND)
if article.is_public and not validate_admin(request.headers.get("X-Api-Key")):
raise Article.DoesNotExist
어떤가, 합리적인가? 우선 다음과 같은 부분이 반복되는 걸 볼 수 있다.
is_admin = validate_admin(request.headers.get("X-Api-Key"))
if is_admin:
articles = Article.objects.all()
else:
articles = Article.objects.filter(is_public=True)
하단도 사실 같은 부분이라는 걸 알 수 있다. 하단을 다음과 같이 바꾸면 코드 중복이 여실히 드러난다.
@api_view(["GET"])
def article_detail_api_view(request, pk):
try:
is_admin = validate_admin(request.headers.get("X-Api-Key"))
if is_admin:
articles = Article.objects.all()
else:
articles = Article.objects.filter(is_public=True)
article = articles.get(pk=pk)
except Article.DoesNotExist:
return Response({"error": {
"code": 404,
"message": "Article not found"
}}, status=status.HTTP_404_NOT_FOUND)
serializer = ArticleSerializer(article)
return Response(serializer.data)
물론 둘이 완벽하게 동일하지는 않다. 하단의 경우 단 건 조회시 쿼리가 달라지기 때문이다. (is_public=True
조건이 추가된다) 하지만 지금은 코드 이야기를 하는 시간이니 지나가보자. 위 중복은 CBV를 쓴다면 다음과 같이 정리할 수 있다.
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin
class ArticleView(ListModelMixin, RetrieveModelMixin, GenericAPIView):
serializer_class = ArticleSerializer
def get_queryset(self):
if self._is_admin:
return Article.objects.all()
else:
return Article.objects.filter(is_public=True)
# protected
@property
def _is_admin(self):
return validate_admin(self.request.query_params.get('X-Api-Key', ''))
훨씬 간단해졌을 뿐더러 이제 내 코드 내에서 http 계층과 관련된 부분을 볼 수 없다. 만약 HTTP 관련하여 세밀하게 조정해야한다면 그 부분에 대한 함수(가령, list
)만 오버라이드하여 조절하면 된다. 내 비즈니스 로직(쿼리 조건)과 도메인과 관계 없는 바깥 부분 (HTTP 응답)을 격리한 것이다.
또한 _is_admin
처럼 구현의 아주 아랫부분을 숨겨 코드를 읽을 때 훨씬 수월하다. get_queryset
처럼 자주 읽고 만지게 되는 함수 내의 추상화 수준이 비슷하기 때문이다. 추상화의 수준이 왜 비슷한 것끼리 뭉쳐있어야 하는지에 대해서는 SLASH 21 - 실무에서 바로 쓰는 Frontend Clean Code을 참고하자.