회원가입

[리팩토링] 10. (1) board 함수 리팩토링 일반 게시판 View 재구성

Beany 2024-06-01

AS-IS Code

def get_board_set_from_board_group(request, board_group_id):
    ...완료!!!

def home(request):
    ...완료!!!

def board(request, board_url):
    ...이번장!!!

def post_detail(request, board_url, pk):
    ...

def reply_write(request, board_url, pk):
    ...

def rereply_write(request, board_url, pk):
    ...

def reply_delete(request, board_url, pk):
    ...

def rereply_delete(request, board_url, pk):
    ...

def like(request, board_url, pk):
    ...

[ get_board_set_from_board_group 함수 리팩토링 ]
get_board_set_from_board_group 리팩토링 정보 보기

[ home 함수 리팩토링 ]
home 페이지 성능 개선 Board View 코드 리팩토링
home 페이지 좋아요, 댓글 수 Board View 코드 리팩토링

Board 앱의 View 함수들을 살펴보니 총 9개의 코드가 존재합니다.
하나하나씩 불필요한 코드를 제거하거나 리팩토링해 보겠습니다


 

일반 게시판에 대한 로직을 새로운 endpoint 로 재구성

엔드포인트를 재구성하기 위해 일반 게시판, 검색 게시판, 태그 게시판에서 공용으로 사용되는 함수를 Service Layer로 분리하여 각각의 새로운 View에서 해당 로직을 사용할 예정입니다.

이렇게 하면, Service Layer 용의 코드를 작성하여 각각의 엔드포인트에 대응하는 View에서 로직을 공통으로 사용하여 해결할 수 있기 때문입니다.

    if search:
        tag_id_list = Tag.objects.filter(
            tag_name__icontains=search
        ).values_list(
            'id', flat=True
        )

        q = q & Q(title__icontains=search) | Q(body__icontains=search) | Q(tag_set__in=tag_id_list)

    # 게시판 선택
    if page == 1:
        board_obj = get_object_or_404(Board, url=board_url)
        posts = board_obj.post_set.filter(q).order_by(
            '-created_at'
        )
    # 태그 검색
    elif page == 2:
        tag_board = get_object_or_404(Tag, tag_name=tag_option)
        posts = tag_board.post_set.filter(q).order_by(
            '-created_at'
        )
    # 전체 검색
    elif page == 3:
        posts = Post.objects.active().filter(q).order_by(
            '-created_at'
        )

코드를 보면 search 하는 검색 Query Param 도 공통으로 사용하고 있으며 다른 게 없습니다.

 

코드로 작성하면 이렇게 Service Layer 로 코드를 분리할 수 있겠네요.

def get_active_filtered_posts(search: str = None,
                              board_urls: List[str] = None,
                              tag_names: List[str] = None) -> QuerySet[Post]:
    q = Q()
    qs = get_active_posts()
    if search:
        q = q & Q(title__icontains=search) | Q(body__icontains=search)
    if board_urls:
        q = q & Q(board__url__in=board_urls)
    if tag_names:
        tag_id_list = Tag.objects.filter(
            tag_name__in=tag_names
        ).values_list(
            'id',
            flat=True
        )
        q = q & Q(tag_set__in=tag_id_list)
        qs = qs.prefetch_related('tag_set')
    return qs.select_related(
        'board',
    ).filter(
        q
    )

키워드 검색 시 기존에는 태그 검색도 수행했으나, 이제는 태그 검색을 제외하기로 했습니다. 그 이유는 태그 검색이 키워드 검색과 맞지 않다고 판단했기 때문입니다.

 

일반 페이지를 리팩토링 해봅시다.

def get_board_posts(request, board_url):
    board_detail = get_object_or_404(Board, url=board_url)

    board_posts_request = BoardPostsRequest.of(request)

    paging_data = web_paging(
        get_active_filtered_posts(
            search=board_posts_request.search,
            board_urls=[board_url],
        ).select_related(
            'author'
        ).order_by(
            '-id'
        ),
        int(request.GET.get('page', 1)),
        10,
        5,
    )
    page_posts = paging_data['page_posts']
    has_previous = page_posts.has_previous()
    has_next = page_posts.has_next()
    return render(
        request,
        'board/board_detail.html',
        BoardPostsResponse(
            board_detail_info=BoardDetailInfo(
                name=board_detail.name,
                info=board_detail.info,
                url=board_detail.url,
                board_img_url=board_detail.board_img.url if board_detail.board_img else None,
                name_background_color=board_detail.name_background_color,
                name_text_color=board_detail.name_text_color,
                info_background_color=board_detail.info_background_color,
                info_text_color=board_detail.info_text_color,
            ),
            posts=[
                BoardPost(
                    id=post.id,
                    title=post.title,
                    short_body=post.short_body(),
                    board_url=post.board.url,
                    author_nickname=post.author.nickname,
                    created_at=post.created_at.strftime('%Y-%m-%d'),
                    like_count=post.like_count,
                    reply_count=post.reply_count,
                    image_url=post.post_img.url if post.post_img else static('logo.ico'),
                ) for post in page_posts
            ],
            has_previous=has_previous,
            has_next=has_next,
            previous_page_number=page_posts.previous_page_number() if has_previous else None,
            current_page_number=page_posts.number,
            next_page_number=page_posts.next_page_number() if has_next else None,
            last_page_number=page_posts.paginator.num_pages,
            page_range=paging_data['page_range'],
        ).model_dump()
    )

