Union Project - learning - 1주차

김기훈·2026년 1월 7일

부트캠프 프로젝트

목록 보기
31/39
post-thumbnail

2026/01/06 ✅


ai


기능 구현에 필요한 이론

  • 1. 토큰 (Token)과 컨텍스트 윈도우 (Context Window)

    • 가장 기초적이면서 중요한 비용/제약 관련 개념
  • 토큰 (Token): AI는 텍스트를 글자 단위가 아닌 의미 단위인 '토큰'으로 쪼개서 인식
    • 예: "Apple"은 1토큰이지만, 한글은 보통 1글자가 1~2토큰을 차지함
    • 중요한 이유: API 비용은 토큰 수에 비례해서 청구됨 리뷰 원문이 길수록 비용이 비싸짐
  • 컨텍스트 윈도우 (Context Window): AI가 한 번에 기억(처리)할 수 있는 토큰의 한계치
    • 중요한 이유: 만약 한 페이지의 리뷰 양이 너무 많아서 이 윈도우를 넘어가면
      • AI는 앞부분 내용을 잘라먹거나 에러를 냄
  • 리뷰 텍스트를 보낼 때 무작정 다 보내지 말고, 불필요한 조사나 특수문자를 제거하는 전처리가 필요함

  • 2. 프롬프트 엔지니어링 (Prompt Engineering)

    • AI에게 일을 시키는 '명령어 작성 기술'
    • 단순히 "요약해줘"라고 하는 것과 개발적으로 구조화하는 것은 다름
  • 시스템 프롬프트 (System Prompt)
    • AI에게 역할을 부여하는 설정 (예: "너는 10년 차 이커머스 MD야")
  • 퓨샷 러닝 (Few-Shot Learning)
    • 예시를 몇 개 보여주면 성능이 비약적으로 올라가는 이론
  • Zero-Shot
    • "이거 요약해" (예시 없음)
  • Few-Shot
    • "A리뷰 -> B요약, C리뷰 -> D요약. 자 이제 E리뷰를 이렇게 요약해." (예시 제공)
  • 요약 결과의 형식을 JSON 등으로 일정하게 받고 싶다면,
    • 퓨샷 러닝으로 예시 데이터를 프롬프트에 포함시키는 것이 필수적임

  • 3. 환각 현상 (Hallucination)

    • AI가 사실이 아닌 내용을 그럴싸하게 지어내는 현상
  • 개념
    • LLM(대규모 언어 모델)은 진실을 말하는 기계가 아닌
    • "다음에 올 가장 확률 높은 단어"를 이어 붙이는 기계
    • 리뷰에 없는데 "재밌어요"라는 말을 습관적으로 넣을 수 있음
  • 대처법
    • 프롬프트에 제약 조건(Constraint)을 강력하게 걸어야 함
    • ex. "제공된 텍스트에 없는 내용은 절대 포함하지 마시오."

  • 4. 템퍼러처 (Temperature)

    • AI의 '창의성' 혹은 '랜덤성'을 조절하는 파라미터(변수)(보통 0.0 ~ 1.0 사이 값)
    • 낮은 값 (0.0 ~ 0.3)
      • 매우 논리적이고, 사실적이며, 정해진 답을 반복 (요약 기능에 적합)
    • 높은 값 (0.7 ~ 1.0)
      • 창의적이고 다양하며, 할 때마다 다른 말을 함 (소설 쓰기에 적합)
    • 실무 팁
      • 리뷰 요약 기능을 구현할 때는 Temperature를 0에 가깝게 설정해야
      • 매번 일관되고 사실에 입각한 요약이 나옴

  • 5. Stateless (상태 비저장)

    • REST API를 사용할 때 AI 모델은 이전 대화를 기억하지 못함
  • 개념
    • 1페이지 요약을 요청하고 나서,
    • 바로 이어서 "아까 그거 영어로 번역해줘"라고 요청하면 AI는 "아까 그거"가 뭔지 모름
  • 실무 팁
    • 모든 요청(Request)은 그 자체로 완결성을 가져야 함
    • 즉, 매번 필요한 리뷰 텍스트 데이터를 함께 실어 보내거나, 이전 문맥을 다시 보내줘야 함
      • (단, 페이지별 요약 기능은 각 페이지가 독립적이므로 이 부분은 구현이 일반적으로 쉬운편)

