Django, Django REST framework 본격적으로 사용하기

안녕하세요, 화해팀에서 백엔드 개발을 담당하고 있는 윤정원입니다. Django REST framework

 

화해라는 서비스가 론칭한지도 어느덧 8년이 지났습니다. 론칭 초기에 비해 서비스가 점점 커지고 복잡해짐에 따라 기존의 운영 업무 지원을 위한 도구들의 교체가 필요한 시기가 오게 되었고, 이에 따라 신규 운영 도구 개발에 대한 니즈도 점점 커지고 있습니다.

 

지금까지는 필요에 따라 최소한의 범위로만 어드민을 개발했기 때문에 부가기능이 없거나 어드민 자체가 존재하지 않는 경우도 있었습니다. 적지 않은 시간 동안 운영팀에서는 수동으로 서비스를 관리해야 했고, 필연적으로 운영에 필요한 리소스가 늘어날 수밖에 없었습니다.

 

이를 해소하기 위해 어드민 페이지를 별도 제품으로 취급하고 본격적으로 개발하기 위해 운영 어드민 TF를 조직하게 되었습니다. 운영 어드민 TF에서 프로젝트를 진행하며 Django, DRF의 세부적인 기능을 활용했던 경험을 얘기해볼까 합니다.

 

 


 

들어가며

화해팀의 백엔드 플랫폼에서는 4년 전부터 API 개발을 위한 프레임워크로 Django / Django REST framework를 채택하여 사용해 오고 있습니다. 그 이전엔 프레임워크를 사용하여 개발하지 않았기 때문에 코드 재사용, 테스트 부재 등의 이유로 지속 가능한 개발이 어려운 상황이었습니다.

 

Django 채택 이후 즐거운 마음으로 기존의 불편한 점을 하나씩 해소해나갔고, 적지 않은 시간이 흐르는 동안 Django로 개발된 프로젝트도 다수 생겼습니다. 하지만 기존 API들을 이관하고, 또 새로운 API를 추가하며 각 프로젝트의 규모가 커질수록 점점 불편한 지점들이 생기기 시작했습니다.

 

 

 

파편화

그간 백엔드 플랫폼에서는 다수의 프로젝트를 Django를 사용하여 개발해왔는데, 밴드(화해팀은 크로스 펑셔널 팀을 밴드로 지칭하고 있습니다.)나 도메인(제품, 광고, 커머스로 분리) 별로 담당 프로젝트가 나뉘어 있습니다. 밴드나 도메인별로 각자 개발하다 보니 프레임워크에 대한 이해가 서로 다르고 스타일도 달라서 전체 프로젝트가 통일성 있게 개발되지 못했습니다.

 

같은 기능을 하지만 다른 형태로 작성된 코드도 있고, 프레임워크에서 제공하는 기능을 사용하지 않고 직접 구현한 코드나 패키지 구조의 차이 등의 파편화가 누적됐습니다. 그러다 보니 분명 같은 언어와 프레임워크를 사용하지만 밴드 이동이나 지원 등의 이유로 다른 프로젝트에서 개발하게 되는 경우 개발 속도가 현저히 느려지는 경우가 발생했습니다.

 

이런 문제에 대한 백엔드 플랫폼 차원에서의 공감대는 있었지만, 파편화된 영역을 확인하고 이를 공통화하여 문제를 풀어나가기 위한 계획과 실행은 리소스 부족으로 계속 미뤄지고 있었습니다.

 

 

 

기회

그러던 중 신규 어드민 구축을 위한 TF가 꾸려졌습니다. 아예 처음부터 프로젝트를 만들어서 개발하게 되었는데, 이번 기회에 그동안 겪었던 불편함을 개선하면 좋겠다는 생각이 들었습니다. 다행히 TF의 공감과 지지를 얻어 어느 정도 시간을 확보할 수 있었고, 필요한 패키지들과 프레임워크 기능에 대한 공식 문서를 확인하고 실습해보면서 세부 기능을 하나씩 확인할 수 있었습니다.

 

우선 현재 프로젝트를 잘 정리된 형태로 진행하고, 이를 다른 프로젝트에도 하나씩 적용해나가도록 본격적인 개발을 진행하게 되었습니다.

 

 

 

결과

진행한 작업 중 적은 노력으로 큰 효과를 얻었던 것들을 추려보았습니다.

 

renderer class

최근 팀 차원에서 정해진 컨벤션에 따라 신규로 작성되는 API는 응답 본문에 메타 정보와 원래의 응답 데이터를 함께 전달하고 있습니다. 이러한 응답을 만들기 위한 별도 코드를 작성해서 사용하거나, API마다 직접 형태를 만들어서 응답하던 것을 renderer를 커스터마이즈하여 손쉽게 응답할 수 있도록 했습니다.

 


