Coding Garden - Main

김기훈·2026년 3월 18일

개인프로젝트

목록 보기
7/10
post-thumbnail

프로젝트 기능구현 기간: 2026/02/15 ~ 2026/03/18


일자 별 기능구현


1주차

02/15

  • 로그인 / 회원가입
    • JWT 인증방식 사용

02/16

  • 에러메세지 통합

02/17

  • 프론트 도입 및 포스트 작성 기능 추가

02/18

  • 기존 코드 최적화
  • 게시글 작성의 비즈니스로직 추가 및 기능 최적화

02/19

  • 작성 글 공개/비공개 기능 추가
  • 포스트 작성 플로우 분석 / 목록 조회 플로우 분석
  • 포스트 수정, 삭제 기능 추가

02/20

  • 페이지네이션 도입
  • 한 개의 게시글만 가져오는 기능 추가

02/21

  • 등급 기능 추가(SVG)
  • 깃허브같은 일일 잔디심기 기능 추가
    • 기존 28칸으로 했는데 안이뻐서 그냥 365일로 전환해버렸음

02/22

  • 프론트

    • 홈페이지 프론트 수정
      • 최근 작성글 부분에 기존의 전체 글이 조회되던걸 사용자가 작성한 최근 글이 조회되도록 변경
    • 프론트 페이지네이션 도입
      • 페이지 보여주기 최적화
    • 본문 작성 페이지 'Toast UI Editor'를 사용하여 기능 추가
  • 백엔드

    • 태그에 따른 필터기능 도입
    • "좋아요" 기능 도입
      • 멱등성 / 동시성 / 데이토 정합성 고민
    • 인덱스 처리하여 DB단에서 동시성제어
  • 이미지 처리방식 고민

    • S3 / Presigned URL

2주차

02/23

  • 기존 기능(포스트 조회)에 태그 추가
  • 태그 개수를 세어주는 로직 추가
  • 댓글기능 추가
  • 댓글 조회 기능 추가

02/24 ~ 02/28

  • 개인사정으로 인한 부재

03/01

  • 문제 해결
    • 수정 페이지에 태그 입력창 추가 - F
    • 좋아요 기능 오류
      • "좋아요"를 누른 게시글에 다시 "좋아요"를 눌렀을때 취소되지 않음
  • 기능 개발
    • 댓글 수정 / 댓글 삭제 기능 추가 및 댓글 디자인 도입

3주차

03/02

  • 시리즈 등록 / 조회 기능 구현

03/03

  • 시리즈 수정 / 삭제 기능 구현
  • 게시글 작성시에 시리즈 선택 기능 넣기
  • 게시글 상세조회 창에서 시리즈 보이는 기능 추가

03/04

  • 코딩테스트 내용 정리
  • 기능 추가 고민
    • 오늘의 운세 / 일정관리 기능
  • 구현 고민
    • 이미지 처리방식 고민

03/05

  • 휴지통 기능 구현
    • 휴지통에 있는 포스트 내용 미리보기 기능 추가 - F
  • 태그 필터링 기능 추가
  • 태그개수 카운트 기능 추가

03/06

  • 맞춤법검사 기능 고민
    • 약하게 추가
  • 기능 추가 고민

03/07

  • 검색기능 구현 및 프론트 도입
    • 전체글 검색 / 본인이 작성한 글 검색

03/08

  • 이미지 처리방식 도입 (S3 / Presigned URL 둘다 해봄 )

4주차

03/09

  • env오류 해결
    • 프로젝트 설계 초반에 env가 깃허브에 올라가 있던걸 이미지 처리로 인한 env수정할 때 인지함
  • 유출된 DB비밀번호 수정
  • IAM / Bucket 재발급 및 오류 해결
  • 프론트 이미지 처리 디자인 수정
  • 깃허브 소셜로그인 구현

03/10

  • 디스코드 소셜로그인
  • ai기능 고민
    • 작성 문체 변환기 구현

03/11

  • ai 작성 문체 변환기 토큰값 확장
  • 스트리밍응답 적용
    • 하지만 딱히 성능적으로 좋아진거같지는 않음, 현재가 최선인것같음
    • 2초정도 더 빨리 나오기는 함
  • 본문 이미지 첨부 방식 Presigned URL로 전환
  • 사용자 등급 이미지 전환
  • 잔디 처리방식 전환(365번 -> 1번으로 최적화)

03/12

  • UI전환
    • 등급이미지를 닉네임 옆에 붙이기

03/13

  • 글자수 카운트 기능 추가
  • 자동 임시저장 기능 추가

03/14

  • 원래 프론트에서 관리하던 등급관리 로직을 백엔드로 전환
  • 날짜데이터 중복 제거
  • 진행률 소수점 처리
  • 마이페이지 기능 구현
    • 프로필이미지 구현
  • 소셜과 일반 아이디 닉네임 중복 방지

03/15

  • ai라이브러리 전환
  • 배포준비

5주차

03/16

  • 배포 진행 및 도메인 연결

03/17

  • 회원가입 이메일 인증
  • 비밀번호 찾기 기능 추가
  • 마이페이지에서 닉네임 중복 여부 판단

03/18

  • 서버 상태 모니터링 방법 시도
  • 시리즈 관련 오류 및 애매한 부분 수정
    • 초반 테스트할때 넣었던 안어울리는 이모지 삭제
    • 시리즈에 속해있던 포스트 수정시 시리즈에서 빠지는 문제 해결

03/19

  • 블로그 프로젝트를 진행하면서 작성한 TIL전체 다듬기
  • README작성

03/20

  • 각 기능별 사용 요소 정리 및 포트폴리오 포트폴리오/자소서 수정

03/22~23

  • 쿼리최적화 진행

코드 분석


User ❗️


User ✅

목적

  • Django REST Framework(DRF)를 기반으로 작성
    • 자체 이메일 회원가입 및 JWT 기반 인증
    • 소셜 로그인(GitHub, Discord)
    • 이메일 인증을 통한 비밀번호 찾기
      • 그리고 유저의 활동 기반 통계(잔디밭, 등급) 등을 제공하는 종합적인 사용자 관리 시스템

주요 모듈 및 함수별 역할

model

  • 장고의 기본 유저 모델을 확장한 User 모델을 정의
  • 아이디 대신 이메일을 로그인 식별자로 사용하며, 닉네임, 프로필 이미지 등을 관리

view

  • DRF의 APIView를 상속받아 클라이언트(프론트엔드)의 HTTP 요청을 처리
  • 로그인(login.py), 회원가입(signup.py), 소셜 로그인 콜백(social_login.py) 등
    • 엔드포인트별로 파일이 모듈화

service

  • View에서 복잡한 로직을 덜어내어 수행하는 '서비스 레이어'
    • 이메일 인증번호 발송 및 Redis 저장(email_service.py)
    • 외부 OAuth 토큰 요청 및 고유 닉네임 생성(social_login_service.py)
    • 유저의 게시글 수 기반 등급 계산(users_stat_service.py) 등의 핵심 동작을 수행