ai 비용 아끼기

    1. 결과 캐싱
    • 똑같은 페이지의 리뷰를 여러 명이 조회할 때마다 AI를 부르면 낭비
    • 한 번 요약된 내용은 DB나 Redis에 저장해두고, 리뷰 내용이 바뀌지 않았다면 저장된 값을 보여주기
    1. 배치 요약
    • 사용자가 페이지를 넘길 때마다 실시간으로 요약하기보다
    • 새로운 리뷰가 일정 개수(예: 10개) 쌓였을 때 미리 요약해두는 방식
  • 그래도 다행히 프리티어를 사용할 경우 한도를 넘으면 자동으로 멈추고 돈이 나가지 않음
    • 한도 초과 시 429 Resource Exhausted 에러와 함께 중단
    1. DRF의 ScopedRateThrottle 기능을 사용하여
    • 한 명의 사용자가 1분에 5번 이상 요약 버튼을 못 누르게 막아버리기

OpenAPI


OpenAPI를 사용하는 이유

  • 이미 잘 만들어진 기능을 가져와서, 더 빠르고 효율적으로 내 서비스를 만들기 위해서 사용
  • 개발 시간과 비용 절약

    • Open API를 사용하면 이미 검증된 기능을 빌려 쓸 수 있음
  • 사용자 편의성 증대 (접근성)

    • 사용자가 이미 익숙하게 쓰고 있는 서비스와 연동하면, 사용자가 내 서비스에 진입하는 장벽이 낮아짐
    • 대표적 예시 (소셜 로그인): 회원가입 시 일일이 정보를 입력하는 대신 "카카오톡으로 로그인",
      • "구글로 로그인" 버튼을 누르죠? 이게 바로 카카오와 구글의 Open API를 사용한 것

API 비용이란?

  • 남이 만들어 둔 기능(지도, 날씨 정보, 인공지능 번역 등)을 내 프로그램에서 가져다 쓸 때 발생하는 요금을 말함

2026/01/07 ✅


ai

기능구현에 필요한 부분

  • 프롬프트 엔지니어링

    • 모델에게 "어떻게 일해야 하는지" 지시문을 아주 잘 작성해야 함
    • ex. 너는 게임 리뷰 요약 전문가야. (페르소나) 들어오는 텍스트를 분석해서 3줄로 요약해(작업)
      • 욕설이 있으면 거절해. (안전 규칙) 한국어로 출력해. (언어 규칙)"
  • 데이터 전처리

    • Gemini에게 보내기 전에 데이터를 다듬는 과정 필요
      • HTML 태그 제거 (<div>, <br> 등 삭제)
      • 너무 긴 리뷰는 자르기
  • API 연동 및 예외 처리 (6~9)

    • Spring/Node.js 등 서버에서 Gemini API에 HTTP 요청을 보내는 코드 작성
    • 응답이 15초 이상 걸릴 경우 타임아웃 처리
    • 화면에 "AI가 읽고 있어요..." 로딩 표시 (프론트엔드와 협업)

RESTful 원칙

  • Representational State Transfer-ful

    • 웹(Web)의 장점을 최대한 활용하기 위해 제안된 네트워크 아키텍처 스타일
      • "HTTP 프로토콜을 의도에 맞게 정확하고 효율적으로 사용하기 위한 6가지 제약 조건

1. 클라이언트-서버 구조

  • Client-Server
    • 핵심

      • 역할의 확실한 분리
    • 내용

      • 클라이언트(UI/사용자 경험)와 서버(데이터 처리/저장)가 독립적으로 작동해야 함
      • 서로 간의 의존성이 줄어들어 클라이언트와 서버를 각각 독립적으로 개발하고 확장 가능

2. 무상태성

  • Stateless
    • 핵심

      • 서버는 클라이언트의 상태(Context)를 저장하지 않는다
    • 내용

      • 모든 요청은 그 자체로 완전(Complete)해야 함
      • 즉, 요청 하나만 보더라도 서버가 작업을 수행하기 위한 모든 정보가 포함되어야 함
    • 장점

      • 서버가 세션 정보를 유지할 필요가 없으므로 서버 확장이 용이

3. 캐시 가능

  • Cacheable
    • 핵심

      • HTTP의 웹 표준을 그대로 사용하므로 캐싱 기능을 적용할 수 있어야 함
    • 내용

      • 자주 요청되는 데이터는 클라이언트나 중간 서버(Proxy)에 저장(Cache)해두고 재사용 가능 해야함
      • 이를 통해 네트워크 트래픽을 줄이고 응답 속도를 높임
        • ex. HTTP Header의 Last-Modified, ETag 활용