위 코드처럼, HTML에 있는 모든 로직을 View에서 관리할 수 있도록 데이터를 가져왔습니다.
(Request, Response DTO 정보는 코드로 작성하는 것을 생략)
 

[Request, Response DTO Github 코드]

 

board_detail.html 이라는 이름을 가진 HTML 파일도 따로 생성해 주었습니다.

기존 HTML 파일은 모든 게시판의 케이스에 대한 HTML 로직이 들어있어서 복잡했습니다.

그러나 이제 따로 관리하게 되어 더 간단하게 관리할 수 있게 되었습니다.

{% extends 'base.html' %}
{% load static %}

{% block style %}
<style>
.board-name {
    display: inline-block;
    {% if board_detail_info.name_background_color %}background: {{ board_detail_info.name_background_color }};{% endif %}
    padding: 10px;
    border-radius: 10px;
    font-weight: bold;
    {% if board_detail_info.name_text_color %}color: {{ board_detail_info.name_text_color }};{% endif %}
}
.board-info {
    {% if board_detail_info.info_background_color %}background: {{ board_detail_info.info_background_color }};{% endif %}
    padding: 10px;
    border-radius: 10px;
    {% if board_detail_info.info_text_color %}color: {{ board_detail_info.info_text_color }};{% endif %}
}
.card_img{
    background-repeat:no-repeat;
    background-position-y:center;
    background-size:contain;
}
.truncate-3 {
    font-size: 15px;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
}
</style>
{% endblock %}

{% block title %}{{ board_detail_info.name }} 게시판{% endblock %}
{% block og_title %}{{ board_detail_info.name }} 게시판{% endblock %}
{% block description %}{{ board_detail_info.info|striptags }}{% endblock %}
{% block current_url %}https://cwbeany.com{{ request.get_full_path }}{% endblock %}
{% block img %}{% if board_detail_info.board_img_url %}https://cwbeany.com{{ board_detail_info.board_img_url }}{% else %}https://cwbeany.com/static/logo.ico{% endif %}{% endblock %}

{% block top %}
<!-- 게시판 소개 및 이미지 -->
{% if board_detail_info.board_img_url %}
<div class="p-4 p-md-5 mb-3 rounded" style="background-image:url({{ board_detail_info.board_img_url }});background-size:cover;background-position:center center;">
{% else %}
<div class="p-4 p-md-5 mb-3 rounded">
{% endif %}
    <div class="col-md-8 px-0">
        <h3 class="display-4 board-name">{{ board_detail_info.name }}</h3>
        <p class="lead my-3 board-info">{{ board_detail_info.info }}</p>
    </div>
</div>
<!-- 글 목록 -->
<div class="row g-2 mb-4">
    {% for post in posts %}
    <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
        <a class="text-decoration-none text-dark" href="{% url 'board:post' post.board_url post.id %}">
            <div class="card">
                <div class="row g-0">
                    <div class="col-lg-8">
                        <div class="card-body">
                            <h5 class="card-title" style="white-space:nowrap;overflow: hidden;text-overflow:ellipsis;">{{post.title}}</h5>
                            <p class="card-text truncate-3">{{post.short_body|striptags}}</p>
                            <div class="card-text"><small class="text-muted"><i class="bi bi-person-circle"></i> {{post.author_nickname}}</small></div>
                            <div class="card-text"><small class="text-muted"><i class="bi bi-calendar-week"></i> {{post.created_at}}</small></div>
                            <div>
                                <i class="bi bi-heart text-danger"></i></i> {{post.like_count}} <i class="bi bi-chat-right-text"></i> {{post.reply_count}}
                            </div>
                        </div>
                    </div>
                    <div class="col-lg-4 card_img d-none d-lg-block rounded" style="background-image:url({{post.image_url}});">
                    </div>
                </div>
            </div>
        </a>
    </div>
    {% endfor %}