serializer

  • 클라이언트로부터 들어온 JSON 데이터를 검증(이메일 중복 체크 등)하거나
    • 파이썬 객체를 다시 JSON으로 변환하여 프론트엔드에 응답하기 위한 틀 역할을 수행

핵심 기법

현재 적용

  • 서비스 레이어 패턴 (Service Layer Pattern)

    • 비즈니스 로직을 views.py에 작성하지 않고 services 디렉토리 하위의
      • 클래스/메서드(예: UserService.authenticate_user())로 분리
  • Redis를 활용한 TTL(Time-To-Live) 캐싱 기법

    • 이메일 인증번호나 비밀번호 초기화 코드를 RDBMS에 저장하지 않고
    • Django 캐시(Redis)에 300초(5분) 설정과 함께 cache.set(key, code, timeout=300)으로 보관
  • 데이터베이스 트랜잭션(transaction.atomic)

    • 소셜 로그인 연동 시, 새로운 유저를 생성하는 쿼리와 SocialAccount를 생성하는 쿼리를
      • 하나의 트랜잭션 블록으로 묶어
      • 중간에 에러가 발생하면 전체를 롤백(Rollback)하도록 데이터 무결성을 보장

현재 방법 장점

  • 메모리 사용량 및 DB 부하 최적화

    • 인증번호 저장을 위해 RDBMS 대안을 사용했다면
      • 생성되고 버려지는 일회성 데이터(인증코드) 때문에 DB에 잦은 쓰기/삭제 I/O가 발생하고
      • 스케줄러를 통한 만료 데이터 청소 로직이 추가로 필요했을 것
    • 현재의 Redis 채택으로 인하여
      • I/O 병목을 없애고 애플리케이션 코드를 획기적으로 줄였음
  • 유지보수와 단일 책임 원칙(SRP) 준수

    • 서비스 레이어 분리를 통해 View는 오직 '요청 데이터 검증과 응답 반환'에만 집중하게 되었음
    • 향후 새로운 인증 방식이나 로직이 추가되더라도 View 코드를 손대지 않고 Service 모듈만
      • 확장하면 되므로 가독성과 유지보수성이 향상됨

ai ❗️


ai ✅

목적

  • Google Gemini API(gemini-2.5-flash 모델)를 활용하여
    • 사용자가 입력한 원본 텍스트를 원하는 특정 문체(IT 전문가, 친근한 리뷰어, 감성 에세이 등)로
    • 실시간 변환하여 제공하는 AI 텍스트 스트리밍 변환 시스템

주요 모듈 및 함수별 역할

prompt

  • AI에게 지시할 각 문체별 페르소나 및 규칙(System Prompt)을
    • 상수 딕셔너리(TONE_MAPPING) 형태로 매핑하여 관리

service

  • convert_text_tone 함수가 핵심 역할
  • 클라이언트의 요청(문체)에 맞는 프롬프트를 찾아 Gemini Client에 주입하고 AI 모델이 생성해내는
    • 텍스트 조각(chunk)들을 yield 키워드를 사용해 제너레이터(Generator) 형태로 반환

view

  • 클라이언트의 변환 요청(JSON)을 받아 유효성을 검사한 뒤 서비스 계층을 호출
    • 이후 반환받은 제너레이터를 Django의 StreamingHttpResponse에 담아
    • 서버-클라이언트 간 실시간 데이터 스트림 방식으로 응답을 전송

핵심 기법

현재 적용

  • 스트리밍 생성 및 Server-Sent Events (SSE) 응답 기법

    • AI 텍스트 생성은 완료까지 수 초 이상 소요될 수 있음
    • 이를 해결하기 위해 모델이 전체 문장을 완성할 때까지 기다리지 않고
      • generate_content_stream과 파이썬의 yield를 사용해 텍스트 조각이
      • 생성되는 즉시 클라이언트로 전송(StreamingHttpResponse)
  • 프롬프트 엔지니어링의 모듈화

    • 비즈니스 로직(Service) 내부에 길고 복잡한 프롬프트 문자열을 하드코딩하지 않고,
      • tone_prompts.py라는 별도 파일로 완전히 분리
  • 웹 서버 버퍼링 우회 (X-Accel-Buffering: no)

    • Nginx 등 리버스 프록시를 사용하는 배포 환경에서
      • 서버가 데이터를 모았다가 보내지 않고 조각이 발생할 때마다
      • 즉시 클라이언트에게 쏘아주도록 HTTP 헤더를 세밀하게 제어

현재 방법 장점

  • 사용자 경험(UX) 극대화 및 타임아웃 방지

    • 동기식 대안을 사용했다면 사용자는 화면이 멈춘 채로 5~10초를 기다려야 하며
      • 브라우저 타임아웃이 발생할 우려가 있음
    • 현재의 스트리밍 기법은 타이핑이 쳐지는 듯한 UI를 프론트엔드에서 구현할 수 있게 하여
      • 체감 대기 시간을 획기적으로 줄여주는 가장 최선의 접근 방식
  • 단일 책임 원칙 준수 및 유지보수성 향상

    • 프롬프트를 분리한 덕분에,
    • 추후 파이썬 코드를 모르는 기획자나 프롬프트 엔지니어도 tone_prompts.py 파일만 수정하여
    • AI의 응답 품질을 튜닝하거나 새로운 문체를 쉽게 추가할 수 있는 유연한 구조가 완성

Post ❗️


Post ✅

목적

  • 게시글 CRUD, 임시저장, 공개 범위 설정, 태그 연동, 소프트 딜리트(휴지통) 기능뿐만 아니라
    • 사용자 간의 상호작용을 위한 '좋아요' 기능과 다중 조건(시리즈, 태그, 검색어) 기반의
    • 최적화된 피드 조회 기능을 제공하는 종합 블로그 게시글 관리 시스템

주요 모듈 및 함수별 역할

model

  • 게시글(Post)의 스키마를 정의
    • 작성자(User), 시리즈(Series)와 외래 키로 연결되며
    • 삭제 시간을 기록하는 deleted_at 필드로 휴지통 기능을 구현
    • 또한 중간 테이블(PostTag)을 명시하여 태그(Tag)와 다대다 관계를 맺음
  • 데이터 모델 - 좋아요
    • 특정 게시글(Post)과 이를 좋아하는 유저(User)를 연결하는 모델
    • UniqueConstraint를 설정하여 한 유저가 동일한 게시글에 여러 번 좋아요를 누를 수 없도록
      • 데이터베이스 레벨에서 중복을 차단

비즈니스 로직 - 생성

  • 게시글을 실제로 DB에 생성하는 로직을 담당
    • 본문을 잘라 자동으로 요약본(summary)을 생성하고
    • 새 태그 생성 및 게시글-태그 간의 연결을 대량(Bulk) 처리하여 쿼리를 최적화

