2026/03/24 Blog - 34

김기훈·2026년 3월 24일

TIL

목록 보기
174/194
post-thumbnail

코딩테스트(2420)


최적화 진행


Post

다른 대안

  • 소프트 딜리트 관리 대안 (커스텀 매니저 활용)

    • 현재 방식
      • 삭제된 글을 제외해야 하는 뷰나 서비스에서
      • 매번 deleted_at__isnull=True 조건을 수동으로 붙여줘야 할 가능성이 있음
    • 대안
      • 장고의 models.Manager를 상속받아 ActivePostManager를 만들고
      • 기본 매니저(objects)가 항상 deleted_at__isnull=True인 쿼리셋만 반환하도록 재정의
      • 삭제된 글은 all_objects = models.Manager() 같은
        • 별도 매니저로만 접근하게 하면 휴먼 에러를 막을 수 있음
  • 태그 관계 설계의 대안 (비정형 데이터 활용)

    • 현재 방식
      • Tag 테이블과 PostTag 중간 테이블을 생성하여 정통적인 RDBMS의 M:N 관계를 구현
    • 대안
      • PostgreSQL을 사용 중이라면 ArrayField(models.CharField())를 사용하거나
      • 범용적인 JSONField를 사용하여 태그 문자열 배열을 하나의 컬럼에 직접 저장 가능
      • 태그별 통계가 매우 중요하지 않다면
        • 중간 테이블을 없애 조인 비용을 줄이고 쓰기 속도를 극대화 가능
  • 좋아요 카운트 관리 방식의 대안 (역정규화)

    • 현재 방식
      • get_global_posts 등에서 Count("likes")를 사용하여
        • 매번 DB 조인을 통해 좋아요 개수를 동적으로 계산
    • 대안
      • 트래픽이 커질 경우 쿼리 부하가 심해질 수 있음
        • Post 모델 자체에 like_count라는 정수형 컬럼을 추가하고
          • 좋아요가 생성/삭제될 때마다 장고의 F 객체(F('like_count') + 1)를 사용해
          • 값을 업데이트(역정규화)하는 방식으로 읽기 성능을 극대화 가능
  • 검색(Search) 구현의 대안 (전문 검색 엔진 도입)

    • 현재 방식
      • RDBMS의 LIKE %keyword% 연산(장고의 icontains)을 사용
    • 대안
      • 데이터가 많아지면 icontains는 인덱스를 타지 못해 성능이 급격히 저하
      • PostgreSQL의 SearchVector를 활용한 Full-Text Search를 도입하거나,
      • ElasticSearch 같은 외부 전문 검색 엔진을 도입하여
        • 형태소 분석 및 초고속 검색을 지원하도록 개선 가능

개선사항

  • 구조 및 가독성 개선: 뷰(View)의 중복 코드 제거 ✅

    • 이슈
      • post_api.py의 PostAPIView와 MyPostAPIView를 보면
        • 쿼리 파라미터(series, tag, search)를 파싱하고
        • 페이징 객체(paginator)를 다루는 코드가 완전히 중복
    • 개선 가이드
      • DRF의 GenericAPIView 또는 ViewSet을 상속받도록 리팩토링하고
        • django-filter 라이브러리를 도입하면
          • 파라미터 파싱 및 페이징 코드를 직접 작성할 필요 없이
          • 한두 줄의 클래스 속성 선언으로 가독성을 대폭 향상시킬 수 있음
  • 성능 및 로직 개선: 자동 요약(Summary) 기능의 한계 ✅

    • 이슈
      • create_post에서 요약이 없을 때 content[:150]으로 단순 문자열 자르기를 시도함
      • 만약 content가 HTML 태그를 포함한 에디터 데이터라면
        • 중간에 <div> 같은 태그가 잘리면서 프론트엔드에서 렌더링 레이아웃이 깨질 위험이 있음
    • 개선 가이드
      • 장고의 내장 유틸리티인 django.utils.html.strip_tags를 사용하여
      • 본문에서 순수 텍스트만 추출한 뒤 150자로 자르도록 수정하는 것을 권장
  • 안정성 개선: get_or_create의 예외 처리 부족 ✅

    • 이슈
      • add_post_like 서비스에서
        • Like.objects.get_or_create(post=post, user=user)를 호출
      • 만약 아주 짧은 순간에 동일한 유저가 여러 번 클릭을 보내면(동시성 이슈)
        • DB의 UniqueConstraint에 의해 장고에서 IntegrityError가 발생하며
        • 서버가 500 에러를 뱉을 수 있음
    • 해결 가이드
      • 예외를 안전하게 잡아 클라이언트에게 적절한 400번대 응답을 주도록 처리해야 함