4. 계층형 구조

  • Layered System
    • 핵심

      • 클라이언트는 서버의 실제 구조를 알 필요가 없음
    • 내용

      • 클라이언트와 서버 사이에 로드 밸런서, 암호화 계층, 게이트웨이 등 중간 계층을 자유롭게 추가 가능
      • 클라이언트는 자신이 연결된 곳이 엔드 서버인지 중간 프록시인지 알 수 없으며, 알 필요도 없음

5. 인터페이스 일관성 ⭐️

  • Uniform Interface (가장 중요)
    • RESTful API를 구분 짓는 가장 핵심적인 특징
    • URI로 자원을 식별하고 HTTP 메서드로 행위를 정의하여 통일된 인터페이스를 제공하는 것
      • 자원의 식별
        • URI는 행위(동사)가 아닌 자원(명사)을 나타내야 함
        • GET /getMembers (X) -> GET /members (O)
      • 메시지를 통한 리소스 조작
        • HTTP Method(GET, POST, PUT, DELETE)를 사용하여 자원을 처리해야 함
      • 자기 서술적 메시지 (Self-descriptive)
        • 메시지(요청/응답)만 보고도 그 내용을 해석할 수 있어야 함
        • Content-Type 헤더 등을 명확히 명시
      • HATEOAS
        • 애플리케이션의 상태는 Hyperlink를 통해 전이되어야 함
        • 응답에 다음 행동을 위한 링크를 포함하는 개념

6. 자체 표현 구조

  • Code on Demand (선택 사항)
    • 내용

      • 서버가 클라이언트에게 실행 가능한 코드(예: JavaScript)를 전송하여
      • 클라이언트의 기능을 일시적으로 확장 가능 (유일한 선택적 제약 조건)

예시

  • 제대로 된 RESTful API는
    • URI만 봐도 어떤 자원인지 알 수 있고, 메서드만 봐도 어떤 동작을 하는지 예측 가능해야 함
  • REST는 공식 '표준'이 아니라 아키텍처 '스타일'이므로
    • 실무에서는 상황에 맞게 유연하게 적용하기도 함
HTTP 메서드			URI (자원)			의미,설명

GET					/users/1			조회,1번 사용자의 정보를 가져온다.
POST				/users				생성,새로운 사용자를 등록한다. (데이터는 Body에 포함)
PUT					/users/1			전체 수정,1번 사용자의 정보를 통째로 교체한다.
PATCH				/users/1			부분 수정,1번 사용자의 정보 중 일부(: 이메일)만 수정한다.
DELETE				/users/1			삭제,1번 사용자를 삭제한다.

2026/01/08 ✅

better-profanity

delete

Hard Delete (물리 삭제)

  • DELETE 명령어로 DB에서 데이터를 완전히 제거 / 복구 불가능

Soft Delete (논리 삭제)

  • UPDATE 명령어로 특정 컬럼(플래그)만 변경하여 삭제된 것처럼 표시만 함 / 데이터는 남아있음
    • 데이터 복구
      • 사용자가 실수로 삭제했을 때 deleted_at을 다시 NULL로 바꾸기만 하면 즉시 복구됨
    • 데이터 무결성
      • ex. 쇼핑몰에서 어떤 회원이 탈퇴했다고 해서 그 사람이 주문한 '주문 내역'까지 싹 지워지면 매출 통계가 꼬임
      • 회원은 '삭제 상태'로 두고 주문 내역과의 연결은 유지해야 함
    • 이력 추적 (Audit)
      • 언제 데이터가 삭제되었는지 법적/운영적 근거로 남겨야 할 때가 많음


is_deleted(Boolean)

  • 타입
    • BOOLEAN (True/False) 또는 TINYINT (0/1)
  • 방식
    • FALSE (0): 활성 상태 (삭제 안 됨)
    • TRUE (1): 삭제된 상태
  • 특징
    • 가장 단순한 형태 / "지워졌는가?"에 대한 답만 줌
  • 단점
    • 언제 지워졌는지 알 수 없음