</div>
<!-- 검색 하기 -->
<form class="d-flex mb-3" style="justify-content: flex-end;" method="get" action="">
    <input class="me-2" type="text" value="{{ request.GET.search }}" name="search" placeholder="검색어를 입력하세요." />
    <button class="btn btn-sm btn-primary" type="submit">검색</button>
</form>

<!-- 페이지 네이션 -->
<nav aria-label="Page navigation example">
    <ul class="pagination justify-content-center">
        {% if has_previous %}
        <li class="page-item">
            {% if request.GET.search %}
                <a class="page-link" href="?page=1&search={{ request.GET.search }}" aria-label="First">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            {% else %}
                <a class="page-link" href="?page=1" aria-label="First">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            {% endif %}
        </li>
        <li class="page-item">
            {% if request.GET.search %}
                <a class="page-link" href="?page={{ previous_page_number }}&search={{ request.GET.search }}" aria-label="Previous">
                    <span aria-hidden="true">&lt;</span>
                </a>
            {% else %}
                <a class="page-link" href="?page={{ previous_page_number }}" aria-label="Previous">
                    <span aria-hidden="true">&lt;</span>
                </a>
            {% endif %}
        </li>
        {%endif%}

        {% for i in page_range %}
            {% if i == current_page_number %}
                {% if request.GET.search %}
                    <li class="page-item active"><a class="page-link" href="?page={{i}}&search={{ request.GET.search }}">{{i}}</a></li>
                {% else %}
                    <li class="page-item active"><a class="page-link" href="?page={{i}}">{{i}}</a></li>
                {% endif %}
            {% else %}
                {% if "search=" in request.get_full_path %}
                    <li class="page-item"><a class="page-link" href="?page={{i}}&search={{ request.GET.search }}">{{i}}</a></li>
                {% else %}
                    <li class="page-item"><a class="page-link" href="?page={{i}}">{{i}}</a></li>
                {% endif %}
            {% endif %}
        {% endfor %}

        {% if has_next %}
        <li class="page-item">
        {% if request.GET.search %}
            <a class="page-link" href="?page={{ next_page_number }}&search={{ request.GET.search }}" aria-label="Next">
                <span aria-hidden="true">&gt;</span>
            </a>
        {% else %}
            <a class="page-link" href="?page={{ next_page_number }}" aria-label="Next">
                <span aria-hidden="true">&gt;</span>
            </a>
        {% endif %}
        </li>

        <li class="page-item">
        {% if request.GET.search %}
            <a class="page-link" href="?page={{ last_page_number }}&search={{ request.GET.search }}" aria-label="Last">
                <span aria-hidden="true">&raquo;</span>
            </a>
        {% else %}
            <a class="page-link" href="?page={{ last_page_number }}" aria-label="Last">
                <span aria-hidden="true">&raquo;</span>
            </a>
        {% endif %}
        </li>
        {%endif%}
    </ul>
</nav>
{% endblock %}

 

cwbeany/board/urls.py 에 새로운 Endpoint 를 생성합니다.

from django.urls import path
...


urlpatterns = [
    path('', home, name='home'),
    path('board/<str:board_url>', get_board_posts, name='get_board_posts'),  # <-- 새로운 ENDPOINT
    path('<str:board_url>', board, name='board'),  # 더 이상 쓰지 않을 ENDPOINT
    path('<str:board_url>/<int:pk>', post_detail, name='post'),
    path('<str:board_url>/<int:pk>/reply', reply_write, name='reply'),
    path('<str:board_url>/<int:pk>/rereply', rereply_write, name='rereply'),
    path('<str:board_url>/<int:pk>/reply_del', reply_delete, name='reply_delete'),
    path('<str:board_url>/<int:pk>/rereply_del', rereply_delete, name='rereply_delete'),
    path('<str:board_url>/<int:pk>/like', like, name='like'),
    path('board-group/<int:board_group_id>/constant', get_boards_info_from_board_group, name='get_boards_info_from_board_group'),
]

 

 

[ 개선 전 ]

SQL Query 횟수: 18 쿼리

현재 쿼리 문제는 Join을 하지 않고 조회하고 있기 때문입니다.

 

[ 개선 후 ]

SQL Query 횟수: 8 쿼리

Join 으로 해결

0 0
블로그 일기
제 블로그의 고도화 과정을 설명합니다. 이는 코드 리팩토링과 추가된 기능들에 대해 기록하기 위한 게시판입니다. 어떤 기능이 추가되었는지, 무엇이 개선되었는지 등 고도화되는 과정을 자세히 다룰 예정입니다.
Yesterday: 456
Today: 126