from django.db import IntegrityError

try:
    Like.objects.get_or_create(post=post, user=user)
except IntegrityError:
    # 이미 좋아요가 눌려있는 경우의 안전한 처리
    pass
  • 쿼리 최적화: is_liked_by_user 서브쿼리 위치 조정

    • 이슈
      • get_post_detail에서는 Exists를 통해 좋아요 여부를 확인하고 있음
      • 하지만 피드 목록(get_global_posts 등)을 조회할 때는
        • 유저가 각 게시글에 좋아요를 눌렀는지 알 수 있는 값이 내려가지 않음
        • 이로 인해 프론트엔드에서 목록 화면의 하트 아이콘을 칠해야 할지 말아야 할지 판단하기 어려움
    • 해결 가이드
      • 목록 조회 서비스 함수에도 인증된 유저가 요청을 보낸 경우
        • Exists(is_liked_subquery) 조건을 annotate에 추가하여
        • 목록에서도 자신의 좋아요 여부를 파악할 수 있도록 프론트엔드-백엔드 간의 데이터 규격을 맞추가

Comment

다른 대안

  • 작성자 권한 검증 방식의 대안 (DRF Permission 클래스 활용)

    • 현재 방식
      • 서비스 레이어 내부에서 if comment.user != user: 조건문을 통해 직접 에러를 발생시킴
    • 대안
      • 비즈니스 로직에 도달하기 전, 뷰(View) 단계에서 DRF의
        • 커스텀 퍼미션(BasePermission 상속)인 IsOwnerOrReadOnly 같은
        • 클래스를 만들어 permission_classes에 선언하는 방법 존재
      • 이렇게 하면 권한 체크 로직을 View 계층의 앞단으로 분리 가능
  • 댓글 구조화 대안 (계층형/대댓글 구조)

    • 현재 방식
      • 모든 댓글이 게시글 하나에만 종속되는 1차원적인 '평면(Flat) 구조'
    • 대안
      • 모델에 자기 자신을 참조하는
        • parent = models.ForeignKey('self', null=True) 필드를 추가
        • django-mptt 라이브러리를 도입하여
          • '대댓글(Nested Comments)' 기능을 지원하는 트리(Tree) 구조로 확장 가능

