필자가 속한 스타트업은 온라인 앱 서비스를 운영하고 있습니다. 등록된 누적 회원수는 올해로 100만을 넘어섰으며, 하루에도 몇번씩이나 업데이트를 하며 쉴 새 없이 새로운 피쳐가 올라가고 내려가는 '린'한 개발 문화를 가지고 있습니다.
하지만 이러한 상황속에서도 이를 책임지는 엔지니어는 단 두 명으로 그중에서도 높은 트래픽을 처리해야하는 백단쪽 업무는 필자 단독으로 맡고 있습니다.
또 기획된 여러 피쳐들에 대한 수익성 및 가치를 빠르게 검증하기 위한 끝 없는 백로그들이 개발팀을 기다리고 있기 때문에, 저희의 개발팀은 코드 품질, 안정성, 완벽함 보다는 '민첩함'에 집중해야합니다.
하지만 그렇다고 '민첩함'에만 집중하게 된다면, 쌓이게 되는 좋지 않은 코드 품질이 가까운 미래의 병목 현상으로 찾아오게 될 수 있습니다.
실제로 필자가 속한 조직은 필자가 들어오기 전까지는 과도하게 '민첩함'에만 집중한 나머지, 코드, 서비스 관점에서의 이상이 생겨, 개발 일정은 지켜지지않기 일쑤였고 몇년동안 작성된 코드에는 안티 패턴들이 수두룩했습니다.
따라서 당장의 그저 빠름은 가까운 미래의 병목현상으로 되돌아오기에, 저희에게는 개발 일정을 딜레이시키지 않는 수준에서 코드 품질을 관리하면서도 빠르게 달릴 수 있는 '장기적인 민첩함'이 필요했고, Django를 이용하여 REST API를 만드는 입장에서 어떻게 하면 민첩함을 1순위로 두면서도 품질에 큰 문제없는 환경을 장기적으로 유지할 수 있을까 많은 고민을 했습니다.
그래서 Django로 REST API를 빠르게 개발하기 위한 경험에 대해 간략히 공유해보려 합니다.
'민첩함'에 집중한 개발 프로세스를 만들기 위해 몇가지 것들을 이해하고 인정했습니다. 모든 방면에서 완벽할 수 없기 때문에 꼭 필요한 것 이외에는 포기하는 '과감함'이 필요합니다.
비즈니스가 요구하는 '민첩함'의 수준을 맞추기 위해 Django/DRF 스택에서 기본적으로 제공(반강제)하는 것들을 포기합니다.
DRF의 Serializer는 queryset와 같은 Django만의 데이터를 JSON 등의 일반적인 데이터 포맷으로 쉽게 렌더링해주는 요소입니다.
Serializer를 활용하기 위해서는 models.py에 데이터 모델을 정의한 뒤, serializer.py에 모델과 필드를 연결시켜주는 작업을 해야하는데, 데이터 모델이 추가될 때 마다 이중 작업을 유발시킵니다.
사실 JSON 형태로 변환시키는 것이야 Django ORM을 활용하여 model.objects.filter().values()
구문만 잘 활용해도 리턴 값을 JSON 형태로 바로 활용할 수 있기 때문에 Serializer가 꼭 필요하지 않았습니다.
Serializer의 가장 큰 장점은 데이터 모델에 Foreign Key로 연결된 nested한 데이터를 바로 바로 가져오는 것인데, 뒤에서 설명하겠지만 민첩한 개발을 하기 위해 Django 단위에서는 Foreign Key 활용을 하지 않고, ORM으로 JOIN 형태의 쿼리를 사용하지 않을 것이기 때문에 과감히 포기했습니다.
Serializer 없이 기본적인 모델과 ORM 만으로도 충분한 구현이 가능합니다.
데이터의 무결성을 위해 데이터 모델링을 하다 보면 많은 Foreign Key를 붙이게 됩니다. 이를 Django Model로 변환해보면 ForeignKey 라는 타입으로 필드가 기본으로 선언되게 됩니다.
Django에서 Foreign Key를 사용한다면 감수해야하는 불편함은 꽤 큽니다.
Foreign Key를 포함한 모델을 생성하거나 조회할 때마다 항상 model.objects.get()
구문을 달고 살아야 하고
Talk.objects.create(
talkId=Talk.objects.get(talkId=talkId)
)
혹시나 데이터베이스 라우팅을 사용하고 있다면 중복적인 get 구문 + using 구문을 달고 살아야 합니다.
Talk.objects.create(
talkId=Talk.objects.using('default').get(talkUID=talkId)
)
또 model.objects.filter().values()
구문으로 바로 데이터를 조회하게 될 때 Foreign Key 필드의 키 값에는 __id
접미사가 붙게 되어 데이터를 응답해주게 될 때 일일이 정제해주어야 하는 불편함이 발생합니다.
따라서 불편함 중복 코드로 인한 병목을 방지하고, 원치 않은 포맷의 키 데이터 구조가 강제되는 것을 막기 위해 Foreign Key는 RDB 상에서만 강제하고 Django 수준에서는 사용하지 않기로 했습니다.
앞서 제가 말씀드렸듯이 저희 조직이 원했던 것은 단순한 '민첩함'이 아닌 장기적인 '민첩함' 이었습니다.
당장 빠르게만 개발하게 된다면 비슷한 로직 끼리는 이전에 작성했던 코드를 복/붙하여 중복 코드를 발생시키면 됩니다. 하지만 이것은 지금 해야할 일을 미래로 미루는 것에 불과하기 때문에 결국은 가까운 미래에 중복 코드라는 부채를 해결하기 위해 더 많은 시간을 써야할 것입니다. 따라서 '민첩함'에 집중한 나머지 기본적인 코드 품질을 잃어버리지 않게 하기 위해 중복 코드를 무의식적으로도 제거할 수 있도록 구조화하는 것이 필요하다고 생각했습니다.
매우 단순한 것입니다만, 같은 수준의 기능에 대해서 반복해서 쓰일 수 있는 데이터 조회 등의 로직(1:1 대화 목록을 가져와라) 은 repository 파일로 모으고, 프로젝트 전체적으로 반복해서 쓰일 수 있는 유틸성 함수(특정 전화번호에 SMS를 발송하라)는 utils 폴더로 모았습니다.
코드 품질에 너무 매몰되어도 안되지만, 기본적인 수준의 품질은 보장이 필요했고, 그에 대한 적절한 수준을 '중복 코드를 최소화할 수 있는가?'로 본 것입니다.
실제로 경험상 코드 품질에 대한 별다른 신경을 크게 쓰지 않더라도 중복 코드만 적절히 통합한다면, 유지보수의 큰 문제는 생기지 않았습니다. 개발 부채에 대한70%의 원인은 중복 코드에 있다고 생각합니다.
중복 코드만 꾸준히 잘 통합된다면, 추후 유지보수 할 때에는 잘 명명된 함수만 나열하는 것으로도 충분합니다. 이는 마치 빈 코드에 주석을 열거하는 것과 비슷할 정도로 유지보수 과정이 간결해집니다.
Django ORM은 매우 강력한 도구입니다. 직관적으로 데이터를 필터링하여 조회할 수 있고 일반 SQL을 쓰는 것보다 훨씬 간결하게 작성할 수 있습니다.
Talk.objects.filter(id=1);
하지만 JOIN이 들어가고 서브 쿼리가 복잡하게 들어가게 되면, 오히려 ORM을 사용하는 것이 코드를 더욱 복잡하게 만들 수 있습니다.
가령, Django ORM으로 JOIN을 구현하려면
Talk.objects.select_related();
Talk.objects.extra(tables=['user'], where=['user.id'=talk.userid]);
와 같은 식으로 작성이 되어야 하는데, 앞서 보여드렸던 기본적인 ORM의 조회 구문과는 사뭇 다르게 복잡해집니다. 하나의 테이블만 JOIN 하는 데도 이렇다면 두개, 세개의 테이블을 조인해야한다면 더욱 복잡해질 것 같습니다.
JOIN을 해야하는 정도로 복잡해질 때 Django ORM 보다는 Raw Query를 활용하면 더욱 빠르게 데이터 로직을 작성할 수 있습니다. 우리에게 SQL은 이미 친숙할테니까요!
model.objects.raw()
구문을 활용하면 Raw Query를 통해 데이터를 관리할 수 있습니다.
따라서 필자도 단순한 데이터 조회는 ORM이 JOIN 및 서브쿼리가 필요한 복잡한 데이터 조회는 SQL로 가져오는 것이 효율적이라 생각하여 나누어 쓰고 있습니다.
최소한의 품질의 보장받으면서도 빠르게 Django REST API를 개발하고 싶다면
유진의 - AUSG (AWS University Student Group) 3기로 활동 중