비즈니스 로직 - 휴지통

  • deleted_at 필드가 존재하는 데이터만 필터링하여
    • 휴지통 목록을 조회하거나 복구(restore_trashed_post)
    • 영구 삭제(hard_delete_post)하는 기능을 수행

비즈니스 로직 - 검색 및 목록

  • 전달받은 파라미터를 바탕으로 실질적인 DB 검색을 수행
    • 장고의 Q 객체를 사용하여 제목, 내용, 태그 이름 중 하나라도
    • 검색어가 포함된(icontains) 게시글을 찾는 다중 검색 로직을 담당

비즈니스 로직 - 좋아요

  • 게시글이 삭제(Soft Delete)되지 않고 유효한지 먼저 검증한 뒤
    • Like 객체를 생성(get_or_create)하거나 삭제(delete)하는 역할을 수행

API 엔드포인트(post,like)

  • 클라이언트 요청을 받아 시리즈, 태그, 검색어 기준의 필터링을 수행하고
    • 페이지네이션을 적용하여 전체 포스트 피드 및 내 블로그 글을 응답
    • 게시글 목록 조회 시 URL 쿼리 파라미터(?search=, ?tag=)를 추출하기만 하고
    • 실제 비즈니스 로직은 서비스 계층으로 위임(Delegate)하여 단일 책임 원칙을 철저히 지킴
    • 좋아요 등록/취소 역시 add_post_like, remove_post_like 서비스를 호출하는 역할만 함

핵심 기법

현재 적용

  • 소프트 딜리트 (Soft Delete) 패턴

    • deleted_at 필드를 두어 삭제 요청 시 실제 DB 레코드를 날리지 않고 삭제된 시간만 기록
    • 이를 통해 사용자에게 '휴지통' 기능을 제공하며 데이터 복구를 가능하게 함
  • Bulk Create를 통한 쿼리 최적화

    • 새로운 태그를 생성하고(Tag.objects.bulk_create)
      • 게시글과 태그를 연결할 때(PostTag.objects.bulk_create)
      • 하나씩 save()를 호출하지 않고 한 번의 쿼리로 삽입하여 N+1 문제 및 쓰기 성능 저하를 방지
  • ORM 조인 최적화 (N+1 문제 방지)

    • 휴지통 목록 조회 시 select_related("user", "series")를 사용하여
      • 정방향 외래 키 데이터를 SQL의 JOIN으로 한 번에 가져오고
      • prefetch_related("tags")로 다대다 관계의 태그 데이터를 IN 쿼리로 묶어 가져옴
  • 서비스 레이어에서의 ORM 고급 쿼리 (Q 객체와 distinct)

    • 다중 검색 로직을 구현할 때 Q(title__icontains=...) | Q(...) 형태를 사용하여
      • 복잡한 OR 연산을 처리
    • 또한 태그 이름으로 검색할 때 발생하는 M:N 조인 중복 결과를 제거하기 위해
      • .distinct()를 사용
  • 서브쿼리(Subquery) 및 Exists를 활용한 최적화

    • 게시글을 조회할 때마다 해당 글에 달린 좋아요 수를 Count("likes", distinct=True)로 집계
    • 특히 현재 접속한 유저가 좋아요를 눌렀는지 여부(is_liked_by_user)를
      • 파이썬 반복문이 아닌 DB 단의 Exists(is_liked_subquery)를 통해
      • 가상 필드로 만들어 N+1 문제를 원천 차단
  • 데이터 무결성을 위한 DB 제약조건

    • 앱 레벨(Python)에서만 중복 좋아요를 체크하지 않고,
      • models.UniqueConstraint(fields=["post", "user"])
        • DB 스키마에 직접 적용하여 동시성 이슈(Race Condition)가 발생하더라도
        • 데이터 무결성을 보장하도록 설계

현재 방법 장점

  • 성능과 데이터 정합성 보장

    • create_post 서비스 내에서
      • transaction.atomic 블록과 bulk_create를 함께 사용했음
      • 만약 태그 10개를 등록할 때 하나씩 INSERT 한다면 DB 트랜잭션 오버헤드가 크지만
      • 현재 구조는 단 2~3번의 쿼리로 모든 작업을 완료하여
        • 서버 메모리 사용량 최적화 및 DB 응답 시간 단축이라는 결과를 냄
  • 유지보수성과 역할 분리의 극대화

    • 뷰에 검색 로직과 좋아요 비즈니스 로직을 넣지 않고 서비스 레이어로 완전히 분리
      • 향후 "관리자용 게시글 검색 API" 등을 추가로 만들 때
        • 뷰 코드 재작성 없이 get_global_posts 서비스 함수만 재사용하면 되므로
        • 코드의 재사용성과 테스트 용이성(Testability) 향상

Comment ❗️


Comment ✅

목적

  • 특정 게시글(Post)에 종속되어 사용자들이 의견을 나눌 수 있도록 하는
    • 댓글(Comment)의 생성, 조회, 수정, 삭제(CRUD) 라이프사이클 관리 시스템

주요 모듈 및 함수별 역할

  • 데이터 모델

    • 댓글 내용을 담는 content 필드를 가지며
    • 어떤 게시글에 달렸는지(post), 누가 작성했는지(user)를 외래 키(ForeignKey)로 연결하여 관계 정의
  • 조회 기능 (List)

    • CommentAPIView의 GET 메서드에서 호출
      • 부모 게시글이 삭제(Soft Delete)되지 않고 존재하는지 먼저 검증한 뒤
      • 해당 게시글에 달린 댓글들을 작성일(created_at) 순으로 정렬하고 페이지네이션을 적용하여 반환
  • 생성 기능 (Create)

    • CommentAPIView의 POST 메서드에서 호출
    • 게시글의 유효성을 확인한 후
      • 클라이언트가 보낸 내용(content)과 로그인한 유저 정보를 엮어 새로운 댓글 레코드를 DB에 INSERT
  • 수정 및 삭제 기능 (Update/Delete)

    • CommentManageAPIView에서 담당
    • 두 기능 모두 타인이 임의로 조작하지 못하도록
      • "댓글이 존재하는지"와 "요청한 유저가 실제 댓글 작성자와 일치하는지"를
      • DB 조회 후 애플리케이션 레벨에서 엄격히 검증(if comment.user != user:)

핵심 기법

현재 적용

  • 시리얼라이저 필드 매핑 및 N+1 방지

    • CommentListSerializer를 보면 source="user.nickname"을 통해
      • 작성자의 닉네임과 프로필 이미지를 엮어서 반환
    • 이 과정에서 발생할 수 있는 N+1 쿼리 문제를 막기 위해
      • 조회 서비스에서 select_related("user")를 사용하여
      • SQL의 JOIN 연산으로 유저 데이터를 한 번에 가져오는 최적화를 적용
  • 부분 업데이트 최적화 (update_fields)

    • 댓글 수정 서비스에서 comment.save(update_fields=["content"])를 명시
    • 이는 UPDATE comments SET content=... 처럼 쿼리를 제한하여
      • 변경되지 않은 다른 필드들까지 불필요하게 덮어쓰기 되는 것을 방지하는 좋은 기법