개선 사항

  • 성능 오버헤드 개선: 불필요한 트랜잭션(@transaction.atomic) 제거

    • 이슈
      • update_comment_service.py와 delete_comment_service.py 상단에
        • @transaction.atomic 데코레이터가 선언되어 있음
      • 이 로직들은 단순히 단일 레코드 하나를 save() 하거나 delete() 하는 작업만 수행
      • 장고는 단일 쿼리에 대해 기본적으로 Auto-commit 속성을 통한 원자성을 보장하므로
        • 별도의 트랜잭션 블록을 생성하는 것은 DB 연결 자원을 미세하게 낭비하는 오버헤드가 될 수 있음
    • 해결 가이드
      • 다중 쿼리(여러 테이블을 동시에 수정)가 아니므로
      • 해당 파일들에서 @transaction.atomic 데코레이터를 제거하여 성능을 최적화하는 것을 권장
  • 논리적 무결성 개선: 뷰(View)의 퍼미션(Permission) 설정 수정

    • 이슈
      • 수정과 삭제를 담당하는 CommentManageAPIView의
        • permission_classes가 [IsAuthenticatedOrReadOnly]로 설정되어 있음
      • 이 엔드포인트는 PUT과 DELETE 메서드만 존재하며 읽기(GET) 메서드가 없음
      • 즉, 비로그인 유저가 읽기 용도로 접근할 일이 없는 API
    • 해결 가이드
      • 비로그인 유저의 잘못된 요청이 서비스 레이어까지 도달하여 불필요한 연산을 발생시키지 않도록
        • CommentManageAPIView의 권한을
        • permission_classes = [IsAuthenticated]로 변경하여
        • 진입 단계에서부터 401 Unauthorized 에러로 튕겨내야 함
  • 데이터 보존 정책: 댓글 물리 삭제(Hard Delete)의 한계

    • 이슈
      • 게시글(Post) 시스템은 deleted_at을 활용한 소프트 딜리트(휴지통)를 구현하여
        • 실수로 삭제한 데이터를 복구할 수 있는 반면
        • 댓글 삭제 서비스는 comment.delete()를 호출하여
          • DB에서 데이터를 영구히 날려버리고(Hard Delete) 있음
    • 해결 가이드
      • 사용자 경험의 일관성을 맞추거나 악의적인 댓글 작성 후 삭제(증거 인멸)를 방어하기 위해
      • Comment 모델에도 deleted_at 필드를 추가하여
        • 소프트 딜리트 방식으로 리팩토링하는 것을 권장

Tag

다른 대안

  • 다대다(M:N) 관계 설계의 대안 (PostgreSQL ArrayField 도입)

    • 현재 방식
      • Tag, PostTag, Post 3개의 테이블을 쪼개어 정규화하고, 조인(JOIN)을 통해 관계를 형성
    • 대안
      • 만약 PostgreSQL을 사용한다면 RDBMS의 조인 비용을 아예 없애기 위해
        • Post 모델에 tags = ArrayField(models.CharField(...))를 선언하여
        • 태그들을 문자열 배열 형태로 한 컬럼에 직접 밀어 넣는 반정규화 방식을 취할 수 있음
        • 읽기 속도는 극한으로 빨라지지만, 특정 태그의 이름을 일괄 변경할 때 다소 번거로울 수 있음
  • 태그 카운트(집계) 로직의 대안 (역정규화 방식)

    • 현재 방식
      • API가 호출될 때마다 서비스 함수에서 동적으로 GROUP BY와 COUNT 쿼리를 실행하여 개수를 계산
    • 대안
      • 태그와 게시글이 수십만 개로 늘어나면 매번 카운트를 세는 것이 무거워짐
      • Tag 모델에 post_count라는 정수형 컬럼을 추가하고
        • 글에 태그가 달리거나 삭제될 때마다 post_count의 숫자를 +1, -1 업데이트해 두는 방식
        • 이렇게 하면 조회 API에서 단순히 값을 읽어오기만 하면 되므로 응답 속도가 획기적으로 개선

개선 사항

  • 데이터 무결성 개선: 휴지통(Soft Delete) 게시글 제외 처리

    • 이슈
      • tag_count_service.py에서 태그 수를 집계할 때, 단순히 연결된 글의 개수만 세게 되면
        • 사용자가 휴지통에 버린 글(deleted_at__isnull=False)이나 비공개 글까지
        • 집계에 포함될 우려가 높음
      • 이러면 화면에는 "Django 태그 (5개)"라고 뜨는데
        • 클릭해 보면 공개된 글이 3개밖에 없는 데이터 불일치가 발생함
    • 개선 가이드
      • 집계 쿼리 내부에 filter 조건을 명시하여 '살아있는 공개 글'만 카운트하고
      • 카운트가 0인 태그는 목록에서 제외하는 로직을 추가해야 함
from django.db.models import Count, Q

