2025/11/26 Django - Project 4

김기훈·2025년 11월 26일

TIL

목록 보기
66/194

오늘 학습 내용 ✅

오늘 추가하고 싶은 기능

  • 가계부 카테고리별 필터 추가
    • 버튼으로 이용 또는 선택지에서 선택하게 함
    • 특정 기간만 필터링하는 기능 추가
  • 현재 만든 그래프 디자인 손보기
    • 금액나오게 추가할까 고민
  • Celery 이용 고민

검토 후 해야할 일

  • 메인페이지 위에 분석 요약 작게 만들어 놓기

  • 분석할게 없을경우 나올 화면 만들기


1. 필터 구현

  • 이미 코드구현은 되어있는 상태
    • DRFView에는 구현되어 있으나 HTMLView에는 구현이 안되있는 상태
    • 즉, API 화면에서는 필터 적용 가능하지만, HTML 템플릿에서는 별도 처리 없어서 작동 X
# transaction/view_html.py
@login_required
def transaction_html_view(request):
    qs = Transaction.objects.filter(user=request.user)

    # 카테고리 필터
    category = request.GET.get("category")
    if category:
        qs = qs.filter(category=category)

    # 기간 필터
    start = request.GET.get("start")
    end = request.GET.get("end")

    if start:
        qs = qs.filter(transacted_at__date__gte=start)
    if end:
        qs = qs.filter(transacted_at__date__lte=end)

    qs = qs.order_by('-created_at')

    return render(request, "transactions/transaction_list.html", {
        "transactions": qs,
    })
    
# templates/transaction/transaction_list.html
<!-- 필터기능 추가 -->
<div class="card shadow-sm p-3 mb-4">
  <form method="GET" class="row g-3">

    <!-- 카테고리 -->
    <div class="col-md-3">
      <label class="form-label fw-semibold">카테고리</label>
      <select name="category" class="form-select">
        <option value="">전체</option>
        <option value="food" {% if request.GET.category == "food" %}selected{% endif %}>식비</option>
        <option value="transport" {% if request.GET.category == "transport" %}selected{% endif %}>교통</option>
        <option value="shopping" {% if request.GET.category == "shopping" %}selected{% endif %}>쇼핑</option>
        <option value="income" {% if request.GET.category == "income" %}selected{% endif %}>수입</option>
        <option value="etc" {% if request.GET.category == "etc" %}selected{% endif %}>기타</option>
      </select>
    </div>

    <!-- 시작 날짜 -->
    <div class="col-md-3">
      <label class="form-label fw-semibold">시작 날짜</label>
      <input type="date"
             name="start"
             class="form-control"
             value="{{ request.GET.start }}">
    </div>

    <!-- 종료 날짜 -->
    <div class="col-md-3">
      <label class="form-label fw-semibold">종료 날짜</label>
      <input type="date"
             name="end"
             class="form-control"
             value="{{ request.GET.end }}">
    </div>

    <!-- 버튼 -->
    <div class="col-md-3 d-flex align-items-end">
      <button class="btn btn-dark w-100 py-2">
        🔍 필터 적용
      </button>
    </div>

  </form>
</div>

