2026/01/06 ✅
ai
기능 구현에 필요한 이론
1. 토큰 (Token)과 컨텍스트 윈도우 (Context Window)
- 가장 기초적이면서 중요한 비용/제약 관련 개념
- 토큰 (Token): AI는 텍스트를 글자 단위가 아닌 의미 단위인 '토큰'으로 쪼개서 인식
- 예: "Apple"은 1토큰이지만, 한글은 보통 1글자가 1~2토큰을 차지함
- 중요한 이유: API 비용은 토큰 수에 비례해서 청구됨 리뷰 원문이 길수록 비용이 비싸짐
- 컨텍스트 윈도우 (Context Window): 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 비용 아끼기
- 결과 캐싱
- 똑같은 페이지의 리뷰를 여러 명이 조회할 때마다 AI를 부르면 낭비
- 한 번 요약된 내용은 DB나 Redis에 저장해두고, 리뷰 내용이 바뀌지 않았다면 저장된 값을 보여주기
- 배치 요약
- 사용자가 페이지를 넘길 때마다 실시간으로 요약하기보다
- 새로운 리뷰가 일정 개수(예: 10개) 쌓였을 때 미리 요약해두는 방식
- 그래도 다행히 프리티어를 사용할 경우 한도를 넘으면 자동으로 멈추고 돈이 나가지 않음
- 한도 초과 시 429 Resource Exhausted 에러와 함께 중단
- 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)
- 타입
- 방식
- NULL
- 2024-01-08 10:00:00: 해당 시간에 삭제됨
- 특징
- 삭제 여부뿐만 아니라 삭제 시점까지 기록
- 값이 있으면 삭제된 것이고, 없으면(NULL) 살아있는 것
- 장점
- 데이터 이력 관리에 훨씬 유리하며, 보관 주기(예: 탈퇴 후 30일 뒤 완전 삭제)를 계산할 때 필수적
2026/01/09 ✅
Linter (린터)
"코드가 올바른가?"
- 문법 에러, 나쁜 코드 패턴, 사용하지 않는 변수 등을 감지
- ex. Flake8
장점
- 잠재적 에러 감지
- 실행해 보지 않아도 알 수 있는 오류를 잡음
- 예: 정의되지 않은 변수 사용, 무한 루프 가능성, 도달할 수 없는 코드 등
- 컨벤션 강제 (스타일 통일)
- 팀에서 정한 규칙을 어기면 경고를 띄움
- 예: "변수명은 snake_case로 써야 하는데 왜 camelCase를 썼어?", "들여쓰기는 4칸이어야 해"
- 코드 품질 향상
"코드가 예쁜가?"
- 줄바꿈, 띄어쓰기, 괄호 위치 등 디자인적인 스타일을 자동으로 수정
- 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="김철수")
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()
- 한 모델이 다른 모델을 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)
class Comment(models.Model):
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)
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')
1:1 관계 (OneToOne): '단수형' 사용
- 1:1 관계는 반환되는 결과가 리스트(QuerySet)가 아니라 단일 객체
- 따라서 s를 붙이지 않고 단수형을 사용
class User(models.Model):
name = models.CharField(max_length=50)
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
역참조 미사용: + 기호로 비활성화
- 로그성 데이터나 히스토리 테이블 등, 굳이
- 부모 쪽에서 자식을 조회할 일이 없거나 참조를 막아야 할 경우 사용 / 불필요한 속성 생성을 막아줌
class User(models.Model):
name = models.CharField(max_length=50)
class AccessLog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+')
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_id와 user_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 등록
INSTALLED_APPS = [
'rest_framework',
'drf_spectacular',
]
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': '내 프로젝트 API',
'DESCRIPTION': 'Django로 만든 API 문서입니다.',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
from django.urls import path
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
스웨거 에디터(Swagger Editor)
스웨거와 OpenAPI
- OpenAPI (OAS)
- RESTful API를 기술하기 위한 표준 규격(문법)
- 예: "API는 이런 형식으로 정의해야 한다"는 규칙
- Swagger
- 이 OpenAPI 규격을 바탕으로 API를 설계, 문서화, 테스트할 수 있게 해주는 도구 모음
- Swagger Editor
- 브라우저에서 OpenAPI 명세를 작성하고 실시간으로 미리보기 할 수 있는 에디터
YAML
- 스웨거 에디터는 JSON도 지원하지만, 주로 YAML 언어를 사용
- 가독성이 좋고 작성이 간편함
- 주의할 점
- 들여쓰기(Indentation)가 문법의 핵심
- 스페이스바 2칸 혹은 4칸을 엄격하게 지켜야 에러가 나지 않음
YAML 파일을 직접 화면에 띄우는 설정
- "자동 생성 기능(SpectacularAPIView)"은 잠시 꺼두고
- 에디터에서 만든 "YAML 파일을 직접 화면에 띄우는 설정"하기
1. YAML 파일 저장 및 위치시키기
2. Django 프로젝트 안에 static 폴더를 만들고 그 안에 파일을 넣기
내프로젝트폴더/static/api_design.yaml
- Django가 static 폴더를 인식할 수 있게 settings.py가 설정
import os
STATIC_URL = 'static/'
STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ]
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularSwaggerView
urlpatterns = [
path('api/docs/', SpectacularSwaggerView.as_view(url='/static/api_design.yaml'), name='swagger-ui'),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
장단점 비교
자동 생성 방식 (Code-First)
- 작동 방식
- 코드를 수정하고 저장하면, 스웨거가 그걸 읽어서 문서를 자동으로 바꿈
- 장점
- 코드를 고치면 문서도 바뀌니 유지보수가 엄청 편함 (거짓말을 안 함)
- 단점
- 코드를 짜기 전에는 문서를 보여줄 수가 없음
- 협업할 때 "나 이렇게 짤 거야"라고 미리 보여주기 힘듦
Design-First / YAML 파일 로드
- 작동 방식
- 코드를 아무리 수정해도 문서는 바뀌지 않음, 오직 YAML 파일을 수정해야만 문서가 바뀜
- 장점
- 코드를 한 줄도 안 짜도 "우리 API는 이렇게 동작할 거야"라고 약속(계약)을 정할 수 있음
- 프론트엔드 개발자가 API가 완성될 때까지 기다릴 필요가 없음
- 단점
- 코드를 수정했는데 YAML 파일을 수정 안 하면, 문서와 실제 동작이 달라짐
- 치명적인 단점
전환 방법
--- [변경 전] ---
urlpatterns = [
path('api/docs/', SpectacularSwaggerView.as_view(url='/static/api_design.yaml'), name='swagger-ui'),
]
--- [변경 후] ---
urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='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에 이 설정이 되어있는지 확인 (자동 생성을 위해 필요함)
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
2026/01/11 ✅
CI/CD 요약
- Workflow (워크플로우)
- Event (이벤트)
- 워크플로우를 실행시키는 트리거 (예: pull_request)
- Job (작업)
- 독립된 실행 단위
- 여기서는 ci와 test 두 개의 Job이 있음
- 기본적으로 병렬로 실행되지만, needs를 통해 순서를 정할 수 있음
- Step (단계)
- Runner (러너)
- 이 코드가 실행되는 가상 컴퓨터(서버) (ubuntu-latest)
- Service (서비스)
- 테스트 시 필요한 DB(Postgres, Redis) 등을 도커 컨테이너로 띄워주는 기능
DEBUG

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