# 올바른 집계 로직 예시
tags_with_count = Tag.objects.annotate(
    valid_post_count=Count(
        'posts', 
        filter=Q(posts__deleted_at__isnull=True, posts__visibility='PUBLIC')
    )
).filter(valid_post_count__gt=0)  # 카운트가 1 이상인 태그만 반환
  • 성능 최적화: 통계 API 캐싱(Caching) 적용

    • 이슈
      • 태그 클라우드(목록 및 통계)는 블로그 방문자들이 메인 페이지에 접속할 때마다
        • 매우 빈번하게 호출되지만, 매초마다 극적으로 숫자가 변하는 데이터는 아님
        • 매 요청마다 DB에서 무거운 GROUP BY 연산을 수행하는 것은 서버 리소스 낭비
    • 개선 가이드
      • Redis를 활용하여 tag_api.py 혹은 서비스 로직에 장고 캐시를 적용하는 것을 권장
from django.core.cache import cache

# tag_count_service.py 내부
def get_tag_counts():
    tags = cache.get("global_tag_counts")
    if not tags:
        tags = list(Tag.objects.annotate(...))
        cache.set("global_tag_counts", tags, timeout=3600)  # 1시간 동안 캐싱
    return tags
  • 품질 향상: 문자열 정규화 (소문자/여백 처리)

    • 이슈
      • 사용자들은 태그를 입력할 때 "Python", "python", " python " 등 제각각으로 입력
      • 별도의 정규화 과정이 없다면 DB에 이 세 가지가 전혀 다른 태그로 생성되어 통계와 검색 결과가 파편화됨
    • 개선 가이드
      • 태그를 생성하거나 등록하는 서비스 로직의 앞단(또는 시리얼라이저)에서
        • 입력된 태그 문자열의 양옆 공백을 자르고(strip())
        • 모두 소문자로 강제 변환(lower())하는 전처리 파이프라인을 추가

Series

다른 대안

  • 중복 검증 로직의 대안 (App-Level Validation 활용)

    • 현재 방식
      • DB 에러(IntegrityError)를 try-except로 직접 잡아냄
    • 대안
      • DRF의 Serializer 내부에
      • validators = [UniqueTogetherValidator(queryset=Series.objects.all(), fields=['user', 'name'])]를 선언하여
      • 뷰(View)의 is_valid() 단계에서 미리 중복을 걸러내는 방식
      • 이렇게 하면 서비스 레이어에 에러 처리 코드를 두지 않아도 되어 코드가 더 깔끔해질 수 있음
        • 단, 극단적인 동시성 이슈에는 약할 수 있음
  • 삭제 처리 성능 최적화 대안 (Queryset 직접 삭제)

    • 현재 방식
      • 삭제할 때 filter(...).first()로 객체를 DB에서 메모리로 한 번 불러온(SELECT) 뒤, series.delete()(DELETE)를 수행합니다.
    • 대안
      • 객체를 굳이 메모리로 가져올 필요 없이
      • deleted_count, _ = Series.objects.filter(id=series_id, user=user).delete()
        • 한 줄로 처리할 수 있음
        • deleted_count가 0이면 존재하지 않거나 남의 것이므로 에러를 발생시키면 됨
        • 불필요한 SELECT 쿼리 1회를 아낄 수 있는 고성능 튜닝 기법