2. Celery

  • 비동기 작업 및 작업 대기열을 관리하는 Python 기반의 분산 메시지 큐 시스템

  • 주로 백그라운드 작업 처리, 비동기 작업 처리, 그리고 스케줄링된 작업을 처리하는 데 사용

  • Celery는 분산 시스템이기 때문에 여러 노드에서 동시에 작업을 처리할 수 있어 확장성이 뛰어나며,

    • 주로 웹 애플리케이션에서 장시간 소요되는 작업을 백그라운드에서 처리하는 데 자주 사용됨
  • 작동 요약

      1. 클라이언트가 작업을 생성하고 브로커에 전송
      1. 브로커는 받은 작업을 큐에 저장
      1. 워커는 브로커의 큐를 모니터링하고 있다가 새 작업이 들어오면 가져감
      1. 워커가 작업을 실행
      1. 작업 결과는 결과 백엔드에 저장(설정시)
      1. 클라이언트는 필요애ㅔ 따라 결과 백엔드에서 작업 상태나 결과를 조회

  • 주요기능

      1. 비동기 작업 처리
      • Celery는 작업을 비동기로 처리 가능
      • 즉, 클라이언트가 요청을 보내면 즉시 응답을 반환하고, 작업은 백그라운드에서 처리됨
        • 이로 인해 사용자는 작업이 끝날 때까지 기다릴 필요가 없음
      1. 예약된 작업 처리 (Periodic Task)
      • Celery는 작업을 일정 시간 간격으로 실행 가능
      • 이 기능을 위해 celery beat라는 스케줄러가 필요
        • 예를 들어, 매일 자정에 데이터를 백업하는 작업을 예약 가능
      1. 확장성 (Scalability)
      • Celery는 매우 높은 확장성을 제공
      • 여러 대의 워커가 동시에 작업을 처리할 수 있으며, 필요에 따라 워커를 추가해 작업 처리 능력을 확장
      1. 내결함성 (Fault Tolerance)
      • Celery는 작업 실패에 대비해 재시도 기능을 제공함
      • 만약 작업이 실패하면 일정 시간 후 다시 시도하게 할 수 있음
      1. 타임아웃 설정
      • Celery는 작업에 시간 제한을 설정할 수 있어,
        • 특정 시간이 초과되면 작업을 중단하거나 실패 처리할 수 있음

  • 아키텍쳐

    • 프로듀서 (Producer)
      • 프로듀서는 작업을 생성하고, 이를 대기열에 넣는 역할
      • Django, Flask 같은 웹 애플리케이션이 주로 프로듀서 역할을 하며,
        • 사용자가 요청한 작업을 Celery로 보내 대기열에 등록
    • 브로커 (Broker)
      • Celery가 사용하는 메시지 브로커는 작업을 대기열에 저장, 워커가 이 작업을 가져갈 수 있게 관리
      • RedisRabbitMQ가 이 역할을 자주 수행
    • 컨슈머 (Consumer, 워커)
      • 워커는 대기열에 있는 작업을 가져가서 실행하는 역할
      • 워커는 여러 개의 프로세스나 머신에서 실행될 수 있으며, 이를 통해 동시에 많은 작업을 처리 가능
    • 결과 백엔드 (Result Backend)
      • 작업이 완료되면 결과를 저장하는 시스템으로, 클라이언트가 요청한 작업의 결과를 추적할 수 있게 해줌

  • 예시

      1. 예약된 작업 처리
      from celery.schedules import crontab
      
      app.conf.beat_schedule = {
          'add-every-midnight': {
              'task': 'tasks.add',
              'schedule': crontab(minute=0, hour=0),
              'args': (16, 16),
          },
      }
      1. 기본 설정
      from celery import Celery
      
      app = Celery('my_project', broker='redis://localhost:6379/0',
      backend='redis://localhost:6379/0')
      
      @app.task
      def add(x, y):
          return x + y
    1. 작업 호출
      result = add.delay(4, 6)

  • Celery 를 사용해야 할 때

    • 웹 애플리케이션에서 시간이 오래 걸리는 작업을 백그라운드에서 처리하고 싶을 때
      • (예: 이미지 처리, 이메일 전송)
    • 주기적으로 실행되어야 하는 작업을 자동화할 때
      • (예: 정기 백업, 데이터 수집)
    • 여러 서버에서 동시에 작업을 분산 처리하고 싶을 때

  • Celery 와 함께 자주 사용하는 도구

    • Redis 또는 RabbitMQ:
      • 메시지 브로커로 자주 사용됨
    • Flower
      • Celery의 실시간 모니터링 도구로, 작업 상태를 시각적으로 관리
    • Django
      • Celery는 Django 프로젝트와 매우 잘 통합되어 백그라운드 작업 처리에 자주 사용됨

  • 장점

    • 분산 시스템으로 확장 가능
    • 다양한 백엔드 및 메시지 브로커 지원
    • 비동기 및 동기 작업 모두 지원
    • 주기적인 작업 스케줄링 지원
  • 단점

    • 복잡한 설정 및 운영이 필요할 수 있음
    • 작업 실패 시 디버깅이 까다로울 수 있음

