프리 온보딩 프로젝트 리팩토링 [aimmo]

유동헌·2021년 12월 22일
0

프리 온보딩의 첫 번째 기업 과제였던 aimmo 프로젝트를 리팩토링 해보았습니다. 첫 번째 과제였던만큼 그 당시엔 요구 받은 기능 구현을 완벽하게 하지 못하고 제출해서 아쉬움이 컸는데요, 그때 함께 했던 팀원 분과 가볍게 배포까지 하여 마무리하였습니다.

리팩토링 방향

우선 가장 중요한 것은 기업에서 요구한 기능 전체에 대한 구현입니다. 당시엔

  • 댓글에 대댓글을 달 수 있는 기능
  • 검색 기능 (완벽하게 구현하지 못하고 제출하였다)
  • query 최적화
  • 댓글, 대댓글 리스트 검색
  • 테스트 코드 작성

등을 구현하지 못하고 제출하였고, 따라서 이번 리팩토링의 목표는 위 기능을 마무리하는 것입니다.

리팩토링하며 알게된 점

Swagger + drf-yasg

custom query parameter

첫 번째 프로젝트였던 만큼 swagger + drf+yasg 사용이 미숙하였습니다. 이 내용은 블로그로 한 번 정리를 해 두었는데요. aimmo 프로젝트 중 swagger + drf+yasg 에 대해 공부한 부분은 query parameter 를 커스텀하여 관리하는 부분이었습니다.

query_parent_comment_id = openapi.Parameter(
        "parent_comment_id",
        openapi.IN_QUERY,
        description = "parent_comment_id",
        type = openapi.TYPE_INTEGER
    )
    query_limit = openapi.Parameter(
        "limit",
        openapi.IN_QUERY,
        description = "limit",
        type = openapi.TYPE_INTEGER
    )
    query_offset = openapi.Parameter(
        "offset",
        openapi.IN_QUERY,
        description = "offset",
        type = openapi.TYPE_INTEGER
    )
    
@swagger_auto_schema(manual_parameters = [query_parent_comment_id, query_limit, query_offset])

위의 코드에 이에 해당하는 것인데, manual_parameters 라는 인자를 통해 구현합니다. API 통신을 위해 필요한 query parameter 들을 리스트에 담에 관리합니다. 이 부분도 아쉬운 부분 중에 하나인데, 원래는 django에서 제공하는 Paginator 라이브러리를 사용하려고 했었으나 그렇게 하지 못하고 수동으로 pagination을 구현하였습니다. 지난번에 쓴 블로그에 자세히 설명이 되어 있고, 가장 중요한 건 openapi.IN_QUERY 이라는 부분을 통해 query 로 보낼지 header 에 보낼지 옵션으로 설정해 줄 수 있다는 사실입니다.

error message 관리

parameter_token = openapi.Parameter(
        "Authorization",
        openapi.IN_HEADER,
        description = "access_token",
        type = openapi.TYPE_STRING
    )
    
    query_comment_id = openapi.Parameter(
        "comment_id",
        openapi.IN_QUERY,
        description = "댓글 ID를 넣어주세요",
        type = openapi.TYPE_INTEGER
    )
    
    error_field = openapi.Schema(
        'error', # 제목 
        description = '입력 부분을 수정해주세요', # 설명
        type=openapi.TYPE_STRING # 타입
    )
    
@swagger_auto_schema(request_body = CommentSerializer, 
                        manual_parameters = [parameter_token, query_comment_id],
                        responses = {
                            400 : error_field # responses 
                            }
                        )

이 부분의 코드는 아직 미완성입니다. 하지만 기록해두고 싶어서 적어 봅니다. swagger는 매우 강력한 문서화 도구이지만 아쉬운 점이 많다고 합니다. (지난 번 블로그에 관련 내용이 있습니다) 가장 불편한 점은 response schema 를 제공하지 않는 점입니다. 이를 해결하기 위해 drf-yasg 를 사용하는 것 같습니다. 미완성이라고 했던 부분은 위 코드의 error_field 인데요, drf-yasg를 사용한다면 이렇게 error type 관리를 문서화 할 수 있도록 제공해줍니다.

query 최적화