개선 사항

  • 사용자 경험(UX) 개선: 시리즈 삭제 시의 사이드 이펙트(부작용) 핸들링

    • 이슈
      • 현재 코드는 시리즈를 데이터베이스에서 영구 삭제(Hard Delete)
      • Post 모델에서 시리즈를 FK로 참조하며 on_delete=models.SET_NULL이 설정되어 있다면,
      • 시리즈 삭제 시 해당 시리즈에 속했던 게시글들은 소속을 잃고 흩어지게 됨
    • 해결 가이드
      • 유저가 실수로 시리즈를 삭제하는 것을 방지하기 위해 삭제 전
      • "이 시리즈에 포함된 N개의 게시글이 소속을 잃게 됩니다. 그래도 삭제하시겠습니까?"
        • 같은 프롬프트를 띄울 수 있도록, 삭제 API 응답이나 별도 API를 통해
        • '시리즈에 종속된 게시글의 개수'를 미리 체크해 주는 로직을 추가하는 것을 권장
  • 성능 및 확장성 개선: 페이지네이션(Pagination)의 부재

    • 이슈
      • SeriesAPIView의 get 메서드에는 현재 페이징 처리가 되어있지 않음
      • 만약 유저가 블로그를 오래 운영하여 1,000개의 시리즈를 만들었다면
        • API 호출 한 번에 1,000개의 데이터가 한꺼번에 직렬화되어 내려가므로
        • 서버 부하와 네트워크 지연이 발생
    • 해결 가이드
      • 다른 View(예: PostAPIView)에서 사용한 것처럼
        • pagination_class = SeriesPageNumberPagination 등을 도입하여
        • 한 번에 20~30개씩 잘라서 응답하도록 개선하는 것이 대규모 트래픽 대비에 좋음
  • 논리적 무결성: 반환 타입(Return Type) 일관성 유지

    • 이슈
      • SeriesListSerializer를 보면
        • 응답에 id, name, created_at, updated_at을 포함하고 있음
      • 하지만 시리즈 생성(POST)과 수정(PUT) API에서는 시리얼라이저를 재사용하지 않고
      • {"id": series.id, "message": ...} 형태의 임의의 딕셔너리를 반환하고 있음
    • 해결 가이드
      • 프론트엔드 상태 관리를 용이하게 하기 위해 생성/수정 성공 시에도
      • 저장된 객체의 전체 정보(특히 업데이트된 시간 등)를 내려주는 것이 REST API 표준임
# 생성/수정 API 응답 개선 예시
return Response(
    SeriesListSerializer(updated_series).data, 
    status=status.HTTP_200_OK
)

도입

Post

뷰(View)의 중복 코드 제거

class PostAPIView(APIView):
    """포스트 등록 및 전체 목록 조회를 담당합니다."""
				....
    def get(self, request: Request):
        # 1. URL에서 '?series=숫자' 값을 꺼내옵니다.
        series_id_str = request.query_params.get("series")
        series_id = (
            int(series_id_str) if series_id_str and series_id_str.isdigit() else None
        )

        # 2. URL에서 '?tag=문자열' 값을 꺼내옵니다.
        tag_name = request.query_params.get("tag")

        # 3. URL에서 '?search=검색어' 값을 꺼내옵니다.
        search_keyword = request.query_params.get("search")

        # 4. 서비스 레이어 호출 시 series_id와 tag_name을 함께 전달합니다.
        posts = get_global_posts(
            series_id=series_id, tag_name=tag_name, search_keyword=search_keyword
        )

        # 5. 페이지네이션 적용 후 반환
        paginator = self.pagination_class()
        page = paginator.paginate_queryset(posts, request, view=self)

        if page is not None:
            serializer = PostListSerializer(page, many=True)
            return paginator.get_paginated_response(serializer.data)

        return Response(PostListSerializer(posts, many=True).data)
					
                    ...
                    
class MyPostAPIView(APIView):
    """내 블로그(공개글만) 조회를 담당합니다."""
				...
    def get(self, request: Request):
        # 1. User 타입 지정
        user = cast(User, request.user)

        # 2. URL 파라미터에서 series 값을 꺼내옵니다.
        series_id_str = request.query_params.get("series")
        series_id = (
            int(series_id_str) if series_id_str and series_id_str.isdigit() else None
        )

        # 3. URL 파라미터에서 tag 값을 꺼내옵니다.
        tag_name = request.query_params.get("tag")

        # 4. URL에서 '?search=검색어' 값을 꺼내옵니다.
        search_keyword = request.query_params.get("search")

        # 4. 서비스 레이어 호출 시 시리즈와 태그 조건 전달
        posts = get_my_published_posts(
            user=user,
            series_id=series_id,
            tag_name=tag_name,
            search_keyword=search_keyword,
        )

        # 5. 페이지 네이션 적용 및 응답
        paginator = self.pagination_class()
        page = paginator.paginate_queryset(posts, request, view=self)

        if page is not None:
            return paginator.get_paginated_response(
                PostListSerializer(page, many=True).data
            )

        return Response(PostListSerializer(posts, many=True).data)
        
  • PostListMixin 클래스 생성

from rest_framework.response import Response