django-celery-beat

  • Celery를 이용해 등록한 Task를 스케줄링 작업으로 변환하는 라이브러리
  • Crontab, Periodick Task, Solar Event, Interval 등의 작업 속성 주기를 택하여
    • Task를 선택한 주기에 맞게 수행하도록 함
  • 등록된 스케줄링 작업은 장고 어드민에서 확인 가능하며,
    • 어드민 페이지에서 스케줄링 작업을 생성, 변경, 삭제도 가능

django-celery-results

  • Celery에 등록된 작업 Task의 수행결과를 데이터베이스에 저장
    • 해당 결과는 Django Admin에서 확인 가능

Celery 이용 ⭐️

  • Celery를 이용하여 스케줄링 작업 구현하기

  • poetry add django-celery-beat

    • Python 3.13에서는 django-celery-beat 를 설치 불가
      • django-celery-beat가 내부적으로 사용하는 django-timezone-field 라이브러리가
      • Python <4.0만 지원 -> requires-python = ">=3.13,<4.0" 버전을 명시

Redis

  • brew

    • 설치: brew install redis
    • 실행: brew services start redis
  • docker

    • docker run -d --name redis -p 6379:6379 redis:7

  • 기본 Celery 세팅

  • celery.py 파일을 core/ 에 넣고 init.py에 Celery App 연결한 이유
    • manage.py와 같은 레벨에 있는 디렉토리(= settings.py가 들어있는 디렉토리)에 넣어야 하기 때문
    • Celery가 Django 설정을 로딩하려면
      • os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moneybook.settings")
      • 이렇게 설정을 읽어야 함 -> 그러기 위해 celery.py도 Django 프로젝트 패키지 안에 있어야 함
      • 이렇게 함으로써, Celery가 settings.py를 올바르게 import 가능
      • 즉, Celery가 Django settings.py를 로딩해야 하기 때문에 core/ 에 추가함

  • app_task 와 shared_task의 차이

구분@app.task@shared_task
Celery 앱 필요?필요함 (app 인스턴스 기반)필요 없음 (프로젝트 어디서나 사용 가능)
추천 용도단일 앱 형태의 Celery 구성Django의 여러 앱에서 task 를 사용할 때
장점명시적이고 구조적임Django 프로젝트에서 가장 많이 사용됨
  • 프로젝트 전체에서 Celery 앱을 공유하기 때문에 Django에서는 대부분 @shared_task가 정석

  • Celery 분석 자동 생성 기능 만들기

    1. analysis/tasks.py
from celery import shared_task
from django.contrib.auth import get_user_model
from .analyzers import Analyzer
from .utils import get_daily_range

User = get_user_model()