현재 방법 장점

  • 현재의 '서비스 레이어 위임 패턴'과 '게시글 생존 여부 검증(deleted_at__isnull=True)' 방식은
    • 이미 삭제된 휴지통 속 게시글에 유령 댓글이 달리거나 조회되는 데이터 정합성 오류를 원천 차단

Tag ❗️


Tag ✅

목적

  • 이 코드는 게시글을 주제별로 묶어 분류하고 쉽게 검색할 수 있도록 돕는 태그(Tag) 시스템
    • 중간 테이블(Through Model)을 활용한 관계 매핑과
    • 태그별 사용 빈도(Count) 집계 기능을 독립적으로 관리하는 애플리케이션

주요 모듈 및 함수별 역할

  • tag.py 및 post_tag.py (데이터 모델)

    • 고유한 태그 이름(name)을 저장하는 Tag 모델
    • 게시글(Post)과 태그(Tag) 간의 다대다(M:N) 관계를 물리적으로 연결하는
      • 중간 다리 역할의 PostTag 모델을 정의
  • 비즈니스 로직 - 태그 집계

    • 시스템에 등록된 태그들을 조회하면서
      • 각 태그가 현재 몇 개의 게시글에 사용되고 있는지(사용 빈도)를 데이터베이스 레벨에서 집계하여 반환
  • 데이터 직렬화

    • 태그의 기본 정보(이름, ID)와 서비스 레이어에서 계산된 '게시글 카운트' 데이터를
    • 프론트엔드가 요구하는 JSON 규격으로 변환
  • API 엔드포인트

    • 클라이언트의 태그 목록 및 통계 데이터 요청(GET)을 받아 서비스 레이어를 호출하고 결과를 응답

핵심 기법

현재 적용

  • 명시적 중간 테이블 패턴 (Custom Through Model)

    • 장고(Django)의 ManyToManyField가 자동으로 만들어주는 숨겨진 테이블을 사용하지 않고
      • PostTag라는 모델을 명시적으로 분리해 선언함
    • 이는 나중에 "이 태그가 이 글에 언제 달렸는가(created_at)" 같은
      • 추가 메타데이터를 쉽게 확장할 수 있게 하기위한 설계
  • 데이터베이스 레벨 집계 (ORM Annotation)

    • 태그 개수를 파이썬(Python)의 반복문으로 하나씩 세지 않고
      • tag_count_service에서 장고 ORM의 annotate(count=Count(...)) 기능을 사용해
      • DB 엔진 내부에서 그룹화 및 집계 연산을 완료하도록 최적화

현재 방법 장점

  • 단일 책임 원칙과 쓰기 성능 극대화
    • tags 앱을 모델부터 뷰까지 별도로 독립시켰기 때문에 시스템 간 결합도가 낮아졌음
    • 특히 PostTag 중간 테이블을 명시한 덕분에
      • post_create_service.py 로직에서 bulk_create를 활용해
      • N개의 태그를 단 한 번의 쿼리로 매핑하는 성능상 이점을 얻음

Series ❗️


Series ✅

목적

  • 블로그 플랫폼 등에서 사용자가 자신의 게시글들을
    • 특정 주제나 흐름으로 묶을 수 있도록 돕는 '시리즈(Series)'
    • 또는 카테고리의 생성, 조회, 이름 수정, 삭제(CRUD)를 관리하는 시스템

주요 모듈 및 함수별 역할

  • 데이터 모델

    • 시리즈의 소유자(user)와 이름(name)을 저장
    • 핵심적으로 UniqueConstraint를 사용하여
      • 한 명의 유저가 동일한 이름의 시리즈를 중복해서 만들 수 없도록 데이터베이스 레벨에서 제약걸기
  • 조회 기능 (series_list_service.py & SeriesAPIView)

    • 로그인한 유저(user)를 기준으로
      • 해당 유저가 생성한 시리즈 목록만 필터링하여 최신 생성일(-created_at) 순으로 반환
  • 생성 기능 (series_manage_service.py & SeriesAPIView)

    • 사용자가 요청한 이름으로 새 시리즈를 만듬
    • 이때 이름 중복 제약조건을 위반하여 발생하는 데이터베이스 에러(IntegrityError)를 잡아냄
  • 수정 및 삭제 기능 (series_manage_service.py & SeriesDetailAPIView)

    • 타인의 시리즈를 조작할 수 없도록
      • 수정/삭제 전 반드시 id와 user가 모두 일치하는
      • 객체를 먼저 조회하여 강력한 소유권(Ownership) 검증을 수행

핵심 기법

현재 적용

  • DB 무결성 제약과 EAFP 패턴

    • Easier to Ask for Forgiveness than Permission
      • 시리즈 이름의 중복을 막기 위해 애플리케이션 단에서 먼저 exists()로 검사하지 않고
      • DB의 UniqueConstraint를 믿고 무작정 create()를 시도
      • 실패할 경우 IntegrityError 예외를 처리하는 전형적인 파이썬의 EAFP 철학을 따르고
      • 이는 다중 접속 환경에서의 동시성 문제(Race Condition)를 막아냅니다.
  • 안전한 객체 참조 (IDOR 방어)

    • 수정과 삭제 서비스 로직에서 Series.objects.filter(id=series_id, user=user).first() 방식을 사용
    • 악의적인 유저가 다른 유저의 시리즈 ID를 파라미터로 몰래 보내더라도
    • user=user 필터 때문에 객체를 찾지 못하게 되어 안전하게 권한 에러를 뱉어내는 설계
  • 부분 업데이트 최적화

    • 시리즈 이름 변경 시 series.save(update_fields=["name"])를 호출하여
      • 불필요한 필드(예: created_at 등)가 덮어씌워지는 것을 방지하고 쿼리 효율을 높임

현재 방법 장점

  • 높은 보안성과 데이터 정합성 보장
    • 사용자 권한 체크 로직(user=user)과 제약 조건 예외 처리(IntegrityError)를 통해
    • 비즈니스 로직(서비스 레이어)이 외부 공격(조작된 ID 전송)이나 비정상적인 데이터 입력을
      • 자체적으로 방어하는 매우 견고한(Robust) 아키텍처를 완성

쿼리 최적화

  • Django-Silk를 사용하여 최적화 진행

Post

전체 글 조회

# 수정 전 
200 GET
/api/v1/post/
23ms overall
5ms on queries
6 queries