class PostListMixin:
    """게시글 목록 조회 시 반복되는 필터링 및 페이지네이션 로직을 분리한 믹스인 클래스입니다."""

    def get_filter_params(self, request):
        """요청(request)에서 쿼리 파라미터를 추출하고 정제하여 딕셔너리 형태로 반환합니다."""

        # 1. URL에서 '?series=숫자' 형태의 값을 문자열로 꺼내옴
        series_id_str = request.query_params.get("series")

        # 2. 값이 존재하고 숫자로만 이루어져 있다면 정수(int)로 변환하고, 아니라면 None을 할당
        series_id = int(series_id_str) if series_id_str and series_id_str.isdigit() else None

        # 3. URL에서 '?tag=문자열' 값을 꺼내옴 (태그 필터링용)
        tag_name = request.query_params.get("tag")

        # 4. URL에서 '?search=검색어' 값을 꺼내옴 (검색 기능용)
        search_keyword = request.query_params.get("search")

        # 5. 추출 및 정제된 파라미터들을 딕셔너리로 묶어서 반환
        # (서비스 레이어 함수 호출 시 **kwargs 형태로 깔끔하게 전달 가능)
        return {
            "series_id": series_id,
            "tag_name": tag_name,
            "search_keyword": search_keyword,
        }

    def get_paginated_response(self, queryset, serializer_class, request, context=None):
        """쿼리셋에 페이지네이션을 적용하고 DRF 규격에 맞는 Response 객체를 반환합니다."""

        # 1. 뷰(View) 클래스에 정의되어 있는 pagination_class를 기반으로 페이지네이터 인스턴스를 생성
        paginator = self.pagination_class()

        # 2. 전체 쿼리셋을 페이징 처리하여, 사용자가 요청한 현재 페이지에 해당하는 데이터만 잘라냄
        page = paginator.paginate_queryset(queryset, request, view=self)

        # 3. 페이징 처리가 성공적으로 이루어졌다면 (데이터가 방대하여 page 단위로 나뉘었다면)
        if page is not None:
            # 4. 잘라낸 페이지 데이터를 전달받은 시리얼라이저를 통해 직렬화 (필요시 context 포함)
            serializer = serializer_class(page, many=True, context=context)

            # 5. DRF의 페이지네이션 양식(count, next, previous, results)이 적용된 최종 응답을 반환
            return paginator.get_paginated_response(serializer.data)

        # 6. 만약 페이징 처리가 설정되지 않았거나 예외적으로 page가 None이라면, 전체 쿼리셋을 직렬화
        serializer = serializer_class(queryset, many=True, context=context)

        # 7. 직렬화된 전체 데이터를 일반 Response 객체로 감싸서 반환
        return Response(serializer.data)
  • 수정 후

class PostAPIView(APIView, PostListMixin):
    """포스트 등록 및 전체 목록 조회를 담당합니다."""

    # 인증 권한과 페이지네이션 클래스 설정은 기존과 동일하게 유지합니다.
    permission_classes = [IsAuthenticatedOrReadOnly]
    pagination_class = PostPageNumberPagination

    @extend_schema(...) # 스키마 설정 생략 (기존과 동일)
    def get(self, request: Request):
        # 1. Mixin의 메서드를 호출하여 복잡했던 쿼리 파라미터 파싱 로직을 단 한 줄로 처리합니다.
        filter_params = self.get_filter_params(request)

        # 2. 서비스 레이어 함수를 호출할 때, 딕셔너리 언패킹(**)을 사용하여 인자를 매우 깔끔하게 전달합니다.
        posts = get_global_posts(**filter_params)

        # 3. Mixin의 페이지네이션 메서드를 호출하여 결과물(Response)을 바로 반환합니다.
        return self.get_paginated_response(
            queryset=posts,                      # 페이징할 대상 데이터
            serializer_class=PostListSerializer, # 직렬화에 사용할 시리얼라이저 클래스
            request=request                      # 현재 요청 객체
        )
        
        				...
                        
