장고의 뷰 함수를 작성하다 보면 공통적으로 사용되어야 하는 기능들이 있다. 한 예시로, 특정 기능들은 보안이나 권한에 대한 검증이 항상 필요할 수 있다.
게시판을 만들었다고 생각해보자. 게시판의 글을 보는것은 로그아웃된 상태에서도 가능하지만 게시판에 글을 쓰거나 추천을 하고, 자신의 글을 수정, 삭제하는 것은 항상 로그인된 상태에서만 가능해야 한다. 다음 코드를 보자.
# post/views.py
def detail_view(request, id):
if request.method == "GET":
post = get_object_or_404(PostModel, id=id)
comments = post.comment.annotate(count_likes=Count("likes")).order_by(
"-count_likes", "-created_at"
)
return render(request, "post/detail.html", {"post": post, "comments": comments})
else:
return HttpResponseNotAllowed(["GET"])
def create_view(request):
if request.method == "GET":
if request.user.is_authenticated:
return render(request, "post/form.html")
else:
return redirect(reverse("accounts:sign-in"))
elif request.method == "POST":
if not request.user.is_authenticated:
return redirect(reverse("accounts:sign-in"))
...
return redirect(reverse("post:home"))
else:
return HttpResponseNotAllowed(["GET", "POST"])
def update_view(request, id):
if request.method == "GET":
if not request.user.is_authenticated:
return redirect(reverse("accounts:sign-in"))
post = get_object_or_404(PostModel, id=id)
if post.author == request.user:
return render(request, "post/form.html", {"post": post})
else:
return redirect(reverse("post:detail", args=[id]))
elif request.method == "POST":
if not request.user.is_authenticated:
return redirect(reverse("accounts:sign-in"))
...
return redirect(reverse("post:detail", args=[id]))
else:
return HttpResponseNotAllowed(["GET", "POST"])
위 코드를 보면 detail_view
함수는 상관이 없지만 create_view
, update_view
는 매번 유저의 인증을 검증하고 로그인된 사용자가 아니라면 sign-in
페이지로 리다이렉트하는 코드를 구현해야 한다.
이러한 방식은 가독성도 떨어지고 코드의 중복이 빈번해서 권장되지 않는다. 그래서 장고에서는 이런 상황을 위한 데코레이터를 제공하는데, @login_required
는 로그인된 사용자만 해당 뷰 함수에 접근 가능한 기능을 추가해 준다. 이것을 적용하여 위 코드를 리팩토링해 보겠다.
# post/views.py
...
from django.contrib.auth.decorators import login_required
def detail_view(request, id):
if request.method == "GET":
post = get_object_or_404(PostModel, id=id)
comments = post.comment.annotate(count_likes=Count("likes")).order_by(
"-count_likes", "-created_at"
)
return render(request, "post/detail.html", {"post": post, "comments": comments})
else:
return HttpResponseNotAllowed(["GET"])
@login_required
def create_view(request):
if request.method == "GET":
return redirect(reverse("accounts:sign-in"))
elif request.method == "POST":
...
return redirect(reverse("post:home"))
else:
return HttpResponseNotAllowed(["GET", "POST"])
@login_required
def update_view(request, id):
if request.method == "GET":
post = get_object_or_404(PostModel, id=id)
if post.author == request.user:
return render(request, "post/form.html", {"post": post})
else:
return redirect(reverse("post:detail", args=[id]))
elif request.method == "POST":
...
return redirect(reverse("post:detail", args=[id]))
else:
return HttpResponseNotAllowed(["GET", "POST"])
이전 코드에 비해 요구사항과 제공하는 기능이 분리되어 훨씬 명확하게 함수의 역할을 파악할 수 있다. 이제 @login_required
데코레이터가 붙어있는 함수는 모두 로그인이 필요한 기능이라는 것을 알리면서 내부 구현은 제공하는 기능에만 집중하면 되는 것이다.
그럼 @login_required
데코레이터는 로그인되지 않은 사용자의 요청이 들어왔을 때 어떻게 처리할까? 우리가 이전에 구현한 것처럼, 특정 URL로 리다이렉트한다. 이 URL은 기본적으로 LOGIN_URL = "/accounts/login/"
로 세팅되어 있다. 따라서 다른 URL로 변경하고 싶다면 settings.py
파일에 명시해 주어야 한다.
# settings.py
# login 대신 signin을 사용할 경우
LOGIN_URL = "accounts:signin"
데코레이터를 사용하면 좀 더 편리하게 관리할 수가 있겠네요~!