# 수정 후 
200 GET
/api/v1/post/
23ms overall
5ms on queries
3 queries
  • 기존

    • 기존 로직에서는 메인 데이터 조회 외에도 시리얼라이저(Serializer) 단에서
      • 추가적인 쿼리가 지연 평가(Lazy Evaluation)되며 N+1 문제와 불필요한 단일 쿼리가 발생
    • 작성자 등급 계산용 카운트 쿼리 발생
      • PostListSerializer에서 작성자의 등급(Grade)을 계산하기 위해
        • 유저별 총 게시글 수를 구하는 .count() 쿼리가 실행되었음
        • __init__에서 딕셔너리로 캐싱하여 동일 작성자에 대한 중복 쿼리는 막았지만
        • 페이지 내에 등장하는 '새로운 작성자'마다 최소 1번씩의 추가 쿼리가 데이터베이스로 날아가고 있었음
    • Series 정보 조회를 위한 N+1 문제
      • 시리얼라이저에서 source="series.name"을 통해 시리즈를 참조하고 있었으나
        • 메인 쿼리의 select_related 목록에 "series"가 누락
      • 이로 인해 게시글 목록을 순회할 때
        • 각 게시글의 서로 다른 Series 정보를 가져오기 위한 추가 쿼리가 반복적으로 발생
  • 해결

    • Django ORM의 고급 기능(Subquery, OuterRef)과 즉시 로딩(select_related)을 활용하여
      • 애플리케이션(Python) 메모리 단에서 처리하던 로직을 데이터베이스(DB) 단의 메인 쿼리 하나로 병합
    • Subquery와 OuterRef를 활용한 작성자 게시글 수 통합
      • 기존 시리얼라이저의 딕셔너리 캐싱 로직을 완전히 제거
      • 대신 OuterRef를 이용해 메인 쿼리의 user_id와 매칭되는 서브쿼리를 작성
      • 이를 .annotate(author_total_posts=Subquery(...))를 통해 연결
      • 결과적으로 메인 쿼리가 실행될 때
        • 각 유저의 게시글 수를 DB에서 한 번에 계산하여 가져오도록 변경하여 시리얼라이저 단의 N+1을 제거
    • select_related에 Series 추가
      • get_global_posts()의 메인 쿼리셋에
      • .select_related("user", "series") 처럼 "series"를 명시적으로 추가
  • 결과

    • 쿼리 수 50% 감소 (6회 -> 3회)
      • 불필요한 시리얼라이저 N+1 쿼리들이 메인 쿼리로 병합
      • 남은 3회의 쿼리는 필수적인 페이지네이션 카운트, 메인 데이터+서브쿼리 조회, 태그 Prefetch 쿼리
    • 성능 및 확장성 확보
      • 현재 응답 시간(23ms)과 DB 시간(5ms)은 데이터 모수가 적어 극적인 차이가 보이지 않을 수 있음
      • 하지만 트래픽이 많아지고 한 페이지에 노출되는 다양한 작성자와 시리즈가 늘어날수록
        • 기하급수적으로 늘어날 뻔한 DB 부하를 사전에 차단

내 글 조회

# 수정 전
/api/v1/post/my/
28ms overall
6ms on queries
14 queries

# 수정 후(2026/03/21)
/api/v1/post/my/
17ms overall
4ms on queries
5 queries

# 수정 후(2026/03/22)
200 GET
/api/v1/post/my/
31ms overall
4ms on queries
4 queries
  • 1단계: N+1 문제의 발견 (14 쿼리)

    • 시리얼라이저가 10개의 내 게시글을 응답으로 만들면서
      • 각 게시글마다 작성자의 총 게시글 수를 구하는 .count() 쿼리를 10번이나 반복해서 호출(N+1)
  • 2단계: 애플리케이션 단 메모리 캐싱 (5 쿼리)

    • '내 글 조회'의 작성자는 결국 나 자신 1명이라는 점에 착안
    • 시리얼라이저 __init__에 딕셔너리를 만들어 최초 1회만 DB에서 카운트를 가져오고
    • 나머지 9번은 메모리에서 꺼내 쓰도록 캐싱하여 쿼리를 획기적으로 줄였음
    • 이 과정에서 휴지통에 간 글을 제외하는 예외 처리도 추가했음
  • 3단계: 데이터베이스 단 서브쿼리 병합 (4 쿼리)

    • 이후 '전체 글 조회' API를 최적화하면서 시리얼라이저 캐싱 방식을 제거하게 되었고
      • 이 여파로 내 글 조회에서 등급이 '0'으로 누락되는 버그가 발생
    • 이를 해결하기 위해 파이썬 메모리(캐싱)에 의존하는 대신
      • OuterRefSubquery를 사용해 메인 쿼리 실행 시
        • DB 내부에서 카운트까지 한 번에 연산해 오도록 아키텍처를 완전히 통일

임시글 목록

# 수정 전 
/api/v1/post/my/temp/
30ms overall
3ms on queries
7 queries

# 수정 후 
/api/v1/post/my/temp/
28ms overall
2ms on queries
4 queries
  • 원인 (불필요한 연산 및 N+1 문제)

    • 도메인 특성에 맞지 않는 무거운 연산
      • 기존 코드에는 임시글(작성 중인 비공개 글)을 조회함에도 불구하고
      • 좋아요 수를 집계하는 .annotate(likes_count=Count("likes", distinct=True)) 로직이 포함
      • 공개되지 않은 임시글에는 '좋아요'가 달릴 수 없으므로
        • 데이터베이스 입장에서는 불필요한 조인(JOIN)과 그룹화(GROUP BY) 연산을 낭비
    • Series 참조로 인한 N+1 문제
      • 다른 조회 API들과 마찬가지로, 시리얼라이저에서 임시글이 속한 시리즈(series) 정보를 참조할 때
      • 메인 쿼리에 select_related("series")가 누락되어 있어
        • 각 게시글마다 시리즈 정보를 가져오기 위한 추가 쿼리가 발생
  • 해결 (로직 다이어트 및 즉시 로딩)

    • 불필요한 좋아요 집계 로직 제거
      • 도메인 맥락에 맞게 임시글 목록에서는 좋아요를 카운트하는 .annotate(...) 코드를 삭제
    • select_related 명시적 선언
      • 쿼리셋에 .select_related("user", "series")를 추가
        • 메인 게시글을 가져올 때 SQL JOIN을 통해 작성자와 시리즈 정보를 한 번에 묶어서 가져오도록 변경
        • 이를 통해 시리얼라이저 단에서 발생하던 N+1 문제를 원천 차단
  • 결과 (쿼리 다이어트 및 도메인 로직 정교화)

    • 불필요한 쿼리 제거 (7회 -> 4회)
      • N+1 문제를 유발하던 추가 쿼리들이 제거되어 총 쿼리 수가 절반 가까이 줄임
    • 데이터베이스 부하 감소
      • 의미 없는 Count 연산을 제거함으로써
      • DB의 컴퓨팅 리소스 낭비를 막고 쿼리 실행 속도를 미세하게나마 단축(3ms -> 2ms)시킴
    • 코드의 목적 명확화
      • 기술적인 성능 향상뿐만 아니라
      • "임시글에는 좋아요가 필요 없다"는 비즈니스 로직(도메인 규칙)이 코드에 정확히 반영

