자바스크립트를 활성화 해주세요

django, 협업하기 좋은코드 만들기

django clean code

 ·  ☕ 5 min read

들어가며


   django를 사용하게 되면 보통 django rest framework(이하 drf)를 같이 사용하여 rest api 서버를 구성하는 경우가 많다. 여러 프로젝트를 거치며 다양한 스타일의 django 코드를 경험하였는데, 추상화 수준이 낮은 function based view 로 구현하는 방식부터 DRF GenericViewSet을 활용하여 구현 코드를 최소화 하는 방식 등등 상황에 따라 구현방식을 적절히 선택해왔던 것 같다. 하지만 공통적으로 서비스가 커짐에 따라 비즈니스 로직들이 복잡해지게 되면서, 코드의 복잡성이 높아지고 유지보수가 어려워지는 경우가 많이 발생하게 된다.

django orm

   django의 orm은 model과 DB가 직접 연결되어있는 Active Record 패턴이다. 이를 통해 SQL문을 직접 작성하지 않고 모델의 메서드로 DB 쿼리를 실행할 수 있다. 이는 단순한 CRUD 작업에는 매우 효율적이지만, 도메인이 복잡해지면 여러개의 테이블을 연결하여 사용하는 경우가 많아지게 되는데, 이때 코드는 걷잡을 수 없이 복잡해지게 된다. 또한 N+1 문제를 발생시키기 쉬운 코드를 작성하게 된다.

django style guide


   이번 회사에 온보딩을 하면서 코드를 이해하는데 어려움을 느꼈는데, 먼저 핀테크에 대한 경험이 부족하다보니 관련 용어나 플로우에 대해 익숙치 않았던 점도 있지만, 코드컨벤션 없이 비즈니스 로직들이 파편화 되어 있어 코드파악이 어려웠다. model, serializer, view등 여러곳에 비즈니스 로직들이 구현되어있었고 재사용이 어려웠다. 특히나 django, drf의 높은 추상화 수준의 클래스를 상속받아 그 의도와 다르게 메소드를 오버라이드 하는경우도 있어 더욱 파악이 어려웠다. 그래서 백엔드 팀의 코드컨벤션이 절실했고 이를 위해 팀 내 ‘django convention’ 문서를 작성하였다.

django style guide: 링크

   전체적인 컨셉은 위링크 django style guide를 참고하였다. django project들을 진행하면서 고민했던 내용들이 정리되어 있었고 그에 대한 해결책을 제시해주고 있다. 주요 내용을 정리하면 아래와 같다.

  • Service(Selector) layer를 추가하여, 비즈니스 로직을 작성
    • 실제 서비스는 단순히 하나의 model 과 1:1 매핑되지 않기 때문에 이를 어떤 위치에 두어야 하는지 명확하지 않다.
    • 그렇기 때문에 별도의 layer를 통해 관리 해야 한다.
  • 아래 위치에 비즈니스 로직을 놓는것은 지양
    • APIs and Views.
    • Serializers and Forms.
    • Form tags.
    • Model save method.
    • Custom managers or querysets.
    • Signals.
  • model property에 많은 relation 로직을 넣게 되면(fat model) 이는 N+1 문제를 발생시킬 수 있다.
    • relation 로직은 serivce layer로 분리하고 직접적으로 모델과 관련된 로직을 작성
  • data validation은 model objects full_clean 을 활용
  • API & Serializer는 심플하게 유지
  • Input Serializer 와 Output Serializer를 통해 data in/out 관리
  • Custom Exception, logging으로 에러처리에 대한 관심사 분리