class MyPostAPIView(APIView, PostListMixin):
    """내 블로그(공개글만) 조회를 담당합니다."""

    permission_classes = [IsAuthenticated]
    pagination_class = PostPageNumberPagination

    @extend_schema(...) # 스키마 설정 생략 (기존과 동일)
    def get(self, request: Request):
        # 1. User 타입 지정 (기존 유지)
        user = cast(User, request.user)

        # 2. Mixin을 통해 중복되던 쿼리 파라미터 파싱 로직을 한 줄로 대체합니다.
        filter_params = self.get_filter_params(request)

        # 3. 서비스 레이어 호출 시 작성자(user) 인자와 파싱된 파라미터(**filter_params)를 함께 전달합니다.
        posts = get_my_published_posts(user=user, **filter_params)

        # 4. 페이지네이션 처리 및 응답 반환 역시 Mixin을 활용하여 한 줄로 압축합니다.
        return self.get_paginated_response(
            queryset=posts,
            serializer_class=PostListSerializer,
            request=request
        )			
        					...
  • 임시저장 / 휴지통 view 수정

class MyTempAPIView(APIView):
		...

    def get(self, request: Request):
		
        ...

        # 3. 페이지 네이션 적용
        paginator = self.pagination_class()
        page = paginator.paginate_queryset(posts, request, view=self)

        if page is not None:
            return paginator.get_paginated_response(
                PostListSerializer(page, many=True).data
            )

        return Response(PostListSerializer(posts, many=True).data)
        
——————————————————————————————————————[비교]—————————————————————————————————————————
class MyTempAPIView(APIView, PostListMixin):
		...
            
    def get(self, request: Request):
		...

        # 3. 페이지 네이션 적용
        return self.get_paginated_response(
            queryset=posts,
            serializer_class=PostListSerializer,
            request=request
        )
class TrashAPIView(APIView):
	...

    def get(self, request: Request):
		...
        
        # 3. 페이지네이션을 적용하여 응답
        paginator = self.pagination_class()
        page = paginator.paginate_queryset(posts, request, view=self)

        if page is not None:
            serializer = PostListSerializer(
                page, many=True, context={"request": request}
            )
            return paginator.get_paginated_response(serializer.data)

        serializer = PostListSerializer(posts, many=True, context={"request": request})
        return Response(serializer.data)
        
——————————————————————————————————————[비교]—————————————————————————————————————————
class TrashAPIView(APIView, PostListMixin):
			...
            
    def get(self, request: Request):
		...

        # 3. 페이지네이션을 적용하여 응답
        return self.get_paginated_response(
            queryset=posts,
            serializer_class=PostListSerializer,
            request=request,
            context={"request": request}
        )

——————————————————————————————————————[비교]—————————————————————————————————————————

성능 및 로직 개선: 자동 요약(Summary) 기능의 한계

def create_post(*, author: User, validated_data: dict[str, Any]):
	...

    # 3. 요약(summary)이 없을 경우 본문에서 앞부분을 추출하여 저장
    content = validated_data["content"]
    summary = validated_data.get("summary") or content[:150]

	...
    
——————————————————————————————————————[비교]—————————————————————————————————————————
from django.utils.html import strip_tags

def create_post(*, author: User, validated_data: dict[str, Any]):
	...

    # 3. 요약(summary)이 없을 경우 본문에서 앞부분을 추출하여 저장
    content = validated_data["content"]
    
    # 4. strip_tags(content)를 통해 HTML 태그(<p>, <div> 등)를 모두 제거한 순수 텍스트만 추출
    summary = validated_data.get("summary") or strip_tags(content)[:150]
    
    ...

안정성 개선: get_or_create의 예외 처리 부족

def add_post_like(*, post_id: int, user: User) -> None:
				...

    # 2. Like 객체를 가져오거나 생성
    Like.objects.get_or_create(post=post, user=user)
    
——————————————————————————————————————[비교]—————————————————————————————————————————
def add_post_like(*, post_id: int, user: User) -> None:
				...

    # 2. Like 객체를 가져오거나 생성
    try:
        Like.objects.get_or_create(post=post, user=user)
    except IntegrityError:
        # 이미 좋아요가 눌려있는 경우의 안전한 처리
        pass

profile
안녕하세요.

0개의 댓글