휴지통 목록

# 수정 전
/api/v1/post/my/trash/
31ms overall
7ms on queries
21 queries

# 수정 후
/api/v1/post/my/trash/
17ms overall
3ms on queries
4 queries
  • 원인 (지연 평가로 인한 N+1 문제 폭발)

    • 시리얼라이저의 지연 평가(Lazy Evaluation)
      • 기존 코드에서는 메인 게시글 10개를 먼저 가져온 뒤
      • 시리얼라이저가 JSON 응답을 만드는 과정에서 추가 데이터를 그때그때 DB에 요청하는 구조
    • N+1 쿼리 연쇄 발생
      • 휴지통에 10개의 글이 있다고 가정할 때
        • 1번 글의 series를 찾기 위해 1번, tags를 찾기 위해 1번 쿼리를 날리는 작업이 10번의 게시글 내내 반복
        • 이로 인해 메인 쿼리 1개에 시리즈 조회 최대 10개, 태그 조회 최대 10개가 더해져 총 21번의 쿼리가 폭주
    • 불필요한 연산
      • 임시글 목록과 마찬가지로, 휴지통에 있는 글에는 불필요한
      • 좋아요 수 집계(.annotate(likes_count=Count("likes")))가 포함되어 있어 DB 리소스를 낭비
  • 해결 (즉시 로딩을 통한 사전 지시 및 다이어트)

    • 불필요한 좋아요 집계 제거
      • 휴지통 목록의 도메인 특성에 맞게 Count("likes") 연산을 제거하여 쿼리를 가볍게 만듬
    • select_related (JOIN 병합)
      • 쿼리셋에 .select_related("user", "series")를 추가하여 DB에게
        • "이따가 시리얼라이저가 유저랑 시리즈 정보도 물어볼 테니
        • 처음부터 JOIN해서 하나의 표로 가져와!"라고 미리 지시
        • 이로 인해 10번 발생하던 시리즈 조회 쿼리가 메인 쿼리 1개로 완벽히 흡수
    • prefetch_related (IN 쿼리 병합)
      • 다대다(M:N) 관계라 JOIN이 까다로운 태그 정보는 .prefetch_related("tags")를 통해 해결
      • DB가 조회된 게시글 ID들을 모아 IN (...) 조건으로 단 1번의 추가 쿼리만 날려 태그 정보를 싹 쓸어옴
  • 결과

    • 쿼리 수 80% 대폭 감소 (21회 -> 4회)
      • 수십 개로 쪼개져 날아가던 쿼리들이 정확히 4개의 필수 쿼리
      • 유저/세션 확인 1개 + 페이지네이션 COUNT 1개 + 메인 및 JOIN 1개 + 태그 IN 조회 1개 로 정리
    • 응답 속도 향상
      • DB에서 소요되는 시간(7ms -> 3ms)은 물론
      • 전체 API 응답 시간(31ms -> 17ms)까지 눈에 띄게 단축되며 쾌적한 성능을 확보

상세조회

# 수정 전
/api/v1/post/104/
20ms overall
3ms on queries
6 queries

# 수정 후
/api/v1/post/104/
21ms overall
5ms on queries
3 queries
  • 원인 (시리얼라이저에 숨어있던 3가지 쿼리 낭비)

    • 목록 조회가 아닌 단일 상세 조회임에도 불구하고
      • 시리얼라이저가 응답 데이터를 조립하는 과정에서 3개의 불필요한 추가 쿼리가 발생
    • [낭비 1] Series N+1
      • 시리얼라이저에서 series.name을 요구하지만
      • 서비스 레이어의 메인 쿼리에 select_related("series")가 누락되어 있어
      • DB에 시리즈 정보를 다시 묻는 쿼리가 발생
    • [낭비 2] 작성자 총 게시글 카운트
      • 작성자의 등급(Grade) 이미지를 계산하기 위해 get_author_grade_image 메서드 내부에서
      • Post.objects.filter(...).count()가 매번 실행되고 있었음
    • [낭비 3] 좋아요 여부(is_liked) 확인
      • 현재 접속한 유저가 이 글에 좋아요를 눌렀는지 확인하기 위해 get_is_liked 메서드 내부에서
      • obj.likes.filter(...).exists()가 실행되며 또다시 DB와 통신하고 있었음
  • 해결 (Subquery와 Exists를 활용한 연산 위임)

    • 뷰(View)에서 현재 요청을 보낸 유저(request.user) 정보를 서비스 레이어로 넘겨주고
      • 시리얼라이저의 연산 로직을 모두 DB 단의 쿼리로 끌어내림
    • select_related("series") 추가
      • 메인 쿼리에 시리즈를 JOIN하여 [낭비 1]을 해결
    • Subquery 도입
      • 작성자의 총 게시글 수를 구하는 서브쿼리를 작성하고
      • .annotate(author_total_posts=Subquery(...))로 결합하여 [낭비 2]를 해결
    • Exists 서브쿼리 도입
      • 뷰에서 넘겨받은 user 객체를 활용해 현재 게시글에 해당 유저의 좋아요가 존재하는지 확인하는 로직을
      • .annotate(is_liked_by_user=Exists(...))로 결합하여 [낭비 3]을 해결
    • 시리얼라이저 다이어트
      • 결과적으로 시리얼라이저는 DB를 다시 찌르지 않고
      • getattr을 사용해 메인 쿼리가 미리 계산해서 붙여준 값을 읽기만 하도록 역할을 축소
  • 결과 (완벽한 구조 통일 및 쿼리 최소화)

    • 쿼리 수 50% 감소 (6회 -> 3회)
      • 유저 검증, 메인+서브쿼리 1회, 태그 Prefetch 1회라는 필수적인 3개의 쿼리만 남기고
      • 시리얼라이저 잉여 쿼리를 모두 제거
    • DB와 Application의 역할 분리
      • 복잡한 연산은 데이터베이스가(DB 쿼리), 응답 포맷팅은 애플리케이션이(시리얼라이저) 담당하도록
      • 아키텍처의 책임과 역할을 명확히 분리
    • 성능 지표에 대한 이해
      • 최적화 후 로컬 응답 시간(20ms -> 21ms)과 DB 시간(3ms -> 5ms)이 미세하게 늘거나 비슷해 보일 수 있음
      • 이는 서브쿼리가 추가되면서 단일 쿼리의 무게가 살짝 무거워졌기 때문
      • 하지만, 네트워크 통신 횟수(쿼리 개수)를 절반으로 줄였기 때문에
        • 실제 운영 환경(Production)에서 DB와 웹 서버 간의 네트워크 지연(Latency)이 발생할 경우
        • 훨씬 더 안정적이고 압도적인 성능 이점을 가짐