@shared_task
def create_daily_analysis(user_id):
    """특정 사용자에 대한 DAILY 분석을 자동으로 생성"""
    user = User.objects.get(id=user_id)
    start_date, end_date = get_daily_range()

    analyzer = Analyzer(
        user=user,
        about="TOTAL_SPENDING",
        period_type="DAILY",
        start_date=start_date,
        end_date=end_date,
        description="자동으로 생성된 매일 분석",
        graph_type="CATEGORY",
    )

    result = analyzer.run()
    return f"Analysis created: {result.id}"

    1. Celery worker와 beat 실행하기
    • 다른 터미널 2개 열고 아래 두 개 실행해야 한다
    • 해석

      • Django 서버(runserver)는 웹 요청만 처리한다.
      • Celery는 백그라운드 작업(비동기/스케줄링)을 처리한다.
      • 즉, Celery는 2개의 엔진을 따로 실행해야 한다.
    • 두개의 엔진

      • 1) Worker -> 작업을 실제로 처리하는 사람(일꾼)
        • ex. 분석 생성 / 그래프 이미지 저장 / 이메일 발송 / 무거운 계산
      • 2) Beat -> 스케줄러(일정 관리자)
        • ex. 매일 00:00 자동 분석 생성 / 매시간 데이터 수집 / 매주 보고서 생성
      • 즉, 터미널을 두개 켜야 함
      • celery -A core worker --loglevel=INFO
      • celery -A core beat --loglevel=INFO

    1. Django에서 Celery Task 직접 테스트하기
    • Celery가 잘 동작하는지 먼저 “수동 실행”으로 테스트함

    1. API/HTML 버튼을 Celery 기반으로 바꿔주기 (선택)
    • 현재 "분석 생성" 버튼은 Django에서 바로 analyzer 실행 → 시간이 오래 걸림.
    • Celery로 바꾸면 즉시 응답하고, 백그라운드에서 분석 생성함.
      • 이 기능을 적용하면, 버튼을 누르면 “분석 생성 중…”으로 유지되고,
        • 백그라운드에서 Celery가 그래프 + 분석 저장을 해줌.
from analysis.tasks import create_daily_analysis

class AnalysisCreateView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        task = create_daily_analysis.delay(request.user.id)
        return Response({
            "message": "분석 생성이 요청되었습니다.",
            "task_id": task.id
        })

    1. 자동 스케줄링 등록하기 (진짜 Celery의 핵심 기능)

  • 17:30 분에 테스트 완료
    • 이 기능이 유지되기 위해서는 worker / beat 둘다 켜져있어야 함 (현제는 딱히 유용하지는 않을 듯)

Django Signal

Django 공식문서 - signal


Django Signal ⭐️

  • 특정 이벤트가 발생할 때 다른 부분에서 알림을 받아 추가 작업을 수행할 수 있도록 하는 기능

    • 이 시그널은 이벤트 기반 프로그래밍의 한 형태로,
      • 모델의 저장, 삭제 등의 중요한 작업이 발생할 때 추가적인 로직을 실행 가능
  • 사용 ex.

    • 모델이 저장되거나 삭제될 때 이를 감지하여 다른 작업을 트리거하는 것
    • Django는 이런 이벤트들을 처리하기 위해 여러 내장 시그널을 제공하며,
      • 사용자 정의 시그널을 만들 수도 있음

주요 개념

  • 시그널 (Signal)
    • 시그널은 특정 이벤트가 발생했을 때 발송됨
      • 예를 들어, 모델의 인스턴스가 저장될 때 시그널이 발송될 수 있음
    • Django는 다양한 내장 시그널을 제공하며, 사용자는 이를 직접 정의할 수도 있음
  • 수신기 (Receiver)
    • 수신기는 시그널을 받아 처리하는 함수
      • 시그널이 발송될 때 이를 수신하여 특정 동작을 수행하는 역할을 함
    • 일반적으로 시그널을 정의하고 나면, 이 시그널에 수신기를 등록하여 시그널을 처리
  • 발송자 (Sender)
    • 시그널이 발송될 때, 어떤 객체나 클래스가 시그널을 발송했는지를 나타냄
    • 시그널을 특정 발송자와 연결하여, 그 발송자에서 발생하는 이벤트에 대해서만 시그널을 수신 가능