def get(self, request, posting_id):
        parent_comment_id = request.GET.get("parent_comment_id",0)
        offset            = int(request.GET.get("offset", 0))
        limit             = int(request.GET.get("limit", 10))
        
        if parent_comment_id == 0:
            all_comments = Comment.objects.filter(posting_id=posting_id, parent_comment_id=0).select_related("user")
            
        else:
            all_comments = Comment.objects.filter(posting_id=posting_id, parent_comment_id=parent_comment_id).select_related("user")
        
        comments = all_comments[offset:offset+limit]
        
        comment_list = [
            {
                "content"           : comment.content,
                "user"              : comment.user.email,
                "posting_title"     : comment.posting.title,
                "parent_comment_id" : comment.parent_comment_id
                } for comment in comments
            ]
        return JsonResponse({"message" : comment_list}, status=200)

엄청나게 간단한 코드지만 django-extentions 를 사용해 ORM이 SQL문으로 바뀌는 모습을 확인하고 그 후 수정한 코드입니다. 수정한 부분은 select_related 부분인데요, 다른 필드에서 가져오는 값이 포함되어 있을 경우 select_related method 를 이용해 미리 캐싱해 주면 ORM이 데이터베이스에 접근하는 횟수를 획기적으로 줄일 수 있었습니다. 가령 filter method를 통해 5개가 나왔다면 select_related method 가 없는 경우에는 매번 user 필드에 따로 접근을 했다면 select_related method 를 사용한 코드에서는 단 한번의 요청만 있었습니다.

postings = Posting.objects.filter(Q(author__name=keyword)|\
                    Q(title__icontains=keyword)|\
                        Q(text__icontains=keyword))\
                    .select_related("category", "author")

이 부분은 팀원분께서 작성하신 코드인데 이 부분에서도 사용이 되었네요. icontains 도 알게 되었습니다. select_related 의 인자는 필드 네임을 써주고 컴마로 구분하여 복수로 들어올 수 있습니다.

알아둘 것

배포 시 while noise 필요

지난 번 배포 시에 잘 되지 않았었던 부분을 해결한 경험부터 적어보려 합니다. 아직 정확하게 이해를 한 것은 아니지만 배포가 되면 static 파일을 직접 관리해줘야 한다고 합니다. 개발시점에서는 DEBUG = TRUE인 경우에 django가 static 파일을 자동으로 관리를 해 주는데, 배포가 되면 따로 관리를 해줘야 하나 보네요. 이 부분에 대해서는 아직 이해가 필요합니다. 해결은 django whitenoise 를 설치하여 배포를 하였습니다.

pip install whitenoise

settings.py
STATIC_URL = '/staticfiles/'
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

이번 프로젝트는 이렇게 해결을 하였는데 미들웨어에 추가하는 방법도 있는 것 같습니다.

settings.py 파일에서
 
미들웨어 부분 :
    'whitenoise.middleware.WhiteNoiseMiddleware',
 
static_root 부분 :
    STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'

docker 배포

리팩토링을 하면서 배포하는 방법을 docker로 바꾸어 보았습니다. 일단 기존 프로젝트도 docker로 배포를 해 본 경험이 있었기에 기본적인 문법은 숙지가 된 상태이나 오랜만에 해보는 것이어서 순서 정도만 정리를 해두려고 합니다. 사실 배포라고 하기에는 이미지만 만들어서..서버만 띄워보았다고 생각합니다. docker의 경우 구글링해보면 처음부터 따라하는 컨텐츠나 개념 정리부터 하는 글이 많은데요, 제대로 알고 쓰기까지는 시간이 걸릴 것 같습니다.

로컬 환경

  1. Dockerfile 작성
  2. docker 이미지 빌드
  3. docker 이미지 push (docker hub에서 체크 가능)

EC2 환경

  1. EC2 환경 전역에서 우분투에 docker를 설치
  2. docker 이미지 확인 컨테이너는 아직 실행이 안된 상태
  3. docker 이미지 pull, run

이번에 저는 이런 순서로 진행을 하였습니다. 더 공부가 필요합니다!

Mongo DB 문제 해결

MongoError: user is not allowed to do action [find] : 기타치는 개발자의 야매 가이드 블로그를 참고하여 해결하였습니다.

그 밖에..

  • Django ORM 중 __ 사용하는 방법
  • try - except 문은 조금 무거울 수 있으니 if 로직으로 처리하기

등.. 신경써야 할 것이 많다는 걸 다시 한 번 느꼈습니다.

정리

음.. 쓰고 나서 보니 프로젝트 리팩토링이라기 보다는 구현 못했던 기능을 구현했다라고 표현하는게 더 올바른 것 같습니다..

Reference

https://phpdoumi.tistory.com/204 [IT로 뭘할까?] : whitenoise 참고

https://wangkisa.tistory.com/48 [wangkisa의 기술블로그] : whitenoise 참고

profile
지뢰찾기 개발자

0개의 댓글