의도


Post

Like

  • DB의 UniqueConstraint를 활용하는 상황에서는 명시적인 락(Lock)을 걸지 않아도 완벽하게 안전
    • 현재 Like 모델에 ['post', 'user'] 조합으로 유니크 제약조건을 걸었고
    • add_post_like에서 get_or_createIntegrityError 예외 처리를 통해 방어
    • 사용자가 아무리 빠르게 '좋아요' 버튼을 여러 번(따닥!) 누르더라도
      • 데이터베이스 자체가 두 번째 인서트부터는 무조건 에러를 뱉어내고 튕겨내기 때문에 데이터 무결성이 보장
  • DB 유니크 제약조건(Constraint) 활용

    • 이 방식은 "일단 넣어보고, DB가 안 된다고 하면(IntegrityError) 무시하자!"라는 낙관적인 접근
      • 장점 (Pros)
        • 최고의 성능: 락을 획득하고 해제하는 오버헤드가 없어서 응답 속도가 매우 빠름
        • 데드락(교착 상태) 위험 제로
          • 트랜잭션들이 서로 자원을 기다리며 멈추는 데드락이 원천적으로 발생하지 않음
        • 높은 동시성 처리량: 여러 사용자가 동시에 요청을 보내도 병목 없이 처리됨
      • 단점 (Cons)
        • 충돌이 발생했을 때 예외(IntegrityError)를 발생시키고
          • 이를 캐치하는 과정이 일반적인 로직 흐름보다는 서버 자원을 미세하게 더 사용함
          • 하지만 '좋아요 따닥' 같은 케이스는 빈도가 낮아 전혀 문제 되지 않음
  • DB 락 (Locking) 방식: 비관적 락 (select_for_update)

    • 이 방식은 "누군가 이 게시글의 좋아요 데이터를 건드리고 있으면
      • 끝날 때까지 다른 사람들은 줄 서서 기다려!"라는 엄격한 접근
    • 장점 (Pros)
      • 데이터의 상태를 완벽하게 통제 가능
      • 데이터를 읽고, 복잡한 검증을 거친 후 업데이트해야 하는 복잡한 로직에서 필수적
    • 단점 (Cons)
      • 성능 저하 및 병목
        • 트래픽이 몰릴 때 락을 얻기 위해 대기하는 시간이 길어져 전체 서버 응답이 느려질 수 있음
      • 데드락 위험
        • 락을 잡는 순서가 꼬이면 시스템이 멈추는 데드락이 발생할 위험이 있음
  • 락(Lock)이 필요한 순간

    • 나중에 조회 성능을 극대화하기 위해
      • Post 모델에 likes_count = models.IntegerField(default=0) 필드를 추가(역정규화)
      • 이때는 A 사용자와 B 사용자가 동시에 좋아요를 누를 때
        • 기존 likes_count를 읽어와서 +1을 하는 과정에서 동시성 문제(Lost Update)가 발생할 수 있음
        • 이런 경우에만 락(select_for_update)이나 DB 원자적 연산(F())이 필요해짐

개선사항


개선 사항

User

  • 보안성 개선: 무차별 대입 공격(Brute-Force) 방어 로직 부재 ✅

    • 이슈
      • 현재 코드는 이메일 인증번호 발송 API나 로그인 API에 대해
        • 재시도 횟수 제한(Rate Limiting)이 존재하지 않음
      • 악의적인 봇이 짧은 시간에 수만 건의 로그인 시도나 이메일 발송을 요청할 경우
        • 서버 다운이나 SMTP 쿼터 초과가 발생할 수 있음
    • 해결 가이드
      • DRF에서 기본 제공하는 Throttling 기능을 도입 추천
  • 가독성 및 예외 처리 구체화 ✅

    • 이슈
      • social_login_service.py에서 requests.post()를 통해
      • 외부 서버와 통신할 때 네트워크 오류나 HTTP 상태 코드 에러에 대한 방어가 적음
    • 해결 가이드
      • requests 라이브러리의 raise_for_status() 메서드나 Timeout 설정을 명시적으로 추가하여
      • 외부 API 장애가 우리 서버의 무한 대기(Hang) 현상으로 이어지지 않도록 방어 로직을 리팩토링 하기
  • 성능 최적화: 이메일 발송 로직의 비동기(Asynchronous) 처리

    • 이슈
      • password_service.py와 email_service.py의 send_mail(...) 함수가 동기적으로 동작함
      • 이메일을 발송하기 위해 구글 SMTP 서버와 통신하는 시간 동안
        • 사용자는 회원가입이나 메일 발송 API 응답을 하염없이 기다려야 함 (네트워크 지연 발생)
    • 해결 가이드
      • Celery + Redis (또는 장고 최신 버전의 async/await 기능)를 도입하여
        • 이메일 발송 작업을 백그라운드 워커(Worker)로 넘기기

AI

  • 보안 및 비용 최적화: 입력값 길이 제한과 Rate Limiting 부재 ✅

    • 이슈
      • 외부 LLM API(Gemini)는 요청한 텍스트의 길이에 비례하여 비용이 발생
      • 현재 뷰에서는 사용자가 보낸 text의 길이를 제한하지 않으므로
        • 악의적으로 수만 자의 텍스트를 요청하여 API 비용을 폭증시키거나
          • 쿼터를 고갈시킬 수 있음
          • 또한 무차별 요청(Rate Limiting)에 대한 방어도 없음
    • 개선 가이드
      • 단순 딕셔너리(request.data.get) 추출 대신 DRF의 Serializer를 도입하여
        • serializers.CharField(max_length=2000) 등으로
        • 요청 텍스트의 최대 길이를 반드시 제한 필요
      • 해당 View에 throttle_classes = [UserRateThrottle, AnonRateThrottle]
        • 추가하여 하루/분당 API 호출 횟수를 제한 필요
  • 성능 최적화: SDK Client 인스턴스 재사용 ✅

    • 이슈
      • openai_service.py를 보면 convert_text_tone 함수가 호출될 때마다
        • 매번 client = genai.Client(...) 객체를 새롭게 생성하고 있음
    • 개선 가이드
      • 클라이언트 초기화 비용을 줄이기 위해
        • 파일 최상단(글로벌 영역)에서 client를 한 번만 초기화해 두고
          • 여러 요청이 이를 재사용하도록 수정하는 것이 성능상 유리
          • (만약 해당 SDK가 멀티스레드 환경에서 Thread-Safe 하다는 전제 하에 적용)
  • 안정성 개선: 스트리밍 중 예외 처리의 한계 ✅

    • 이슈
      • ToneConverterAPIView의 try-except 블록은
        • 초기 연결 실패 시에는 400, 500 에러를 잘 반환함
      • 하지만 StreamingHttpResponse가 이미 시작되어
        • 클라이언트로 200 OK 상태 코드가 날아간 이후에 텍스트를 생성하다가
        • 오류(예: Google 서버 일시 끊김)가 발생하면
          • 서버는 상태 코드를 500으로 바꿀 수 없고 그냥 연결을 뚝 끊어버리게 됨
    • 개선 가이드
      • 프론트엔드가 중간 에러를 명확히 인지할 수 있도록
      • 순수 텍스트(문자열 바이트)만 yield 하는 대신 구조화된 SSE(Server-Sent Events) 포맷을 사용