Django 내장 시그널

  • Django는 몇 가지 내장 시그널을 제공 / 가장 자주 사용되는 시그널 다음 7가지.
      1. pre_save: 모델의 인스턴스가 저장되기 에 호출
      1. post_save: 모델의 인스턴스가 저장된 에 호출
      1. pre_delete: 모델의 인스턴스가 삭제되기 에 호출
      1. post_delete: 모델의 인스턴스가 삭제된 에 호출
      1. m2m_changed: 다대다 관계가 변경될 때 호출
      1. request_started: HTTP 요청이 시작될 때 호출
      1. request_finished: HTTP 요청이 끝났을 때 호출

시그널 사용 방법

1. 시그널 정의 및 수신기 연결

  • 시그널을 사용하기 위해서는 수신기(Receiver)를 정의하고, 이 수신기를 특정 시그널에 연결 필요
  • Django에서 시그널을 정의할 필요는 없으며,
    • 내장 시그널을 사용하거나 필요에 따라 사용자 정의 시그널을 만들 수 있음

2. 수신기 함수 작성

  • 수신기 함수는 시그널이 발송될 때 호출되는 함수
    • 수신기 함수는 반드시 두 개의 인자를 받아야 함 : sender / **kwargs.
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

# 수신기 함수 정의
@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    if created:
        print(f"{instance}이(가) 생성되었습니다!")
    else:
        print(f"{instance}이(가) 수정되었습니다!")

3. 시그널과 수신기 연결

  • 시그널과 수신기를 연결하기 위해 @receiver 데코레이터를 사용 가능
    • 이 데코레이터는 특정 시그널이 발송될 때 해당 수신기 함수를 실행하도록 설정
    • 또는 signals.connect() 메서드를 사용해 시그널과 수신기를 명시적으로 연결도 가능
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

# 시그널과 수신기 연결
@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    # 저장된 후 처리할 로직
    if created:
        print(f"새 인스턴스 {instance}가 생성되었습니다!")

사용자 정의 시그널

  • 내장 시그널 외에도 사용자 정의 시그널을 만들어 특정 이벤트가 발생했을 때 알림을 발송 가능

      1. 시그널 정의
      • Django의 Signal 클래스를 사용하여 사용자 정의 시그널을 정의 가능

        from django.dispatch import Signal
        
        # 사용자 정의 시그널 정의
        my_custom_signal = Signal(providing_args=["arg1", "arg2"])
      1. 수신기 연결
      • 사용자 정의 시그널과 수신기 함수를 연결
      from django.dispatch import receiver
      
      @receiver(my_custom_signal)
      def handle_my_custom_signal(sender, **kwargs):
          print("사용자 정의 시그널이 호출되었습니다!")
          print(f"인자: {kwargs}")
      1. 시그널 발송
      • 특정 이벤트가 발생했을 때 시그널을 발송
      # 시그널 발송
      my_custom_signal.send(sender=None, arg1="Hello", arg2="World")

시그널의 예시

1. 사용자 생성 후 프로필 생성

  • ex. 사용자가 회원가입할 때, 자동으로 프로필을 생성하는 시나리오
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from myapp.models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

2. 모델 삭제 전후 처리

  • 모델이 삭제되기 전 또는 후에 특정 작업을 수행 가능
from django.db.models.signals import pre_delete, post_delete
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(pre_delete, sender=MyModel)
def before_model_delete(sender, instance, **kwargs):
    print(f"{instance}이(가) 삭제되기 전입니다.")

@receiver(post_delete, sender=MyModel)
def after_model_delete(sender, instance, **kwargs):
    print(f"{instance}이(가) 삭제된 후입니다.")

시그널의 장점과 단점

장점

  • 중앙 집중화된 이벤트 처리: 특정 이벤트가 발생할 때마다 다른 곳에서 로직을 실행 가능
  • 코드의 분리: 모델의 로직과 추가 작업을 분리하여 코드의 응집도를 높일 수 있음
  • 확장성: 추가 로직을 별도의 파일에 두어 유지보수와 확장이 용이

