4달차 백엔드 개발자


저는 이제 막 주니어에서도 주니어 그냥 뭐 베이비 개발자👶인 입사 4개월차 백엔드 개발자입니다.

실무 경험이라고는 정말 1도 없는 저는 입사하고 얼마 지나지 않아 처음으로 서비스 개발(!)을 하게 되었습니다.

ONE님: amy님이 서버 개발을 하실 거예요.

저:

저: (큰일났다큰일났다어떻게덜덜덜)
저: (큰일났다큰일났다어떻게덜덜덜)

정말… 갑자기 막막하게 느껴지고 그러는 거 있죠? 사실 학교에서 공부를 하고 프로젝트를 했다고 한들, 실서비스가 되는 것은 아니었고, 입사한 지 얼마 되지 않은 저에게는 무척 부담으로 다가왔습니다. (물론 지금은 어떤 프로젝트든 할 수 있다는 마음가짐으로 일하고 있습니다😉)

그래도 해야죠
그래도 해야죠

하지만 어쩌겠어요 마감 시간은 다가오고 있고, 저는 백엔드 개발자이니 개발해야지요!

이슈 난리났네 난리났어


생애 첫 실 서비스를 개발하게 되다보니 정말 우여곡절이 많았습니다.

코드를 짜면 생기는 버그와 다른 분이 짜신 코드와는 다르게 스파게티 코드마냥 길고 난해한 제 코드를 보며 왜 이따구로 코드를 짰을까하는 자기 비관 그리고 아니야 잘하고 있어! 넌 정말 멋지고 짱이야! 라는 자아회복을 반복하며 프로젝트를 해나갔습니다.

이따구로 개발할 거면 개발자 때려쳐 / 난 킹왕짱 멋진 개발자다
이따구로 개발할 거면 개발자 때려쳐 / 난 킹왕짱 멋진 개발자다

그렇게 코드 짜고, 수정하고, 코드 짜고, 수정하고, 로직 짜고, 코드 짜고, 울며 수정하고, 코드 짜고, 수정하고, 로직 짜고, 코드 짜고, 수정하고, 코드 짜고, 모르는 거 찾아보고, 코드 짜고, 수정하고, 모르는 거 검색하고 …

짜잔 프로젝트 기간이 사라졌습니다.
짜잔 프로젝트 기간이 사라졌습니다.

프로젝트가 막바지에 이르러 있었습니다.

정말 눈 깜짝할 사이에 프로젝트가 막바지에 이르러 정말 정말 서비스 운영까지 그리 많은 시간이 남지 않은 때 였습니다. 프론트와 연동도 하고 그 과정에서 나온 수정사항도 고치고 버그도 고치며 개발하던 와중에 속도 이슈가 발생했습니다.

소프트웨어에서 속도란 단순히 성능 뿐만 아니라 사용자와 아주 밀접하게 관련되어 있습니다. 속도를 높히는 것은 성능을 향상 시키는 것뿐만이 아닌 사용자 유지 및 사용 경험의 질을 향상 시키기 때문에 몹시 중요하죠.

그래서 저희는 이 이슈를 잡아야 했습니다.

무엇때문에 발생했는가? 🤔


이슈를 잡기 위해 원인분석을 시작했습니다. 처음에는 프론트에서 로딩 시간이 느린 것일까 해서 프론트의 속도를 최적화를 해야하는 가하고 생각했었어요.

하지만 알고보니 서버에서 데이터를 조회하는 과정에서 매우 많은 중복쿼리가 발생하여 속도가 느린 것이었습니다.😥

# models.py
class Product(models.Model):
   content = models.TextField()

class Media(models.Model):
   product = models.ForeignKey('Product', on_delete=models.CASCADE)
   url = models.URLField()

가령 Post들의 이미지들 중 첫 번째 이미지만 불러온다고 했을 경우,

product = Product.objects.prefetch_related('media_set').get(content='test')
media = product.meida_set.first()
MediaSerializer(instance=media).data

기존 코드는 이런 식으로 짜여져 있었습니다.

하지만 이 코드는 prefetch_related로 Media들을 한꺼번에 가져왔지만 post.media.first()를 통해 한 번 더 데이터를 조회하고 있습니다.

어떻게 해결했는가?


annotate와 subquery 이용하여 데이터를 가져올 때 한꺼번에 가져와 쿼리를 줄이는 방법을 쓰게 되었습니다.

thumbnail = Subquery(
   Media.objects.filter(
      product_id=OuterRef('pk')).values('url')[:1], output_field=models.URLField(),
   )