Post

  • 구조 및 가독성 개선: 뷰(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

  • 성능 오버헤드 개선: 불필요한 트랜잭션(@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

  • 데이터 무결성 개선: 휴지통(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

  • 사용자 경험(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
)

다른 대안


User

다른 대안

  • 인증번호 상태 관리의 대안 (RDBMS 테이블 활용)

    • 현재 방식
      • Redis의 만료 기능을 사용
    • 대안
      • DB에 EmailVerification 같은 별도 테이블을 만들고 created_at 컬럼을 둠
      • 사용자가 인증번호를 입력할 때
        • 현재 시간과 created_at의 시간 차이가 5분 이내인지 DB 쿼리문으로 직접 계산하여 검증하는 방식
  • 소셜 로그인 연동 방식의 대안 (외부 패키지 도입)

    • 현재 방식
      • requests 라이브러리를 통해 Github, Discord의 API 엔드포인트로 직접 HTTP 통신을 보내어
      • 액세스 토큰과 유저 정보를 파싱
    • 대안
      • 장고 생태계의 표준인 django-allauth와 dj-rest-auth 라이브러리를 도입하여
      • 소셜 로그인 파이프라인 흐름, 콜백 처리, 소셜 모델 연결을 패키지에 위임 가능

AI

다른 대안

  • 스트리밍 대안 (동기식/블로킹 방식):

    • 현재 방식
      • generate_content_stream과 yield를 활용
    • 대안
      • API의 스트리밍 기능을 끄고(generate_content 사용) 서버가 AI 텍스트 생성을 끝까지 기다림
      • 완성된 하나의 큰 문자열을 일반적인 DRF의 Response({"converted_text": ...}) 형태로
        • 한 번에 반환하는 방식
  • 비동기 큐와 웹소켓 (Celery + WebSockets) 활용

    • 현재 방식
      • HTTP 연결을 열어둔 상태로 스트리밍
    • 대안
      • AI 요청 자체를 백그라운드 워커(Celery)로 넘기고
        • HTTP 응답은 즉시 "작업 접수됨"으로 끝냄
      • 이후 워커가 처리를 완료하거나 조각을 생성할 때마다
        • WebSocket이나 Redis Pub/Sub을 통해 클라이언트에게
          • 푸시(Push) 알림을 보내는 방식
          • 대규모 트래픽에서 HTTP 스레드 고갈을 막을 수 있음

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 같은 외부 전문 검색 엔진을 도입하여
        • 형태소 분석 및 초고속 검색을 지원하도록 개선 가능

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) 구조로 확장 가능

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에서 단순히 값을 읽어오기만 하면 되므로 응답 속도가 획기적으로 개선

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회를 아낄 수 있는 고성능 튜닝 기법

배포

AWS 기반 도커 컨테이너 배포 및 CI/CD 자동화 파이프라인 구축

  • 인프라 환경 구축 및 최적화

    • AWS EC2(t3.small) 인스턴스를 프로비저닝하고
      • 고정 IP(Elastic IP) 및 가비아 도메인 DNS 설정을 완료
    • t3 인스턴스의 제한된 RAM 용량으로 인한 OOM(Out of Memory) 현상을 방지하기 위해
      • 2GB의 스왑 메모리(Swap Memory)를 할당하여 서버 안정성을 확보
  • 웹 서버 및 HTTPS(SSL) 보안 적용

    • S3(대용량 미디어)와 Nginx(정적 파일 캐싱)를 분리하여 파일 서빙 비용과 성능을 최적화
    • Let's Encrypt(Certbot)를 이용해 SSL 인증서를 발급받고
      • Nginx의 리버스 프록시 설정을 통해 80번(HTTP) 포트 요청을
        • 443번(HTTPS)으로 자동 리다이렉트하도록 구축
  • GitHub Actions를 활용한 CI/CD 구축

    • main 브랜치에 코드가 푸시되면 GitHub Actions가 동작하여 AWS EC2에 SSH로 접속하고
    • 최신 코드를 pull 받아 Docker Compose를 재빌드하도록 배포 자동화 스크립트를 작성
  • 배포 환경 트러블슈팅 (Troubleshooting)

    • 도커 네트워크 분리 문제 해결
      • 로컬 환경의 127.0.0.1로 설정되어 있던 DB 호스트를
      • 도커 컴포즈의 서비스명(db)으로 변경하여 컨테이너 간 통신 에러를 해결
  • OAuth 동적 Redirect URI 설정

    • 소셜 로그인 시 로컬 호스트로 하드코딩되어 있던 콜백 주소를
      • 장고의 request.build_absolute_uri()를 활용해 동적으로 생성하도록 리팩토링하여
      • 로컬과 배포 환경 모두에서 정상 동작하도록 개선

전문적

  • 인프라 및 배포 (Infrastructure & Deployment)

    • AWS EC2, Nginx, Gunicorn 기반의 프로덕션 환경을 구축하고
      • Docker Compose를 활용하여 백엔드 웹 서버와 데이터베이스를 컨테이너화하여 배포 환경의 일관성을 확보
    • Let's Encrypt를 활용한 SSL 인증서 발급 및 Nginx 리버스 프록시 설정으로 HTTPS 보안 통신을 적용
    • GitHub Actions와 SSH를 연동하여 코드 푸시 시 자동으로 서버에 접속해 컨테이너를 재빌드하는
      • CI/CD 자동화 파이프라인을 구축하여 배포 소요 시간을 단축
  • 성능 및 서버 최적화 (Performance Optimization)

    • EC2 t3 인스턴스의 메모리 부족(1GB)으로 인한 서버 다운(OOM) 문제를 해결하기 위해
      • 2GB의 스왑 메모리(가상 램)를 설정하여 무중단 운영 안정성을 확보
    • 정적 파일(Static)은 Nginx가 캐싱하여 직접 서빙하고
      • 유저 업로드 파일(Media)은 AWS S3로 분리하여 서버의 부하를 줄이고 응답 속도를 최적화
  • 트러블슈팅 (Troubleshooting)

    • 도커 네트워크 환경에서 발생하는 DB Connection Refused 에러를 컨테이너 간 서비스명(DNS) 매핑으로 해결
    • 배포 환경에서 소셜 로그인(GitHub, Discord) 콜백이 실패하는 문제를 해결하기 위해
      • build_absolute_uri()를 활용해 현재 접속 호스트를 기반으로 동적인 Redirect URI를 생성하도록 결합도를 낮춤
profile
안녕하세요.

0개의 댓글