code snippet


   위 정리한 내용을 토대로 백엔드 팀에서 활용할만한 snippet을 작성해보았다. 해당 snippet을 보면 API View만 보아도 한눈에 Flow (view → serializer → selector or service → model(manager or property)) 를 파악할 수 있다는 장점이 있다. view 에서는 사용할 selector 나 service를 import 하여 사용하면 된다. selector나 service의 특징에 따라 추상화하여 인터페이스를 정의하면 더욱 가독성 좋은 코드를 만들 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  class {Entitiy}{Action}API(views.APIView):
      """
      Example API
      """

      class InputSerializer(serializers.Serializer):
          """
          Input Serializer
          """

          input1 = serializers.CharField()
          input2 = serializers.CharField(required=False)

          class Meta:
              ref_name = "{entitiy}_{action}"

      class OutputSerializer(serializers.Serializer):
          """
          Output Serializer
          """

          ouput1 = serializers.CharField()
          ouput2 = serializers.CharField()

          class Meta:
              ref_name = "{entitiy}_{action}"

      permission_classes = (permissions.IsAuthenticated,)

      @swagger_auto_schema(
          operation_summary="Example api action",
          operation_description="",
          request_body=InputSerializer,
          responses={
              status.HTTP_201_CREATED: openapi.Response("success", OutputSerializer),
              status.HTTP_400_BAD_REQUEST: openapi.Response("validation error"),
              status.HTTP_409_CONFLICT: openapi.Response("duplicate error"),
          },
      )
      
      def post(self, request): # or def get(self, request)
          input_serializer = self.InputSerializer(data=request.data)
         input_serializer.is_valid(raise_exception=True)
          
          # your service, selector logic Class or function
          service = ExampleService(request.user)
          data = service.something(**input_serializer.validated_data)
          
          output_serializer = self.OutputSerializer(data=data)
          output_serializer.is_valid(raise_exception=True)

          return response.Response(output_serializer.data, status=status.HTTP_201_CREATED)  

Custom Exception handler


Custom Exception handler:

   또 한가지 유용하게 사용하고 있는 내용으로는 exception을 handling하는 방식이다. 아래 그림을 보면 Service(Selector) layer에서 Exception 을 발생시키면 Custom handler를 통하여 Response 하게 된다. 에러나 예외케이스에 대해 일관성있게 Response 할 수 있다. 실제로 현재 Custom handler에 error_code와 error_msg를 정의하여 Response 하고 있으며, 이를 swagger에 정의하여 Frontend와의 커뮤니케이션하고 있다. 아래는 사용하고 있는 에러 response 형식이다.

1
2
3
4
5
  {
      "error_code": 000000,
      "error_class": "NotFoundSomething",
      "error_detail": "에러 메시지에 대한 내용",
  }

모든문제가 해결되었나?


   사실 컨벤션 도입으로 코드의 가독성, 유지보수성은 크게 좋아졌지만 여전히 문제들은 남아있다. Service(Selector) layer 로직들이 특정 기능들에 특화되다 보니 이를 재사용하기가 어려워지고 중복코드가 발생하거나 layer 안에서 의존성이 생기는 문제가 발생하였다. 그래서 Service(Selector) layer에서 자주 사용하는 기능은 모듈화 + 최적화를 진행하여 최대한 재사용성이 좋게 하였고, 너무 복잡하거나 성능이 중요한 부분은 무리해서 ORM을 사용하지 않고 Raw Query를 허용하여 유연하게 대처하였다.

   추가적으로 DDD를 도입하여 Repository and Unit of Work Patterns를 도입하는것도 고려해보았는데(도메인 layer와 영속성(Persistent) layer의 완전한 분리를 위해), 현재 운영중인 django project에 적용하기에는 너무 공수가 컸고, 새로운 Pattern을 익히는데에도 러닝커브가 클거 같아 일단은 보류하였다. 추후에 DDD EventStorming 이후에 컴포넌트 분리가 어느정도 정리되는대로 진행해볼 예정이다.

마치며


   클린코딩은 여러가지 방법론과 규칙들이 존재한다. SOLID 원칙 준수, 디자인패턴 적용, 적절한 변수명, 파일명 사용하기 등등 구글링을 하면 유용한 정보들이 많이 존재한다. 하지만 개인적으로 제일 중요한건 협업하는 사람들과의 약속이라고 생각한다. 이런 약속안에서 작성된 코드는 예측이 가능해지고 코드를 파악하는데 크게 도움이 된다. 그리고 코드리뷰를 하기 위한 좋은 참고가 되기도 한다. 아직 현재 회사에서는 레거시 코드들이 남아있긴 하지만, 현재 새로 만들어지는 기능들이나 리팩토링 작업들은 위 style guide를 기반으로 작업이 되고 있고, 이를 통해 같이 일하는 팀원들이 기존 코드 파악이나 새롭게 만들어지는 기능의 가독성이 크게 좋아졌다고 말해준다. 앞으로 이 style guide를 더 고도화하여 지금보다 더 나은 코드가 되도록 해야겠다.


shin alli
글쓴이
shin alli
Backend 개발자 (Python, Django, AWS)