AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까
버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까 (준비중) …
Read Article버즈빌이 해당 프레임워크(Django)를 사용하게 된 지는 아마 한 1년 반 정도 전인 2015년 봄쯤이었던 것으로 기억합니다. 그 전에는 Django 함수를 기반으로 한 개발이 주로 되어 있었습니다. 그러다 새로운 프로젝트를 진행하게 되었고, 그때, 회사의 다른 개발자분들과 새로운 프로젝트를 진행할 스펙과 기술에 대해 정하던 중에, Front-end와 Back-end를 분리해야 할 필요가 있어서 알아보게 되었고, Back-end에서는 Django rest framework를, Front-end에서는 Angularjs를 알게 되어 그때 Back-end 기술의 일환으로 Django Rest framework의 사용 결정했습니다.
먼저, 기존의 코드와 Django rest framework를 사용해서 어떤 식으로 단점을 보안 하였는지 말씀드리도록 하겠습니다. 첫번째로, 아래 보이는 코드가 기존 프로젝트에서 view를 return 할때 사용한 코드 입니다. 함수 기반의 view를 사용해서 작업을 했었습니다.
def test_view(self, request):
return render_to_response('test', {})
이 view 방식의 단점은 front-end코드와 back-end코드가 맞물려 있다는 단점과, html 코드가 아닌 다른 코드를 리턴해야 할 필요가 있을 경우, 모든 코드의 수정이 필요하다는 것입니다.
아래의 코드는 json을 리턴하고 싶을 때 사용했던 코드 입니다. (더 좋은 방법이 있을수도 있으나, 간단하게 parameter를 받아서 작업을 하였습니다.)
def test_view(self,request):
# Return JSON type
if request.GET.get('type') == 'json':
return HttpResponse(data, content_type='application/json')
return render_to_response('test', {})
이 또한 마찬가지로 http method를 분기 처리 할때 똑같이 발생하는 문제였습니다.
def test_view(self,request):
# Return JSON type
if request.method == 'POST':
blahblah...
if request.method == 'GET':
return render_to_response('test', {})
이러한 문제가 발생함에 따라 처음에는 저기까지는 뭐 노가다로 하면 할수 있다고 생각이 들수도 있긴 합니다만, Front-end가 점점 복잡 해짐에 따라서 **“ID로 필터를 걸어주세요.! 리턴되는 데이터가 너무 많으니 pagination처리해서 UI에 보여주고 싶어요!” **라는 요구 사항들이 들어오고, 많은 요구 사항에 대해 어떻게 유연하게 처리해 줄수 있지? 라고 생각을 하게 되었습니다.
오랜 고민 끝에, “그래! 장고에서 pagination을 지원하니, 일단 그걸 쓰도록 해볼까?? filter는 어떻게하지?? 아 그냥 손수 parameter 받아서 구현하지 뭐 얼마나 많아 지겠어." 라는 생각이 들어 구현 작업에 나섰습니다.
그리하여 나온 이 아래 코드가 filter와 장고에서 지원하는 pagination을 적용시킨 코드 입니다. 보시면 try…except 같은 코드가 pagination처리할때 보여서, 코드를 보기에도 좋아 보이지 않죠?
from django.core.paginator import Paginator
def test_view(self,request):
model_list = Model.objects.all()
# Filter
some_filter = request.GET.get('some_filter')
if some_filter:
model_list = model_list.filter(some=some_filter)
...
# Pagination
paginator = Paginator(model_list, 25)
page = request.GET.get('page')
try:
models = paginator.page(page)
except PageNotAnInteger:
models = paginator.page(1)
return render_to_response('test', {'models': model})
좋아.. 여기까지는 우리가 잘 버텼어!! 근데, 데이터 validation처리도 해야 하는데 어떻게 하지? 장고 Form을 이용하자!!! 음.. 그럼 사용할때 마다 Form이 필요한거겠네..라는 생각이 안 들 수가 없는데요. 그래서 고안한 방법이 아래의 코드입니다.
# test/forms.py
from django.forms import ModelForm
from test.models import Model
class TestForm(ModelForm):
class Meta:
model = Model
def clean():
cleaned_data = super(TestForm, self).clean()
# add validation code
return cleaned_data
# test.views.py
from django.core.paginator import Paginator
from test.forms import TestForm
def test_view(self,request):
# Return JSON type
if request.method == 'POST':
form = TestForm(request.POST)
if form.isvalid():
form.save()
else:
blahblah
if request.method == 'GET':
return render_to_response('test', {'models': model})
어찌어찌하여 여기까지는 잘 버텼다고 생각했지만 알고보니 아래와 같은 더 많은 요구사항들이 있었습니다.
위의 문제점들을 해결하기 위하여 이 작업을 또하긴 싫은데 어떻게하지 생각하다가 이 단점들을 보안할 수 있고, 나중에도 사용할수 있는 Django rest framework를 알게 되었고, 저희의 문제점을 해결해 줄 수 있을 거 같다는 생각이 들었습니다.
Django rest framework 의 장점을 말씀드리자면 다음과 같습니다.
위의 장점들에서도 알 수 있듯이 Django Rest Framework는 API레벨도 잘 지원하면서, Back-end와 Front-end를 분리할수 있고, Django와도 잘 맞는 프레임워크였고, 더불어, 새로운 프로젝트를 진행하는 시점에서 새로운 기술을 사용하는건 새로운 도전이라고 생각해서 사용하기로 결정하였습니다.
그래서 저희가 처음으로 도입한 방법은 문서에 있는 class 베이스의 generics package에 있는 것을 상속 받아서 구현하는 것이였습니다. 이게 처음에 짠 코드 입니다. 위에 비교해서 엄청 깔끔 해졌죠?
# test/urls.py
urlpatterns = patterns(
'test.views',
url(r'^/api/tests/(?P<pk>\d+)', TestDetailView.as_view()),
url(r'^/api/tests', TestListView.as_view()),
)
# test/views.py
class TestListView(generics.ListCretateView):
queryset = Model.objects.all()
serializer_class = ModelSerializer
class TestDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Model.objects.all()
serializer_class = ModelSerializer
이러한 방법으로 개발의 속도를 높이고 있던 와중에 점점 url이 많아짐에 따라 view도 많아지고, copy&paste 되는 코드도 많아 관리상의 이슈가 생기게 되었습니다. 이건 샘플로 url이 많아졌을 경우에 view를 이용해 컨트롤 할수 있는 방법입니다.
# test/urls.py
urlpatterns = patterns(
'test.views',
url(r'^/api/tests/(?P<pk>\d+)/pets/(?P<pk>\d+)', PetsDetailView.as_view()),
url(r'^/api/tests/(?P<pk>\d+)/pets', PetsListView.as_view()),
url(r'^/api/tests/(?P<pk>\d+)', TestDetailView.as_view()),
url(r'^/api/tests', TestListView.as_view()),
)
# test/views.py
class TestListView(generics.ListCretateView):
queryset = Model.objects.all()
serializer_class = ModelSerializer
class TestDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Model.objects.all()
serializer_class = ModelSerializer
class PetsListView(generics.ListCretateView):
queryset = Pet.objects.all()
serializer_class = PetSerializer
class PetsDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Pet.objects.all()
serializer_class = PetSerializer
또한, url(N) === view(N) 개 생기는 관리상의 이슈와 확장성의 이슈를 해결할수 있는 좋은 방법이 없을까 고민하던 와중에, Django rest framework에 Viewset과 Router를 통해 view들의 집합을 만들수 있다는 것을 알게 되었고, 이것을 통해 위와 같은 관리상 이슈는 해결 할 수 있다는 것을 알았습니다.
이 아래 코드가 viewset과 router를 사용했을 때의 코드 입니다. 깔끔하게 정리 되었죠??
# test/urls.py
router = SimpleRouter()
router.register(r'/pets', PetViewSet)
router.register(r'/', TestViewSet)
urlpatterns = patterns(
url(r'^/api/tests', include(router.urls))
)
# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
serializer_class = PetSerializer
queryset = Pet.objects.all()
class TestViewSet(viewsets.ModelViewSet):
serializer_class = ModelSerializer
queryset = Model.objects.all()
이제 해결해야 할 문제는 페이징 처리와 filter에 관련된 내용인데요. 이 문제도 Django rest framework에서 제공하는 pagination_class와 filter_fields, search_fields ordering_fields를 쉽고 보기 좋게 해결 할 수 있었습니다.
pagination_class 는 페이징 처리시에 참조가 되는 class의 이름을 세팅하는 변수이고, 세팅이 되어 있을 경우에 리턴 되는 데이터를 페이징 처리해서 보여줍니다. 페이징 처리를 하였을 때랑 페이징 처리를 하지 않았을 경우, return 되는 데이터의 구조가 달라지게 됩니다. (중요!!)
pagination_class가 세팅 되어 있을 때 return되는 데이터의 구조
{ count: 1000, results: […] }
pagination_class가 세팅 되지 않았을 때 return되는 데이터의 구조
[…]
filter_backends 변수는 해당 class로 요청이 들어왔을 때 사용할 filter들을 정의할때 사용됩니다. 밑에 예제에서 보면 저는 3개의 필터(SearchFilter, OrderingFilter, DjangoFilterBackend)를 사용 했습니다.
짜잔!! 이 아래에 있는 코드가 filter와 ordering을 썼을때의 최종 코드 입니다.
# test/utils.py
class LargePagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 10000
# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
pagination_class = LargePagination
serializer_class = PetSerializer
queryset = Pet.objects.all()
filter_backends = (filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend, )
filter_fields = ('filter1', )
serach_fields = ('id', 'name', )
ordering_fields = ('id', 'created_at',)
ordering = ('-id')
Django rest framework의 또 다른 장점이라고 할수 있는건, 권한 및 유저 별 데이터의 포맷을 변경 하기 용이 하다는 것입니다. 저희는 Serializer를 사용해서 포맷이나 데이터의 제한을 두고 있습니다. 아래와 같이 Serializer를 통해 client에 들어오는 데이터(to_internal_value)와 나가는 데이터(to_representation)의 변환을 쉽게 할수 있습니다.
# test/serializers.py
class AllDataReturnSerializer(serializers.ModelSerializer):
class Meta:
model = Model
fields = '__all__'
def to_representation(self, val):
return super(AllDataReturnSerializer, self).to_representation(val)
def to_internal_value(self, val):
return super(AllDataReturnSerializer, self).to_internal_value(val)
class SomeDataReturnSerializer(serializers.ModelSerializer):
class Meta:
model = Model
fields = (
'some1',
'some2',
'some3',
)
# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
…
def get_serializer_class(self):
if return_all_data:
return AllDataReturnSerlaizer
elif return_some_data:
return SomeDataReturnSerializer
Django rest framework를 쓰면서 느끼는 점은 완벽하지 않아도 규약이나 규칙을 통해 개발자의 개발 시간과 가독성을 올리는 데 한 몫을 하였습니다.
그와 별개로 인제 Django rest framework를 사용 한 후에 저희가 다음으로 신경 쓰는 부분은 테스트 인거 같습니다. 개발자가 많아지므로 인해서 관리해야 하는 코드도 많아지고, 변경을 했을 때의 생각치도 않은 곳에서 발생하는 오류가 점점 커지면서 소모되는 리소스의 양도 점점 많아지고 있습니다.
그래서 지금까지는 Back-end API레벨단의 테스트만 진행하고 있는데, 앞으로의 고민은 Front-end에 대한 코드가 점점 커지면서, 테스트에 대한 Needs가 필요한데, 이것에 대한 처리를 어떻게 해야 좋을지, 화면단의 테스트 코드까지 테스트코드로 진행해야 할지 고민해야 하는 큰 문제 인거 같습니다. 이에 앞으로도 많은 고민이 수반되어야 할 것 같습니다.
버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까 (준비중) …
Read Article들어가며 안녕하세요, 버즈빌 데이터 엔지니어 Abel 입니다. 이번 포스팅에서는 데이터 파이프라인 CI 테스트에 소요되는 시간을 어떻게 7분대에서 3분대로 개선하였는지에 대해 소개하려 합니다. 배경 이전에 버즈빌의 데이터 플랫폼 팀에서 ‘셀프 서빙 데이터 …
Read Article