단점

  • 디버깅 어려움: 시그널은 이벤트 기반이기 때문에 디버깅이 어려울 수 있음
  • 복잡성 증가: 여러 시그널이 동시에 작동할 때 로직의 흐름을 파악하기 어려울 수 있음
  • 성능 저하: 대규모 애플리케이션에서 시그널이 과도하게 사용되면 성능 문제가 발생 가능


새롭게 알게된 내용 ✅

  • Pyenv -> Poetry

    • pip freeze > requirements.txt
      • 지금 가상환경(venv)에 설치된 패키지 목록을 freeze
      • 프로젝트에서 사용 중인 패키지 목록이 requirements.txt로 정리됨
    • poetry init -n
      • 프로젝트를 Poetry로 초기화 -> -n은 interactive 질문 없이 기본 설정으로 생성.
      • pyproject.toml이 생김
    • poetry add $(cat requirements.txt)
      • Poetry가 이걸 자동으로 해석해서 패키지 의존성을 맞춰줌
      • requirements.txt에 있는 모든 패키지를 poetry 환경에 재설치
    • poetry config virtualenvs.in-project true
      • Poetry 가상환경을 프로젝트 안에 직접 생성하도록 설정
      • 이제 .venv/ 폴더가 프로젝트 안에 생김
    • poetry shell
      • Poetry 환경 활성화
  • pyenv 가상환경 끄기

    • rm .python-version -> .python-version 삭제
    • pyenv global 확인 -> system 나오면 됨
  • Poetry 가상환경 재생성

    • poetry env remove --all -> 기존 env 제거
    • poetry env use ~/.pyenv/versions/3.13.1/bin/python
      • Poetry에서 pyenv Python 지정
    • poetry install -> 프로젝트에 .venv 생성

  • 프로젝트 패키지는?

    • settings.py가 들어 있는 폴더
    • Celery는 Django 설정을 읽기 위해 DJANGO_SETTINGS_MODULE = "core.settings" 사용
      • 그러려면 Celery app 파일도 같은 패키지 안에 있어야 한다.

어려운 내용(추가 학습 필요) ✅

오늘 발생한 문제(발생 했다면) ✅

  • 가상환경을 Poetry로 전환하는데 안됨
    • cat ~/.zshrc 사용시
      • eval "(pyenvinit)"/eval"(pyenv init -)" / eval "(pyenv virtualenv-init -)" 존재 시
        • pyenv가 PATH를 덮어씀
        • Python 실행 파일을 “shims/python”으로 강제 설정
        • pyenv가 감지하는 로컬 버전이 “system” → system도 shims/system을 의미
        • 결국 어떤 작업을 해도 shims/python만 잡히는 상태
        • 즉, 어떤 디렉토리에 있어도 무조건 pyenv/shims/python이 잡히도록 설정된 상태
      • 결과적으로 생기는 문재
        • deactivate 안 먹힘 / .python-version 삭제해도 안 먹힘
        • which python 계속 shims/python / Poetry env use 안 됨
    • 해결방법

      • nano ~/.zshrc -> 문제부분 주석처리
      • source ~/.zshrc -> 터미널 리로드
        • 여기서도 해결 안될경우
      • cat ~/.zprofile -> 확인 -> 여기는 문제없음
      • cat ~/.zshenv
      • echo $PATH
        • /opt/homebrew/Cellar/pyenv-virtualenv/1.2.4/shims
        • /Users/admin/.pyenv/shims 이 두개가 PATH의 맨 앞에 있는게 문제
    • 해결시도 2

      • System-level 설정 제거
        • cat /etc/zshrc -> X
        • ~/.profile -> X
        • ~/.bash_profile ->
        • ~/.bashrc
        • /etc/profile
        • /etc/bashrc or /etc/bash.bashrc
    • 해결시도 3

      • 싹다 없음
        • PATH 강제 리셋 후 새 PATH 재적용
profile
안녕하세요.

0개의 댓글