deleted_at (Timestamp / Datetime)

  • 타입
    • TIMESTAMP 또는 DATETIME
  • 방식
    • NULL
      • 활성 상태 (삭제 안 됨)
    • 2024-01-08 10:00:00: 해당 시간에 삭제됨
  • 특징
    • 삭제 여부뿐만 아니라 삭제 시점까지 기록
    • 값이 있으면 삭제된 것이고, 없으면(NULL) 살아있는 것
  • 장점
    • 데이터 이력 관리에 훨씬 유리하며, 보관 주기(예: 탈퇴 후 30일 뒤 완전 삭제)를 계산할 때 필수적

2026/01/09 ✅

Linter (린터)

  • "코드가 올바른가?"

    • 문법 에러, 나쁜 코드 패턴, 사용하지 않는 변수 등을 감지
    • ex. Flake8
  • 장점

    • 잠재적 에러 감지
      • 실행해 보지 않아도 알 수 있는 오류를 잡음
      • 예: 정의되지 않은 변수 사용, 무한 루프 가능성, 도달할 수 없는 코드 등
    • 컨벤션 강제 (스타일 통일)
      • 팀에서 정한 규칙을 어기면 경고를 띄움
      • 예: "변수명은 snake_case로 써야 하는데 왜 camelCase를 썼어?", "들여쓰기는 4칸이어야 해"
    • 코드 품질 향상
      • 복잡도를 줄이고 가독성을 높여줌

Formatter (포매터)

  • "코드가 예쁜가?"

    • 줄바꿈, 띄어쓰기, 괄호 위치 등 디자인적인 스타일을 자동으로 수정
    • ex. Prettier, Black, Google Java Format

역참조

  • 관계형 데이터베이스에서 두 테이블(모델)이 연결될 때,
    • Foreign Key(외래키)는 항상 '자식(N)' 쪽이 들고 있음
# ex. **작가(Author)** / **책(Book)** → 1:N 관계

- Book (자식): 작가 ID를 가지고 있음
- Author (부모): 책들에 대한 정보가 없음

# 정참조 
방향: 책 → 작가 (Book -> Author)
설명: 책(Book)은 작가(Author) 정보를 ForeignKey로 직접 가지고 있음 그래서 그냥 부르면 됨
코드: book.author 

# 역참조
방향: 작가 → 책 (Author -> Book)
설명: 작가 입장에서 본인의 책을 모두 가져오기 위해서는, 
     내 ID를 가지고 있는 책들을 반대로 찾아가서 긁어모아야 함 이것이 역참조
문제: 작가 모델에는 book이라는 필드가 없음
그래서 Django가 가상의 통로를 만들어주는데, 이때 사용하는 이름이 바로 related_name

  • 미사용

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

[결과]
me = Author.objects.get(name="김철수")

# "김철수가 쓴 책 가져오기"
# Django가 자동 생성한 '_set'을 써야 함
my_books = me.book_set.all()
  • 사용

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    title = models.CharField(max_length=100)
    
[결과]
me = Author.objects.get(name="김철수")

# "김철수가 쓴 책 가져오기"
# 우리가 지어준 이름 사용
my_books = me.books.all()

역참조(related_name)가 필수적인 순간

  • 한 모델이 다른 모델을 2번 이상 참조할 때에는 꼭 사용해야 에러가 안남
class Airport(models.Model):
    name = models.CharField(max_length=50)

class Flight(models.Model):
    # 출발 공항
    departure = models.ForeignKey(Airport, on_delete=models.CASCADE) 
    # 도착 공항
    arrival = models.ForeignKey(Airport, on_delete=models.CASCADE)

[문제]
- 1. departure 필드를 보고 -> airport.flight_set을 만듦
- 2. arrival 필드를 보고 -> 또 airport.flight_set을 만들려고 함
- 3. 충돌! 에러(SystemCheckError)
class Airport(models.Model):
    name = models.CharField(max_length=50)
    
class Flight(models.Model):
    departure = models.ForeignKey(Airport, ..., related_name='departing_flights')
    arrival = models.ForeignKey(Airport, ..., related_name='arriving_flights')

[해결]
incheon_airport.departing_flights.all() : 인천에서 출발하는 비행기들

incheon_airport.arriving_flights.all() : 인천으로 도착하는 비행기들

  • 기본 원칙: 1:N 관계는 '복수형' 사용

    • 부모 입장에서 자식 객체들을 부르는 이름이므로 복수형(Plural)을 사용
# [부모] 게시글
class Post(models.Model):
    title = models.CharField(max_length=100)