# renderers.py
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response


class CustomRenderer(JSONRenderer):
    def render(self, data, accepted_media_type=None, renderer_context=None):
        response: Response = renderer_context['response']

        """
        response, data 의 정보를 활용하여 커스텀한 응답 형태를 만들 수 있습니다.  
        """
        formed_data = {
            'meta': {},
            'data': {},
        }

        renderer_context['response'].data = formed_data
        return super().render(formed_data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)

 

django settings 내 REST_FRAMEWORKDEFAULT_RENDERER_CLASSES를 설정하여 프로젝트 전체 단위에서 적용할 수도 있고, 기존에 작성되어 있던 API에 영향이 없도록 일부만 적용해야 한다면 viewset이나 view 단위에서 renderer_classes를 직접 설정해서 사용 할 수도 있습니다.

 


1. 프로젝트 전체 단위에서 적용
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'renderers.CustomRenderer',
    ],
    ...
}

2. viewset 단위에서 사용
class CustomRendererModelViewSet(ModelViewSet):
    renderer_classes = [CustomRenderer]
    ...

3. view 단위에서의 사용
@api_view(['GET'])
@renderer_classes([CustomRenderer])
def custom_renderer_view(request):
    ...

 

 

exception handler

기존에는 200_OK 응답이 아니어도 요청 측과 정의한 정보를 전달하기 위해 exception을 raise 시키지 않고, 개별 API마다 직접 데이터를 만들고 status code를 지정하여 응답을 생성하였습니다. 이러한 번거로운 과정도 exception_handler를 직접 구현하는 것으로 해결했습니다.

 


# handler.py
from rest_framework.response import Response


def exception_handler(exc, context):
    """
    exc, context 의 정보를 이용하여 익셉션 내용을 원하는 형태의 데이터로 만들어 응답 할 수 있습니다.
    리턴되는 응답 데이터는 설정된 renderer class 에서 처리됩니다.
    """
    formed_data = {
        'meta': {},
        'errors': {},
    }
    status_code = ...
    response = Response(formed_data, status=status_code, exception=True)

    return response

 

custom renderer class와 마찬가지로 django settings 내 REST_FRAMEWORKEXCEPTION_HANDLER를 설정하여 적용 가능합니다.

 


# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'handler.exception_handler',
}

 

DATABASE 설정

 


# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': '',
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '3306',
        'OPTIONS': {
        },
    ...
    }

 

기존 settings의 DB 설정은 위와 같이 DB 접근에 대한 정보가 하드코딩 된 형태였습니다. 이 때문에 소스 코드 내에 민감한 정보를 가지고 있다는 불안함과 접속 정보가 바뀌는 경우 하드코딩된 값을 직접 찾아서 수정해야 하는 불편함이 있었습니다.

 

이를 해소하기 위해 아래와 같이 DB 접속 정보를 별도 파일에 작성하였고, settings에서는 read_default_file 옵션으로 해당 파일의 정보를 불러올 수 있도록 하였습니다.

 

추후 Devops 팀과도 협업해 배포 시 DB 접속 정보가 저장된 파일을 별도로 생성하여 사용하도록 작업해 소스 코드 내 민감정보 저장에 대한 불안감과 관리의 불편함을 동시에 해소할 예정입니다.

 


# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '......./default_db.cnf',
        },
    },
    ...
}


------------------------
default_db.cnf
------------------------
[client]
database = 
host = 
user = 
password = 
port = 
init_command = 

 

 

 

아쉬움

여유시간이 주어지긴 했지만 TF 과제와 병행했기 때문에 처음에 생각했던 만큼의 공통화를 모두 진행하지는 못한 게 아쉽습니다. 공통화하지 못했던 부분과 다른 프로젝트로의 적용에 대해서는 매주 백엔드 플랫폼에서 진행하는 기술 컨벤션&프로세스 논의 시간을 통해 협의를 거쳐 꾸준히 개선해 나갈 예정입니다.

 

 

 

마치며

더 나은 환경을 위해 고민하고 무엇을 고쳐나가야 할지를 파악하는 것도 중요하지만, 이렇게 파악한 문제들을 늦지 않게 해결해 나가는 것 또한 중요합니다. 현재 백엔드 플랫폼은 지금까지 쌓인 부채를 개선해 나가는 동시에 기술력 있는 조직이 되기 위한 활동을 하고 있습니다. 다음 포스트는 이러한 활동들을 주제로 얘기를 나누어 보고자 합니다.

 

 

 

 

채용정보 확인하기

 

 

5
by nc nd