django - filter().exists(), Q() 객체, F() 객체

김영훈·2021년 6월 18일
2

Django

목록 보기
7/11

# django와 친해지기

시니어 개발자분께서 내게 해주셨던 말씀이 있다. "django로 취직해서 밥 벌어먹으려면, 누구보다 django에 빠삭해야 해요." 이 말을 들을 당시에는 사실 한 귀로 듣고 한 귀로 흘렸다.

그런데 최근 이 말의 의미를 다시금 곱씹고 있다. 계기는 아주 사소한 경험에서 시작됐다. 얼마 전부터 1차·2차 프로젝트 코드를 Refactoring하고 있는데, 내가 짠 코드임에도 이해가 안되는 부분이 많았다. 사소한 개념부터, 각종 메서드에 이르기까지 기억이 나지 않는 내용이 많아서 적잖게 당황했다. 아무리 사람이 망각의 동물이고, 지난 몇 주간 Node.js만 주구장창 사용했다고 하지만, 쉽게 지나쳐선 안 되는 문제라는 생각이 들었다. 이런 상황이 반복되면, 또다시 많은 시간을 관련 정보를 찾고 공부하는 데 써야 할 게 뻔하기 때문이다.

문득, 기술에 대해 아무리 많이 알아도 까먹으면 아무 소용이 없다는 생각이 들었다. 사실 django로 개발을 시작했고, 최근 몇 개월 동안 해당 프레임워크에 관해 공부했기 때문에 django에 대해선 어느 정도 알고 있다고 생각했다. 그런데 아니었다. 이미 지식은 깔끔히 사라진 상태였다. 어디 django만 그럴까? 사용 기술의 전환이 쉽게 이뤄지고, 여러 기술을 동시에 사용해야 하는 개발자의 업무 환경을 고려해보면, 다른 언어와 프레임워크에도 똑같이 적용되는 얘기일 것이다.

그래서 배운 지식을 기록하고 정리하는 일의 중요성을 다시금 느꼈다. 정리된 지식은 휘발돼도 금방 떠올릴 수 있다. 기록까지 한다면, 정보 검색하느라 애쓰는 시간과 자원도 아낄 수 있다.

djnago 관련 지식을 오늘부터 꾸준히 정리해 나갈 생각이다. django(python 포함...)에 빠삭한 개발자가 되기 위한 몸부림(?)이다.

# filter().exists()

  • django ORM으로 쿼리문을 작성하다보면 누구나 한 번 궁금증 갖는 주제가 있다. 바로 조건과 일치하는 object가 DB에 존재하는지를 확인할 때, objects.filter().exists()objects.get() 중 어떤 쿼리문을 사용하는 것이 바람직한지에 대한 의문이다. 처음 django를 배울 땐 filter().exsists()를 사용하는 것이 좋다고 배웠다. get()에 비해 처리 속도가 조금 더 빠르다 것이 이유였다. 나도 별다른 의문을 품지 않고 그냥 넘어갔다.

  • 그런데 최근 filter().exsits()get()보다 빠른 구체적인 이유가 궁금해졌고, 몇 가지 정보를 찾아봤다. 자세한 내용은 아래와 같다.

  • objects.filter() vs objects.get()

    • filter().exsits()get()를 비교하려면, 먼저 objects.filter()objects.get()의 차이점에 대해 알아야 한다. 두 쿼리문을 SQL 쿼리문으로 풀어보면 쉽게 알 수 있다.

    • objects.filter()의 경우 일반적인 SQL문으로 풀이되는 걸 확인할 수 있다.

      # User.objects.filter(email=email)의 SQL문
      
      SELECT `users`.`id`, `users`.`name`, `users`.`phone_number`, `users`.`password`, `users`.`email` 
      FROM `users` WHERE `users`.`email` = fcfargo90@gmail.com
    • 놀랍게도... objects.get()의 경우 SQL문으로 표현할 수 없다. 대신 아래의 코드처럼 풀이할 수 있다. <stackoverflow 참고> objects.get() 쿼리문을 실제로 사용해보면 QuerySet 대신 object를 반환하는데, 그 과정을 이해할 수 있는 코드다.

      # user = User.objects.get(email=email)을 풀어보면...
      
      user = User.objects.filter(email = 'fcfargo90@gmail.com')
      if len(user) == 1:
          return user[0]
      else:
          raise exception
    • objects.get()이러한 로직으로 작동하는 것을 비춰볼 때, objects.filter() 처리 속도가 빠른 이유를 짐작할 수 있다. 실제로 처리 속도가 10% 정도 빠르다고 한다. filter().exsits() 사용이 권장되는 이유도 그래서다.

  • 그렇다면 get() 언제 사용해야 할까?

    • 쿼리문으로 오직 한 개의 object(single object)를 가져오는 경우
    • 조건에 맞는 object가 존재하지 않는 경우를 except로 예외처리하는 경우
    def decorator(self, request, *args, **kwargs):
        try:
            user = User.objects.get(id=decoded_token['user_id']) 
            return func(self, request, *args, **kwargs)
        ### 예외처리
        except User.DoesNotExist:
            return JsonResponse({"message":"UNKNOWN_USER"}, status = 401)