# [자식] 댓글 (Post를 바라봄)
class Comment(models.Model):
    # Bad: related_name 없음 -> post.comment_set.all()
    # Good: related_name='comments'
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')

# 사용 예시
- my_post.comments.all()
  • 다중 참조: '동사/형용사'로 문맥 구체화

    • 한 모델이 다른 모델을 2개 이상의 Foreign Key로 참조할 때는 단순 명사만으로 구분할 수 없음
    • 역할(Role)이나 행동(Action)을 접두어로 붙임
# [부모] 사용자
class User(models.Model):
    name = models.CharField(max_length=50)

# [자식] 프로젝트 (User를 두 번 바라봄)
class Project(models.Model):
    # 생성자 (내가 만든 것들)
    creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_projects')
    # 담당자 (나에게 할당된 것들)
    assignee = models.ForeignKey(User, on_delete=models.CASCADE, related_name='assigned_projects')

# 사용 예시
# user.created_projects.all()  -> 내가 만든 프로젝트 목록
# user.assigned_projects.all() -> 내가 담당자로 지정된 프로젝트 목록
  • 1:1 관계 (OneToOne): '단수형' 사용

    • 1:1 관계는 반환되는 결과가 리스트(QuerySet)가 아니라 단일 객체
    • 따라서 s를 붙이지 않고 단수형을 사용
# [부모] 사용자
class User(models.Model):
    name = models.CharField(max_length=50)

# [자식] 프로필 (User와 1:1 매칭)
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')

# 사용 예시
# user.profile.nickname (리스트가 아닌 객체 자체 반환)
  • 역참조 미사용: + 기호로 비활성화

    • 로그성 데이터나 히스토리 테이블 등, 굳이
    • 부모 쪽에서 자식을 조회할 일이 없거나 참조를 막아야 할 경우 사용 / 불필요한 속성 생성을 막아줌
# [부모] 사용자
class User(models.Model):
    name = models.CharField(max_length=50)

