[Django] 장고의 설계 철학
- -
장고의 설계 철학
마감일을 가진 완벽주의자를 위한 웹 프레임워크
장고(Django)는 "The Web framework for perfectionists with deadlines(마감일을 가진 완벽주의자를 위한 웹 프레임워크)"라는 슬로건을 가지고 있으며, 아래의 설계 철학을 중요시하고 있다.
설계 철학 | 의미 |
DRY(Don't Repeat Yourself) | 코드의 중복을 피하고 재사용성을 높이는 것이 목표이다. 장고는 중복 코드를 최소화하고 모델, 뷰, 템플릿 등의 각 부분 간에 논리적 분리를 제공한다. |
Convention over Configuration(CoC) | 장고는 개발자들이 설정할 부분을 최소화하고 대신 일반적인 설정과 구조를 기본으로 제공한다. 이는 개발자가 더 적은 결정을 내려야 하며, 더 빠르게 개발할 수 있도록 돕는다. |
Explicit is better than implicit (명시적인 것이 암시적인 것보다 좋다.) |
코드는 명확하고 명시적이어야 한다. 코드가 명확하게 작성되면 다른 개발자들이 코드를 더 쉽게 이해하고 유지보수할 수 있다. |
Loose Coupling(느슨한 결합) | 각각의 컴포넌트가 독립적으로 동작하고 다른 부분과 강력하게 결합되지 않아야 한다. 이는 유연성을 높이고 시스템을 확장하거나 수정하기 쉽게 만든다. |
Less Code(더 적은 양의 코드) | 장고는 간결하고 간단한 문법을 사용하여 많은 기능을 제공한다. 더 적은 코드로 더 많은 일을 처리할 수 있도록 하는 것이 목표이다. |
Fast Development(빠른 개발) | 장고는 빠른 개발을 지향하며, 프로토타입부터 실제 제품 출시까지 빠르게 개발할 수 있도록 돕는 것이 목표이다. |
신속한 개발
21세기 웹 프레임워크의 핵심은 지루한 부분을 빠르게 만드는 것이다. 이에 맞게 장고는 쾌속 웹 개발을 가능하게 한다.
- 대개의 경우 프로젝트 개발에서 시간이 가장 큰 비용
- 중복을 줄여, 높은 생산성으로 서비스 개발에 집중할 수 있도록 도와준다.
- 규모가 큰 회사는 시간보다 더 중요하게 고려해야 사항들이 있을 수 있다.
- 규모가 큰 회사는 개발자를 수백 명 수백 명 이상 뽑을 수도 있고, 수백 명 이상의 개발자가 하나의 서비스에서 각자의 영역만을 담당하며 개발할 수 있고, 개발기간을 넉넉하게 가져갈 수도 있고, 0.01%의 성능 향상을 위해 더 큰 투자를 할 수도 있다.
느슨한 결합
장고 스택의 근본적인 목표는 "느슨한 결합, 탄탄한 응집"이다. 프레임워크의 각 계층을 필요하기 전에는 서로 결합이 없어야 한다. 또한, 장고 Best Practice제안 구현도 있지만, 이는 제안일 뿐 강제되지는 않는다. 따라서 임의의 아키텍처로도 개발이 가능하다.
URLs 요청을 처리할 함수 매핑 |
Templates HTML 등의 복잡한 문자열 조합 |
File Storages API 추상화된 파일 시스템 API로 다양한 파일시스템을 지원(로컬/AWS S3 등) |
Geo Spatial 지리/공간 |
Email 이메일 발송 |
Views 요청을 처리하는 함수 |
Forms HTML Form 태그 생성 및 입력값에 대한 유효성 검사 |
Caching 계산결과를 저장/활용하여 응답속도 향상(메모리, Redis, 파일, DB 등) |
I18n/l10n 국제화/지역화 : 유저의 언어설정에 맞게 언어번역 및 지역화를 지원 |
Sitemaps SiteMap |
Models 데이터베이스와의 인터페이스를 담당 |
Validators 유효성 검사 로직을 순수 함수로 구현 |
Sessions 서버에 임의 유형의 데이터를 저장 (로그인 유저, 장바구니 등) |
Logging 로깅 |
Syndication feeds (RSS/Atom) |
적은 코드
반복을 줄이고, 가능한 한 최소한의 코드를 사용하며, 다른 언어/프레임워크의 틀에 박힌 코드를 배제한다.
- Introspection(객체의 메타데이터를 조사하는 과정)과 같은 Python의 동적인 기능을 최대한 활용한다.
- 파이썬을 잘 아는 만큼, 장고를 더 잘 활용할 수 있다.
Post REST API를 개발한다면 필요한 5개의 Endpoint
- GET - /api/v1/posts/ : 목록 조회 요청
- POST - /api/v1/posts/ : 단건 생성 요청
- GET - /api/v1/posts/pk/ : 단건 조회 요청
- PUT/PATCH - /api/v1/posts/pk/ : 단건 수정 요청
- DELETE - /api/v1/posts/pk/ : 단건 삭제 요청
# django-rest-framework를 활용한 API 구현
# URL Patterns : 요청 URL에 맞춰, Views와 매핑
router = DefaultRouter()
router.register("posts", PostViewSet)
urlpatterns = [
path("api/v1/", include(router.urls)),
]
django-rest-framework(이하 DRF) 라이브러리는 장고의 철학을 충실히 따르는 웹 API 개발을 위한 라이브러리이다.
# Views : 요청을 직접적으로 처리(컨트롤러 레이어와 유사)
class PostViewSet(ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
DRF의 ModelViewSet을 활용하면, 쿼리 셋과 시리얼라이저 클래스, 2개의 구현 만으로 앞선 5개의 Endpoint에 대한 요청을 처리할 수 있는 Web API가 만들어진다.
# Serializers/Forms
# 데이터 유효성 검사 및 응답 데이터 생성
# (서비스 레이어와 유사)
class PostSerializer(ModelSerializer):
class Meta:
model = Post
fields = "__all__"
시리얼라이저는 어떠한 데이터를 어떠한 구조로 응답을 줄 것인지를 결정한다. 이는 아래에서 나중에 보게 될 스프링의 PostListResponseDto와 유사한 역할이라고 볼 수 있다.
PostSerilizer에 지정한 Meta.fields = "__all__" 속성은 model로 지정한 Post 모델의 모든 필드 내역대로 응답을 생성하겠다는 의미이다.
# Models : 도메인 모델 및 ORM
# 저장소 레이어와 유사
class Post(models.Model):
title = models.CharField(max_length = 100)
content = models.TextField()
장고 모델은 디폴트로 id 필드가 정의되어 있다. 그래서 앞선 PostSerializer의 fields = "__all__" 정의를 통해 id, title, content 필드로 응답이 생성된다. 또한, Post에 대한 생성/수정 요청에 대해서도 PostSerializer가 개입하여, 지정한 필드 내역대로 유효성 검사 및 DB로의 저장을 지원해 준다.
반복하지 않기
중복은 줄이고, 정규화를 지향한다.
- 고유한 개념 및 데이터는 단 한 번, 단 한 곳에 존재하는 것으로 충분하다.
- 그러한 이유로, 장고는 절제된 구현으로 완벽히 동작하는 기능을 만들 수 있다.
- ex) 클래스 기반 뷰(Generic Views/APIViews), ModelForm, ModelSerializer 등
# ORM 모델 및 도메인
class Post(models.Model):
title = models.CharField(
max_length = 100,
verbose_name = "제목",
validators = [
RegexValidator(r"[ㄱ-힣]", message = "한글이 최소 1글자 포함되어야 합니다."),
],
)
content = models.TextField(verbose_name = "내용")
Post 모델을 정의하였다. 이를 "도메인"으로 부르고 Post 모델은 하나의 포스팅에는 제목과 내용을 저장한다. Post 모델에서 유효성 검사를 실시하고 있다.
# 요청내역에 대한 유효성 검사 및 모델을 활용한 데이터 저장의 책임
class PostForm(forms.Form):
title = form.CharField(
label = "제목",
validators = [
MaxLengthValidator(100),
RegexValidator(r"[ㄱ-힣]", message = "한글이 최소 1글자 포함되어야 합니다."),
],
)
content = forms.CharField(label = "내용", widget = forms.TextArea)
def save(self, commit = True):
title = self.cleaned_data.get("title")
content = self.cleaned_data.get("content")
post = Post(title = title, content = content)
if commit:
post.save()
return post
장고 forms.Form 타입의 PostForm 클래스를 정의하였다. 유저로부터 새로운 포스팅을 입력받으려 하거나 기존 포스팅에 대한 수정 요청을 받으려 할 때 유저에게 HTML 입력폼을 보여줘야 한다. 그런 HTML 입력폼을 유저에게 보여주고 유저가 제목과 내용을 입력하고 "작성완료" 버튼을 클릭하면, 서버로 그 요청이 전달되고, 그럼 서버에서 유저가 입력한 값에 대한 유효성 검사를 해야 한다.
서버에서의 유효성 검사는 꼭 필요하다. 유저가 입력한 값이 서버에서 원하는 형태를 가지고 있는지 검사를 하는 것이다. 서버에서 원하는 길이나 포맷에 맞지 않는 값을 요청하였을 때, 이 요청을 거부할 수 있어야 한다. 이러한 기능들을 하는 것이 장고의 Form이라고 할 수 있다.
Post 모델에서 제목은 최대 100자까지 허용하며, 레이블은 "제목", 한글이 최소 1글자가 포함되도록 룰을 정의하고 있다. models.Model과 models.Form의 역할이 서로 다르기에 PostForm에도 Post 모델처럼 룰 정의가 똑같이 필요하다. 룰 정의가 된 코드를 보면, 모델 코드와 거의 유사하다는 걸 확인할 수 있다. 중복이라고 느낄 만큼 코드가 비슷하다.
하지만 해당 구현이 엄밀히 말해 중복이라고는 할 수 없다, 서로 다른 영역이기 때문이다. Model은 DB와 관련되며, Form은 HTML Form과 관련이 있다.
서로 영역이 다르지만, 이런 코드도 줄일 수 있는 기능을 장고에서 제공해주고 있다.
# 요청내역에 대한 유효성 검사 및 모델을 활용한 데이터 저장의 책임
# ModelForm은 중복을 줄여주는 하나의 선택지일뿐이다.
# 그리고 Form은 레거시가 아니다. ModelForm과 함께 상황에 따라 적절히 활용하자.
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title","content"]
# ModelForm은 Meta 클래스에 지정된 옵션으로 폼 필드를 자동으로 추가해준다.
# 그 이후는 Form과 동일하다.
Meta 클래스는 클래스의 인스턴스를 만들려는 목적은 아니고, 단순히 PostForm 클래스에 대한 속성을 지정할 목적으로서 클래스 문법을 사용한 것뿐이다. 필자도 그랬지만, 클래스 안의 클래스 문법이 낯설 수도 있겠지만, PostForm 클래스에 대한 메타속성을 정의했다고 생각하면 될 거 같다.
따라서 위의 코드는 모델폼이 Post 모델의 title과 content 필드 내역을 읽어와서 , 폼 필드를 자동으로 추가해 준다. 위에서 봤던 전의 PostForm과 지금의 PostForm은 동일한 동작을 한다고 볼 수 있다.
결론은, 모델폼은 중복코드를 줄여주는 하나의 선택지일 뿐이지, Form이 아닌 ModelForm을 무조건 써야 한다는 것은 아니다. 상황에 따라 Form과 ModelForm을 사용하면 된다.
def video_list(request):
qs = Video.objects.all()
page_number = request.GET.get("page",1)
paginator = Paginator(qs, 10)
page_obj = paginator.page(page_number)
qs = page_obj.object_list
return render(request, "jack/video_list.html",
{
"page_obj" : page_obj,
"is_paginated" : page_obj.has_other_pages(),
"video_list" : qs,
})
def article_list(request):
qs = Article.objects.all()
page_number = request.GET.get("page",1)
paginator = Paginator(qs, 10)
page_obj = paginator.page(page_number)
qs = page_obj.object_list
return render(request, "app/article_list.html",
{
"page_obj" : page_obj,
"is_paginated" : page_obj.has_other_pages(),
"article_list" : qs,
})
video_list 뷰와 article_list 뷰를 FBV로 구현하였다. video_list 뷰는 비디오 목록 요청에 대한 HTML 응답을 하는 뷰이며, article_list 뷰는 아티클 목록 요청에 대한 HTML 응답을 하는 뷰이다. 위의 코드에서 보면 알 수 있겠지만 로직이 페이징 처리까지 하는 부분까지 전부 똑같다. 이는 반복된 코드라고 볼 수 있다.
# 여러 도메인을 구현하는 과정에서 반복되는 부분은 클래스 기반 뷰(Class Based View)를 통해
# 반복을 줄일 수 있다.
# 클래스 기반 뷰(Class Based View)에서는 매 요청을 처리할 때마다
# ListView 인스턴스가 생성이 되어 처리된다.
# 각 뷰는 모델 클래스만 다를 뿐, 클래스 요청을 처리할 때마다 ListView 인스턴스가 생성이 되어
# 요청을 처리한다.
from django.views.generic import ListView
video_list = ListView.as_view(
model = Video,
paginated_by = 10,
)
article_list = ListView.as_view(
model = Article,
paginated_by = 10,
)
두 개의 중복되었던 코드를 위와 같이 CBV로 만들어 중복을 줄일 수 있다. ListView는 장고에서 기본으로 지원해 주며, 목록 HTML 응답을 위한 기능들이 이미 ListView에 구현되어 있다.
📢 그러면 무조건 클래스 기반 뷰로 구현해야 하는가?
꼭 그렇지만은 않다. 함수 기반 뷰로도 많이 구현한다. 중복이 발생하는 경우 클래스 기반 뷰를 통해 중복을 줄여나갈 수 있다는 것이지 무조건 클래스 기반 뷰로 구현해야 하는 것은 절대 아니다.
장고 vs 스프링
포스팅 목록 조회를 각 프레임워크에서 어떻게 구현하는지 보고 둘이 어떻게 다른지 한 번 살펴보자. 만약, 아직 스프링과 장고에 대해 모르시는 분들은 "아 그렇구나" 정도로만 생각하고 넘어가면 좋을 거 같다.
먼저 스프링에서는 간단하게 어떻게 포스팅 목록 조회를 구현하는지 한 번 살펴보자.
📢 전체적인 구조를 보기 위한 예시일 뿐이니 흐름만 보고 세부적인 부분까지는 이해하지 않아도 된다.
스프링 Controller
// 컨트롤러 : 외부 요청을 받아서, 서비스를 통해 로직을 수행하고, 응답
@RequiredArgsConstructor
@Controller
public class PostController{
private final PostService postService;
@GetMapping("/")
public String index(Model model){
model.addAttribute("postList", postService.findAllDesc());
return "index";
}
}
스프링 컨트롤러이며, 최상위 주소인 "/"로 요청이 들어오면 PostController 클래스의 index 인스턴스 함수가 호출이 되어 요청을 처리한다. 컨트롤러의 index 함수에서는 postService 서비스를 호출하여 요청 처리를 위임한다. postService.findAllDesc()의 반환값을 "postList"라는 이름으로 모델의 새로운 속성으로 추가하고, 추가된 속성은 템플릿 렌더링 시에 사용될 것이다.
스프링 Service
// 서비스 : 실제 비지니스 로직을 처리
@RequiredArgsConstructor
@Service
public class PostService{
private final PostRepository postRepository;
public List<PostListResponseDto> findAllDesc(){
return postRepository
.findAllDesc()
.stream()
.map(PostListResponseDto::new)
.collect(Collectors.toList());
}
}
PostService에서는 다시 postRepository.findAllDesc()의 리턴값을 스트림으로 변환하여 PostListResponseDto 객체 리스트로 변환하고 리턴한다.
스프링 엔티티
// 엔티티
@Getter
@NoArgsConstructor
@Entity
public class Post extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String author;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
}
Post 엔티티는 데이터베이스 테이블과 매핑되고, 테이블 칼럼 내역과 클래스 내역을 일치시킨다.
// 저장소
public interface PostRepository extends JpaRepository<Post, Long>{
@Query("SELECT p FROM Post p ORDER BY p.id DESC")
List<Post> findAllDesc();
}
최종적으로 PostRepository는 데이터베이스를 쿼리하고 응답을 반환한다. findAllDesc를 호출하면 @Query에 저장한 쿼리가 수행되고 리턴값으로 Post 리스트를 리턴한다.
스프링 DTO
// DTO(Data Transfer Object) : 응답 포맷을 정의
@Getter
public class PostListResponseDto{
private Long id;
private String title;
private String author;
public PostListResponseDto(Post entity){
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
}
}
응답은 원래 PostList이지만 PostListResponseDto 형태로 변환하는 과정으로 들어가기 때문에 PostListResponseDto 클래스도 정의해 준다.
템플릿
// index 템플릿
<table>
<thead>
<tr>
<th>ID</th>
<th>제목</th>
<th>작성자</th>
</tr>
</thead>
<tbody>
{{#postList}}
<tr>
<td>{{ id }}</td>
<td>{{ title }}</td>
<td>{{ author }}</td>
</tr>
{{/postList}}
</tbody>
</table>
PostController의 index에서 리턴한 "index" 문자열은 index 이름의 템플릿을 활용하여 HTML 응답을 렌더링 한다.
스프링에서 DB를 통해 HTML응답을 만드는 정말 간단한 예제 코드를 살펴보았다. 그럼 이제 장고에선 같은 기능을 어떻게 구현하는지 한 번 살펴보자.
장고 URL Patterns
# URL Patterns
# 장고는 뷰 정의와 URL 매핑을 따로 한다.
from django.urls import path
from . import views
urlpatterns = [
path("", views.post_list, name = "post_list"),
]
어떤 URL이 들어오면 post_list 함수를 호출하겠다는 URL 정의를 한다.
장고 뷰
📢 뷰를 만드는 방법은 장고에서 FBV(Function Based View)와 CBV(Class Based View)가 있다, 하지만 현재 예제에서는 함수 기반 뷰(FBV)로 살펴보자.
# 뷰(함수로 만든 뷰)
# 앞선 스프링의 컨트롤러의 역할, 서비스의 일부 역할을 하기도 한다.
from django.shortcuts import render
from blog.models import Post, Article
def post_list(request):
qs = Post.objects.all().order_by("-id")
return render(request, "index.html",{
"post_list" : qs,
})
post_list 함수가 호출이 되면 Post 모델을 통해 DB에 쿼리를 하는 쿼리 셋 개체를 생성한다. render 함수는 index.html을 통해 HTML 응답을 생성 시에 "post_list"라는 이름으로 qs 이름의 쿼리 셋 객체를 참조한다.
장고 모델
# 모델
# 앞선 스프링의 <엔티티>와 유사
from django.conf import settings
from django.db import models
class Post(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete = models.CASCADE)
title = models.CharField(max_length = 100)
content = models.TextField()
장고의 모델(Model)은 스프링의 엔티티(Entity)와 유사하며, 그 이상의 기능을 제공한다.
장고 템플릿
{# 템플릿 : 복잡한 문자열 조합의 틀 #}
<table>
<thead>
<tr>
<th>ID</th>
<th>제목</th>
<th>작성자</th>
</tr>
</thead>
<tbody>
{% for post in post_list %}
<tr>
<td>{{ post.id }}</td>
<td>{{ post.title }}</td>
<td>{{ post.author }}</td>
</tr>
{% endfor %}
</tbody>
</table>
템플릿 index.html만 정의하면, 앞에서 본 스프링에서의 구현과 동일한 동작을 수행한다. 코드는 보다 훨씬 간결하다는 걸 알 수 있다.
'Framework > Django' 카테고리의 다른 글
[Django] 장고 URL 설계 (0) | 2023.12.17 |
---|---|
[Django] 장고 뷰(View) (0) | 2023.12.15 |
[Django] 장고에서의 요청 처리 (0) | 2023.12.14 |
[Django] 장고 app 생성 (0) | 2023.11.01 |
[Django] VSCode에서 장고 설치하고 프로젝트 만들기 (0) | 2023.09.29 |
소중한 공감 감사합니다