# filter().first()

  • django에서 filter() 쿼리문을 사용하면 조건과 일치하는 object가 담긴 QuerySet을 반환한다는 사실을 알아봤다. 그렇다면 QuerySet에 담긴 object를 꺼내는 방법은 뭘까? 우선 for 반복문 활용을 생각해볼 수 있다. object가 여러 개일 때의 얘기다. 그렇다면 QuerySet에 담긴 object가 한 개라면 어떨까? python 언어에 익숙한 사람이라면 인덱싱(indexing)을 고려할 것이다.

  • 하지만 인덱싱으로 object를 가져오면 문제가 생긴다. filter() 쿼리문이 None을 반환하는 경우, 인덱싱을 사용하면 Error를 반환한다. API에서 Error에 대한 예외처리가 반드시 필요한 것을 생각해보면, 인덱싱이 가진 한계라 할 수 있다.

  • 반면 django에서 제공하는 filter().first() or filter().last()를 사용하면 얘기가 달라진다. 이들 쿼리문은 가져온 결괏값이 None인 경우(QuerSet이 비어 있는 경우) Error 대신 None을 반환한다. 인덱싱보다 편의성이 좋다고 볼 수 있다.

# Q() 객체

  • Q() 객체는 django에서 복잡한 조건의 쿼리문을 만들 때 사용된다. 가령 쿼리문에서 논리 연산자 OR을 사용하거나, SQL문의 LIKE 연산자를 활용하는 경우다. SQL문의 WHERE 절에서 사용되는 AND, OR, NOW, LIKE 등을 떠올리면 이해가 쉽다.

  • 사용법은 아래와 같다.

    • 사용 방법

    • 예시 코드

      q=Q()                                                        # Q 객체 생성
      if category_id:
         q = Q(category_id=category_id) | Q(name__startswith='함')  # 쿼리문에 사용될 조건 생성
      products = Product.objects.filter(q).annotate(star_rating=Avg('review__star_rating')).order_by(ordering)
  • 주의할 점

    • 오직 AND 연산자만을 사용하여 쿼리문을 만드는 경우라며, 굳이 Q() 객체를 사용할 필요 없다. objects.filter()에서도 AND 연사자 활용이 가능하다.

    • 예시 코드

      products = Product.objects.filter(category_id=category_id, name__startswith='함').annotate(star_rating=Avg('review__star_rating')).order_by(ordering)[:20]

# F() 객체

  • F() 객체를 이해하려면 django 공식문서를 참고해야 한다.

    F() 객체는 모델의 필드 혹은 어노테이트된 열의 값을 나타낸다. 실제로 데이터베이스에서 Python 메모리로 가져오지 않고, 모델 필드 값을 참조하고 이를 데이터베이스에서 사용하여 작업할 수 있다.

    • 공식 문서만으로는 이해가 어려울 수 있는데, 쉽게 말해 django에서 F() 객체를 사용하면 F() 객체가 사용된 연산해당하는 쿼리문생성된다. 생성된 쿼리문을 바탕으로 python의 도움 없이 데이터베이스에 접근하여 연산처리가 이뤄진다.

    • 예시 코드

      from django.db.models import F
      
      reporter = Reporters.objects.get(name='Tintin')
      
      # F() 객체를 사용함으로써, 데이터베이스에서 object(reporter)의 필드값(stories_field)이 1만큼 증가하게 된다. 
      # 업데이트된 필드값은 python 메모리로 가져오지 않은 상태이다. 그러므로 python에서 reporter.stories_field의 값에 접근하면, 업데이트되기 이전의 값을 반환하다.
      
      reporter.stories_filed = F('stories_filed') + 1  
      reporter.save()
    • reporter.stories_filed = F('stories_filed') + 1은 필드 'stories_filed' 값을 1 증가시키는 reporter.stories_filed += 1과 똑같은 기능을 한다. 차이점은 python 도움 없이 DB에서 직접 처리된다는 사실이다.

  • F() 객체의 장점은 크게 네 가지다.

    • 파이썬을 사용하지 않고 DB에 접근 가능

    • 쿼리의 수를 줄여준다.

    • 경쟁 조건 (race condition)을 피할 수 있다는 점이다.

    • 어노테이션, 필터링, 정렬에 효과적으로 사용할 수 있다

      • .order_by() 쿼리문에서 .desc() 또는 .asc()를 활용해 nulls_last 옵션과 nulls_first 옵션을 지정할 수 있다.
      • .annotate() 쿼리문에서 서로 다른 필드의 값들을 연산하여 동적으로 필드추가할 수 있다.
  • 경쟁 조건을 피하면 무엇이 좋을까?

    • 경쟁 조건이란 데이터베이스의 필드값을 python 스레드로 가져와서 처리하는 과정에서 발생한다. 자세한 설명은 아래의 사이트를 참고하면 된다. 요약하면, 웹사이트에 다수의 요청동시 발생하면서, 연산(필드 값을 더하거나 빼는)이 누락되어 데이터베이스반영되지 않는 것을 말한다. 이는 클라이언트의 요청을 무시하는 결과로 이어지기 때문에 방지가 필요하다.

    • F() 객체를 통한 경쟁 조건을 피하는 것은 데이터베이스 충돌을 해결한다는 측면에서 django의 transaction 사용 목적과 비슷한 측면이 있다.

    • 다수의 오브젝트를 업데이트해야 하는 경우에 F() 객체를 사용하면 퍼포먼스 향상의 효과를 볼 수 있다. 특히 쿼리문으로 CRUD하려는 필드가 DB 값을 기준으로 증가하거나 감소하는 필드라면 F()를 사용하는 것이 좋다.

    • F() 사용 시 주의해야 할 점(참고 사이트)

profile
Difference & Repetition

0개의 댓글