# [자식] 접속 로그 (굳이 User에서 로그 전체를 긁어올 필요가 없을 때)
class AccessLog(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+')

# 사용 예시
# user.accesslog_set -> AttributeError 발생 (접근 불가)

2026/01/10 ✅

인덱스(index)

  • 데이터를 미리 정렬해 둬서, 찾는 속도를 획기적으로 줄이는 기술
  • 인덱스가 없는 경우

    • 상태: 데이터가 입력된 순서대로 무작위로 쌓여 있음
    • 비유: 숫자가 마구잡이로 적힌 카드 100장이 바닥에 흩뿌려져 있음
    • 작업: "숫자 77을 찾아라"
    • 방법: 카드 하나하나를 다 뒤집어봐야 합니다. 운이 나쁘면 100장을 다 봐야 함
  • 인덱스가 있는 경우(B-Tree)

    • 상태: 데이터가 순서대로 정렬되어, 나무(Tree) 구조로 정리되어 있음
    • 비유: 업다운(Up & Down) 게임 (1~100 사이 숫자 맞추기)
    • 작업: "숫자 77을 찾아라"
    • 방법
      • 중간값 50을 봄 -> "77은 더 크네? (오른쪽으로 이동)" (왼쪽 50개는 버림)
      • 나머지의 중간값 75를 봄 -> "77은 더 크네? (오른쪽으로 이동)"
      • 몇 번만 반복하면 금방 77을 찾음
  • 인덱스의 대가

    • 쓰기 성능 저하 (Write Penalty)
      • 상황: 새로운 리뷰가 하나 등록(INSERT)
      • 인덱스 없을 때: 그냥 책 맨 뒤에 페이지 한 장 붙이면 끝 (빠름)
      • 인덱스 있을 때:
        • 책 본문에 내용 추가
        • '게임별 인덱스' 페이지 펴서 순서에 맞는 위치 찾아서 끼워 넣기
        • '작성자별 인덱스' 페이지 펴서 또 끼워 넣기
        • '날짜별 인덱스' 페이지 펴서 또 끼워 넣기
      • 결과: 인덱스가 많을수록 저장(Insert), 수정(Update), 삭제(Delete) 속도가 현저히 느려짐
    • 저장 공간 차지 (Storage)
      • 인덱스도 결국 데이터, 실제 데이터 크기의 10~30% 정도를 추가로 차지함
  • 어디에 걸면 효율적?

    • 높은 카디널리티 (Good): 주민번호, ID, 전화번호
      • 거의 유일한 값들이라 인덱스를 타면 바로 1~2개만 나옴 (검색 효과 최고)
    • 낮은 카디널리티 (Bad): 성별(남/여), 삭제여부(T/F)
      • 데이터가 100만 개인데 성별로 인덱스를 걸면? 남을 찾았더니 50만 개가 나옴
      • 어차피 책의 절반을 읽어야 한다면, 인덱스를 거쳐서 가는 게 오히려 더 느림
        • DB가 알아서 인덱스 무시하고 전체 스캔을 하기도 함

  • 이름 규칙

    • 개발자들끼리의 약속
      • idx_... → 일반 인덱스 (Index) -> 검색 속도 향상용
      • uk_... → 유니크 키 (Unique Key) -> 중복 방지용
      • fk_... → 외래 키 (Foreign Key) -> 다른 테이블 참조용
      • pk_... → 기본 키 (Primary Key) -> ID

review

  • 코드의 의미

indexes = [
            models.Index(
                fields=['game', '-like_count'],
                name='idx_reviews_best'
            ),
        ]
  • fields=['game', '-like_count'] → 두 개의 컬럼을 묶어서 하나의 인덱스로 만듬
    • game → 첫 번째 기준 (어떤 게임인지)
    • -like_count → 두 번째 기준
      • 마이너스(-)는 내림차순(DESC)을 의미
      • 즉, 좋아요가 많은 숫자부터 적은 숫자 순서로 미리 정렬해두라는 뜻
    • name='idx_reviews_best'
      • 데이터베이스 내에서 이 인덱스를 식별할 이름입니다.

  • 이 인덱스가 데이터베이스에 저장되는 형태

    • 데이터베이스가 이 인덱스를 생성하면, 내부적으로 별도의 정렬된 목차(Table of Contents)를 만듬
    • 장점

      • 게임별로 이미 뭉쳐져(Grouping) 있음
      • 같은 게임 안에서는 좋아요가 많은 순서대로 이미 줄을 서(Sorting) 있음

  • 성능 차이

    • Review.objects.filter(game_id=1).order_by('-like_count')[:10]
      • "LoL(game_id=1)의 리뷰를 가져와서, 좋아요 많은 순서대로 10개만 보여줘"
  • 인덱스가 없을 때

    • 전체 스캔: DB는 reviews 테이블의 모든 행(수십만 개일 수도 있음)을 다 뒤져봄
    • 필터링: 그중에서 game_id=1인 것을 찾아냄
    • 별도 정렬(File Sort)
      • 찾아낸 데이터를 메모리에 올리고, like_count를 기준으로 다시 줄을 세움(Sorting 부하 발생)
    • 자르기: 맨 위 10개를 가져옴
    • 결과: 데이터가 많아지면 엄청나게 느려짐
  • 인덱스가 있을 때

    • 즉시 이동: 인덱스 목차에서 game_id=1이 시작하는 위치로 바로 점프
    • 읽기: 이미 like_count 순서대로 정렬되어 있으므로, 그냥 위에서부터 10개만 쓱 읽고 끝냄
    • 정렬 과정 생략: 이미 정렬되어 있으므로 DB가 다시 정렬할 필요가 없음
    • 결과: 데이터가 1억 개가 있어도, 0.001초 만에 결과를 가져옴

  • DB에서 저장되는 방식

    • idx_reviews_best는 테이블 목록에서는 나타나지 않고 인덱스 목록(Indexes)에서 확인 가능
    • 테이블도 아닌데 이른을 지어주는 이유

      • 니중에 좋아요순 정렬을 사용하지 않는 경우 이 인덱스가 필요하지 않을경우
      • 용량만 차지하기 때문에 삭제하기 위해서 "이름"이 필요하다 즉, "관리"떼문이다.

review_like

constraints = [
            models.UniqueConstraint(
                fields=['review', 'user'],
                name='uk_review_like_user'
            )
        ]
  • UniqueConstraint
    • Constraint: "중복을 허용하지 마라!"라는 규칙(제약 조건)을 선언
    • 실제로는 같은 index임
  • 기능
    • 규칙: "같은 review_iduser_id 조합은 두 번 들어올 수 없다."
    • 이 규칙을 0.001초 만에 검사하기 위한 고속 검색표(Index)가 만들어짐

스웨거

drf-spectacular

  • 과거에는 drf-yasg라는 라이브러리를 많이 썼지만, 최근에는 drf-spectacular가 사실상 표준
    • drf-yasg는 옛날 버전(Swagger 2.0) 기반이고,
    • drf-spectacular는 최신 규격(OpenAPI 3.0)을 지원하며 유지보수가 훨씬 좋음

세팅 방법

  • 설치

    • poetry add drf-spectacular
  • settings.py 등록

# settings.py

INSTALLED_APPS = [
    # ... 기존 앱들 ...
    'rest_framework',
    'drf_spectacular', # 여기 추가
]

REST_FRAMEWORK = {
    # DRF가 스키마(문서) 생성기로 spectacular를 쓰도록 지정
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
    'TITLE': '내 프로젝트 API',
    'DESCRIPTION': 'Django로 만든 API 문서입니다.',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
    # 인증 관련 설정 (로그인 토큰 등)이 필요하면 여기에 추가
}
  • urls.py 연결

# urls.py
from django.urls import path
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView

urlpatterns = [
    # ... 기존 url들 ...

    # 1. 스키마 파일(YAML)을 다운로드 받는 URL (눈에 안 보이는 데이터)
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),

    # 2. 우리가 볼 스웨거 UI (위의 schema를 가져와서 그림)
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]