product = Product.objects.annotate(thumbnail=thumbnail).prefetch_related('media_set').get(content='test')
MediaSerializer(instance=product.thumbnail)

그러자 놀랍게도 1s에서 200ms까지 줄어들었습니다! 😲

확연히 줄어드는 속도에 쿼리 최적화가 얼마나 중요한지 깨달을 수 있었죠. 또한 이런 식으로 계속해서 코드를 짰다면 아주 큰일이 났을 거라는 생각에 부끄럽기도 했습니다.

들숨에 쿼리 날숨에 최적화


장고에 대한 광기는 커져만 갔다.
장고에 대한 광기는 커져만 갔다.

이런 일을 한 번 겪고 나니 쿼리 최적화에 대한 열망은 더욱 커져만 갔습니다. 때문에 다음 프로젝트에서 더더욱 쿼리 최적화에 공을 들이게 되었죠.

Prefetch와 to_attr

특히나 쿼리 최적화를 하던 중, 가장 크게 쿼리가 줄었던 방법은 Prefetch의 to_attr을 사용하는 방법이었습니다.

prefetch_related와 함께 사용할 수 있는 Prefetch는 prefetch_related를 통해 연산되는 과정에서 사용하는 쿼리를 지정할 수 있습니다.

가령, Post의 Media 중 첫 번째 것만 가져온다고 했을 경우 Prefetch를 사용하면 아래 코드와 같이 작성할 수 있습니다.

post_set = Post.objects.prefetch_related(Prefetch('media_set', queryset=Media.objects.first())).all()

또한 Prefetch에서 제공하는 기능 중 하나인 to_attr이 있습니다.

to_attr을 사용하면 Prefetch로 가져온 데이터들을 리스트 형태로 저장합니다. 리스트 형태로 저장한 데이터들은 to_attr에서 지정한 이름으로 하나의 필드처럼 접근할 수 있습니다. 하지만 QuerySet 형태가 아니기 때문에 update를 사용할 수 없습니다.

post_set = Post.objects.prefetch_related(Prefetch('media_set', queryset=Media.objects.first(), to_attr='to_media_set')).all()
for post in post_set:
   print(post.to_media_set) ### Media

위의 코드를 보면 to_attr을 통해 새롭게 to_media_set이라는 필드가 생기고, 해당 필드에는 Prefetch의 queryset에 따라 post의 media_set 중 첫 번째 Media 데이터가 들어가 있습니다.

to_attr을 사용해 코드를 개선하였는데요

### 기존코드
event_set = Event.objects.prefetch_related('option_set','app_set').filter(state='end', is_active=True)

for event in event_set:
   option_set = event.option_set.all()
   for option in option_set:
      bulk = []
      target_set = event.app_set.filter(option__value=option.value).order_by('?')[0:option.number]
      for target in targer_set:
            target.is_catch = True
            bulk.append(target)
      App.objects.bulk_update(bulk, ['is_catch'])


### 개선 코드
event_set = Event.objects.prefetch_related(
   Prefetch(
      'option_set',
      queryset=Option.objects.all(),
      to_attr='to_option_set'
   ),
   Prefetch(
      'app_set',
      queryset=App.objects.all(),
      to_attr='to_app_set'
   )
).filter(state='end', is_active=True)

for event in event_set:
   for option in event.to_option_set:
      target_set = [app for app in event.to_app_set if app.option.value == option.value]
      random.shuffle(target_set)
      del target_set[option.number:]
      for target in target_set:
         tartget.is_catch = True
      App.objects.bulk_update(target_set, ['is_catch'])

기존 코드와 다르게 개선 코드에서는 to_attr을 이용해 Option과 App를 가져와 리스트 형태로 연산을 진행하여 DB 조회 횟수가 줄어들었으며 속도 또한 빨라졌습니다! 🥳

글을 맺으며…


회사에 들어오고 나서 처음으로 하는 프로젝트였다보니 얼렁뚱땅 코드를 짜기도 하고 많이 배우기도 한 거 같습니다.

특히나 ORM 부분은 아주 얄팍하게 알고 있다는 것을 깨닫기도 했죠. 정말 끊임없이 공부하고 찾아봐야 하는 게 개발인 거 같네요.

조금이라도 이 글이 도움이 되어서 여러분의 서버 속도가 빨라지길 바랍니다😉

amy's profile image

amy

2021-03-31 17:00

Read more posts by this author