스웨거 에디터(Swagger Editor)


스웨거와 OpenAPI

  • OpenAPI (OAS)
    • RESTful API를 기술하기 위한 표준 규격(문법)
      • 예: "API는 이런 형식으로 정의해야 한다"는 규칙
  • Swagger
    • 이 OpenAPI 규격을 바탕으로 API를 설계, 문서화, 테스트할 수 있게 해주는 도구 모음
      • Editor, UI, Codegen 등
  • Swagger Editor
    • 브라우저에서 OpenAPI 명세를 작성하고 실시간으로 미리보기 할 수 있는 에디터

YAML

  • 스웨거 에디터는 JSON도 지원하지만, 주로 YAML 언어를 사용
    • 가독성이 좋고 작성이 간편함
    • 주의할 점
      • 들여쓰기(Indentation)가 문법의 핵심
      • 스페이스바 2칸 혹은 4칸을 엄격하게 지켜야 에러가 나지 않음

YAML 파일을 직접 화면에 띄우는 설정

  • "자동 생성 기능(SpectacularAPIView)"은 잠시 꺼두고
    • 에디터에서 만든 "YAML 파일을 직접 화면에 띄우는 설정"하기
  • 1. YAML 파일 저장 및 위치시키기

    • 스웨거 에디터에서 본인 파트 api정리하기
  • 2. Django 프로젝트 안에 static 폴더를 만들고 그 안에 파일을 넣기

    • 내프로젝트폴더/static/api_design.yaml
    • Django가 static 폴더를 인식할 수 있게 settings.py가 설정
# settings.py
import os
STATIC_URL = 'static/'
STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ]
  • urls.py 수정

# urls.py
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularSwaggerView

urlpatterns = [
    # ... 기존 url들 ...

    # url_name 대신 url 파라미터에 파일 경로를 직접 적어주기
    path('api/docs/', SpectacularSwaggerView.as_view(url='/static/api_design.yaml'), name='swagger-ui'),

] 
# 개발 모드(DEBUG=True)에서 static 파일을 서빙하기 위한 설정
if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])

장단점 비교

자동 생성 방식 (Code-First)

  • 작동 방식
    • 코드를 수정하고 저장하면, 스웨거가 그걸 읽어서 문서를 자동으로 바꿈
  • 장점
    • 코드를 고치면 문서도 바뀌니 유지보수가 엄청 편함 (거짓말을 안 함)
  • 단점
    • 코드를 짜기 전에는 문서를 보여줄 수가 없음
      • 협업할 때 "나 이렇게 짤 거야"라고 미리 보여주기 힘듦

Design-First / YAML 파일 로드

  • 작동 방식
    • 코드를 아무리 수정해도 문서는 바뀌지 않음, 오직 YAML 파일을 수정해야만 문서가 바뀜
  • 장점
    • 코드를 한 줄도 안 짜도 "우리 API는 이렇게 동작할 거야"라고 약속(계약)을 정할 수 있음
    • 프론트엔드 개발자가 API가 완성될 때까지 기다릴 필요가 없음
  • 단점
    • 코드를 수정했는데 YAML 파일을 수정 안 하면, 문서와 실제 동작이 달라짐
  • 치명적인 단점
    • 문서가 거짓말을 하게 됨

전환 방법

--- [변경 전] ---
# urls.py
urlpatterns = [
    # ...
    
    # 1. 스키마 생성 뷰 (지금은 안 쓰니까 주석 처리했거나 있어도 무시됨)
    # path('api/schema/', SpectacularAPIView.as_view(), name='schema'),

    # 2. 스웨거 UI (파일 경로를 직접 지정)
    path('api/docs/', SpectacularSwaggerView.as_view(url='/static/api_design.yaml'), name='swagger-ui'),
]

--- [변경 후] ---
# urls.py 
urlpatterns = [
    # ...

    # 1. 스키마 생성 뷰 (필수! 주석을 풀거나 추가해야 함)
    # 설명: 코드를 읽어서 데이터를 만들어주는 공장 가동
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),

    # 2. 스웨거 UI (위의 공장 이름을 연결)
    # 설명: "static 파일 말고, 'schema'라는 이름의 뷰한테 데이터 달라고 해"
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
  • 핵심

    • SpectacularAPIView 살리기
      • 자동 모드에서는 얘가 데이터를 실시간으로 만들어주는 핵심 공장 / 이 줄이 반드시 필요
    • url → url_name 변경
      • url='/static/...': 파일을 보여줘
      • url_name='schema': name='schema'를 가진 뷰한테 데이터 달라고 해
  • 체크

    • settings.py에 이 설정이 되어있는지 확인 (자동 생성을 위해 필요함)
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

2026/01/11 ✅

CI/CD 요약

  • Workflow (워크플로우)
    • 전체 자동화 프로세스 (이 파일 전체)
  • Event (이벤트)
    • 워크플로우를 실행시키는 트리거 (예: pull_request)
  • Job (작업)
    • 독립된 실행 단위
    • 여기서는 ci와 test 두 개의 Job이 있음
    • 기본적으로 병렬로 실행되지만, needs를 통해 순서를 정할 수 있음
  • Step (단계)
    • Job 안에서 순차적으로 실행되는 명령어들
  • Runner (러너)
    • 이 코드가 실행되는 가상 컴퓨터(서버) (ubuntu-latest)
  • Service (서비스)
    • 테스트 시 필요한 DB(Postgres, Redis) 등을 도커 컨테이너로 띄워주는 기능

DEBUG

  • 개발 중인 내 컴퓨터(Local)에서는 True가 맞지만

    • CI(테스트 서버)에서는 False로 설정하는 것이 정석
  • 왜 CI에서 DEBUG=False를 권장?

    • 개발할 때는 True가 편하지만, 자동화된 테스트(CI)에서는 다음과 같은 이유로 False로 설정함
  • 배포 환경 검증 (Production Readiness)

    • DEBUG=True일 때는 Django가 많은 보안 규칙을 느슨하게 풀어줌
      • 예: ALLOWED_HOSTS 체크를 건너뜀
    • DEBUG=False로 설정해야만 실제 배포 시 발생할 수 있는 설정 오류
      • (호스트 불일치, 정적 파일 경로 문제 등)를 미리 잡아낼 수 있음
  • 에러 핸들링 테스트

    • 사용자가 없는 페이지에 접속했을 때 노란색 디버그 페이지가 뜨는 것이 아니라,
    • 404.html (페이지 없음)이나 500.html (서버 에러) 페이지가 잘 뜨는지 테스트하려면 False여야 함
  • 로그 가독성

    • CI 터미널에서 테스트가 실패했을 때,
    • DEBUG=True라면 엄청나게 긴 HTML 코드가 로그에 찍혀서 오히려 에러 원인을 찾기 힘들 때가 있음

ALLOWED_HOSTS

  • ALLOWED_HOSTS = ["*"]

  • ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])

    • 기본값은 ["*"]로 두어 개발/CI에서 편하게 사용하고,
    • 환경변수(ALLOWED_HOSTS)가 있으면 그 값을 리스트로 파싱해서 사용함
        1. env.list(...): django-environ 라이브러리 기능
        1. 콤마(,)로 구분된 문자열을 리스트로 변환해줌
      • 예) .env 파일에 ALLOWED_HOSTS=naver.com,google.com 라고 적으면
        • -> ["naver.com", "google.com"] 으로 변환됨
        1. default=["*"]
      • .env에 값이 없으면 자동으로 모든 호스트를 허용 (개발/CI용)

profile
안녕하세요.

0개의 댓글