설치 : curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
설치: poetry add --group=dev black==24.10.0
들여쓰기, 따옴표, 줄바꿈 등등 작성한 코드를 black 코드 스타일에 맞게 자동 수정해줌
실행: poetry run black .
pyproject.toml [tool.black] 대괄호를 pyproject.toml 파일에서는 “섹션” 이라고 부름[tool.black] 섹션에서 black에 관련된 설정을 함설치: poetry add --group=dev ruff==0.8.2
코드 스타일 검사, 사용하지 않는 import/변수 탐지, 잠재적 버그 패턴 잡기

설치: poetry add --group dev isort
Python 코드에서 import 구문을 자동으로 정렬해주는 코드 포매터(tool)
핵심 기능
사용법
poetry run isort .poetry run isort app/main.pypoetry run isort . --diffpoetry run isort . --check-only설치: poetry add --group=dev mypy==1.13.0
Python 코드의 타입 힌트(type hint)를 검사해서 실행 전에 타입 오류를 미리 찾아주는 도구
옵션
--strict : 엄격한 검사 모드 (가장 많이 사용)--ignore-missing-imports : 외부 패키지 타입 정보가 없어도 무시--disallow-untyped-defs : 타입 힌트가 없는 함수 정의 금지설치: poetry add --group=dev pytest==8.3.4
Python에서 단위 테스트(unit test)를 쉽게 작성하고 실행할 수 있게 해주는 도구
자주 사용하는 명령어
pytest : 전체 테스트 실행pytest tests/ : 특정 폴더만 테스트pytest -v : 테스트 이름과 결과 자세히 표시pytest -x : 첫 실패 시 멈춤pytest --maxfail=3 : 3개 실패 후 멈춤설치: poetry add --group=dev pytest-asyncio==0.25.0
pytest-asyncio 플러그인을 사용하면 async 함수도 실행할 수 있음

설치: poetry add --group=dev coverage==7.6.9
프로그램의 코드 중 테스트로 검증된 부분의 비율을 의미
사용법
coverage run -m pytestcoverage reportcoverage html| 구분 | 의미 | 예시 |
|---|---|---|
| Dependency (의존성) | 실제 코드가 실행될 때 필요한 라이브러리 | fastapi, uvicorn, sqlalchemy |
| Dev Dependency (개발 의존성) | 테스트, 포매팅, 타입체크 등 개발 과정에서만 필요한 라이브러리 | pytest, mypy, black, isort, coverage |
poetry add fastapi uvicorn[tool.poetry.dependencies] 섹션에 추가poetry add --dev pytest mypy black isort[tool.poetry.group.dev.dependencies] 섹션에 추가됨
| 종류 | 설명 | 예시 도구 |
|---|---|---|
| 단위 테스트 (Unit Test) | 함수나 클래스 단위로 작동 검증 | pytest, unittest |
| 통합 테스트 (Integration Test) | 여러 모듈이 함께 잘 작동하는지 | pytest, requests |
| 엔드투엔드 테스트 (E2E Test) | 실제 사용자 시나리오처럼 전체 동작 검증 | Selenium, Playwright |
| CI 자동화 테스트 | GitHub Actions, GitLab CI 등에서 푸시 시 자동 수행 | pytest + coverage + mypy 조합 |
| 쉘 종류 | 설명 | 기본 위치 |
|---|---|---|
| bash (Bourne Again Shell) | 가장 일반적 (리눅스, 맥 기본) | /bin/bash |
| zsh (Z Shell) | macOS 기본 쉘 (Catalina 이후) | /bin/zsh |
| sh (Bourne Shell) | 오래된 기본 쉘 | /bin/sh |
| 문법 | 설명 | 예시 |
|---|---|---|
# | 주석 | # 이건 주석 |
echo | 출력 | echo "Hello" |
$(명령) | 명령 실행 후 결과를 변수처럼 사용 | echo $(date) |
VAR=value | 변수 선언 | NAME="KIHOON" |
$VAR | 변수 참조 | echo $NAME |
if, else, fi | 조건문 | if [ $a -gt 10 ]; then ... fi |
for, done | 반복문 | for i in {1..5}; do echo $i; done |
GitHub 안에서 자동으로 실행되는 작업(workflow)을 정의하는 CI/CD 도구
동작 구조 (핵심 개념 3가지)
.github/workflows/ 폴더에 .yml 파일로 정의됨# .github/workflows/test.yml
name: Run tests
on:
push: # 코드를 push할 때 실행
branches: [main]
pull_request: # PR이 열릴 때도 실행
branches: [main]
jobs:
test:
runs-on: ubuntu-latest # 실행 환경 (가상머신)
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install poetry
poetry install
- name: Run tests with pytest
run: |
poetry run pytest --cov=app --cov-report=term-missing

| 액션 이름 | 기능 | 설명 |
|---|---|---|
actions/checkout | 코드 가져오기 | 리포지토리 코드 복제 |
actions/setup-python | 파이썬 버전 설정 | Python 환경 자동 설치 |
actions/cache | 캐시 저장 | pip, poetry 등 설치 속도 향상 |
codecov/codecov-action | 테스트 커버리지 업로드 | Coverage 결과 시각화 |
appleboy/scp-action | 서버 배포 | SSH로 파일 전송 |
docker/login-action | 도커 로그인 | Docker Hub 배포용 |
/v1, /v2 같은 API 변경 사항 추적| 표준 | 설명 | 특징 |
|---|---|---|
| OpenAPI (Swagger) | 가장 널리 사용되는 REST API 명세 | JSON/YAML 형식, 자동 문서화 가능 |
| AsyncAPI | 메시지 기반 시스템 (Kafka, MQTT 등) 명세 | 비동기 통신용 |
| GraphQL SDL | GraphQL 스키마 정의 언어 | 쿼리 구조 중심 |
| gRPC ProtoBuf | gRPC 서비스 명세용 | 고성능, 정적 타입 기반 |
API 스펙으로 변환| 사용처 | 설명 |
|---|---|
| Swagger UI | /docs 페이지에서 스펙을 시각적으로 보여줌 |
| Postman Import | OpenAPI JSON을 불러와 자동 테스트 생성 |
| Front-end TypeScript 코드 생성기 | OpenAPI → Axios API client 자동 생성 |
| CI/CD 파이프라인 | 스펙 변경 시 자동 검증 가능 |
| 문서 자동 배포 | GitHub Pages, ReDocly 같은 문서 사이트로 바로 변환 가능 |
| 역할 | 써드 파티 라이브러리 | 설명 |
|---|---|---|
| 웹 프레임워크 | fastapi | API 서버 |
| 비동기 서버 | uvicorn | ASGI 실행기 |
| ORM | sqlalchemy, tortoise-orm | DB 모델 관리 |
| 타입 검사 | mypy | 정적 타입 검사 |
| 코드 포매터 | black, isort | 코드 정렬 및 스타일링 |
| 테스트 | pytest, coverage | 자동화 테스트 |
| 환경 설정 | python-dotenv, pydantic-settings | .env 파일 관리 |
uvicorn main:app --reload 서버와 클라이언트는 JSON, XML 같은 텍스트 형식으로 주고받음 : 직렬화 없으면 주고받기 X
직렬화(Serialization)
역직렬화(Deserialization)
from sqids import Sqids| 기능 | 설명 |
|---|---|
| Encode / Decode | 숫자 혹은 숫자 배열 → 문자열 ID로 변환, 다시 역변환 가능 |
| 블록리스트(blocklist) | 미리 정의된 부적절한 단어가 ID에 포함되지 않도록 필터링 |
| 최소 길이(min length) | 생성되는 ID가 적어도 설정한 길이 이상이 되도록 보장 |
| 사용자 정의 알파벳 | 기본 문자집합 외에 다른 문자 집합을 사용해서 ID 생성 가능 |
| 비암호화 방식 | Sqids는 암호화(encryption)가 아니며 정보 은폐용 설계는 아니에요 — 즉, 완전히 숨기려는 데이터에는 적합하지 않음 |
| 비순차 출력 | 연속 숫자를 인코딩해도 결과가 순차적으로 보이지 않게 설계 가능 |
설치: poetry add orjsonorjson.dumps()는 문자열이 아니라 bytes를 반환orjson.dumps(data).decode()
| 옵션 | 설명 |
|---|---|
orjson.OPT_INDENT_2 | 보기 좋게 들여쓰기 (개발용) |
orjson.OPT_SORT_KEYS | 키 이름을 정렬해서 출력 |
orjson.OPT_NAIVE_UTC | timezone 없는 datetime을 UTC 기준으로 직렬화 |
orjson.OPT_SERIALIZE_NUMPY | numpy 배열을 자동 변환 |
orjson.OPT_PASSTHROUGH_DATACLASS | dataclass 객체를 직렬화 |
orjson.OPT_SERIALIZE_UUID | UUID 객체 지원 |
orjson.OPT_OMIT_MICROSECONDS | datetime의 마이크로초 제거 |
from typing import Final
PI: Final = 3.14159
PI = 3 # ❌ mypy에서 오류 발생!
from typing import Final
class Config:
VERSION: Final = "1.0.0"
cfg = Config()
cfg.VERSION = "2.0.0" # ❌ 타입 검사기에서 오류 발생
from typing import final
class Base:
@final
def save(self):
print("Saving...")
class Sub(Base):
def save(self): # ❌ mypy 오류: Cannot override final method "save"
print("Override")
from pydantic import BaseModel
class Config(BaseModel):
id: int
name: str
class Config:
frozen = True
cfg = Config(id=1, name="Kihoon")
cfg.name = "Lee" # ❌ 실제로 에러 발생 (AttributeError)
import base64
text = "kihoon"
encoded = base64.b64encode(text.encode())
print(encoded) # b'a2lob29u'
decoded = base64.b64decode(encoded)
print(decoded.decode()) # kihoon
import base62
num = 123456789
encoded = base62.encode(num)
print(encoded) # 8M0kX
decoded = base62.decode(encoded)
print(decoded) # 123456789
from fastapi import FastAPI
app = FastAPI(debug=True)
from tortoise import Tortoise, fields, models
class User(models.Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=50)
is_active = fields.BooleanField(default=True)
# 연결 초기화
await Tortoise.init(
db_url="sqlite://db.sqlite3",
modules={"models": ["app.models"]}
)
await Tortoise.generate_schemas()
# 데이터 추가
user = await User.create(username="kihoon")
# 조회
users = await User.all()
print(users)
from cryptography.hazmat.primitives import hashes
digest = hashes.Hash(hashes.SHA256())
digest.update(b"kihoon123")
hash_value = digest.finalize()
print(hash_value.hex())
# e.g. '4e3f78a6df...'
from cryptography.fernet import Fernet
# 키 생성
key = Fernet.generate_key()
cipher = Fernet(key)
# 암호화
token = cipher.encrypt(b"secret message")
print(token)
# 복호화
plain = cipher.decrypt(token)
print(plain)
aerich init - 마이그레이션 설정 생성aerich init -t app.models.TORTOISE_ORM - 초기화aerich init-db - 첫 DB 생성 및 마이그레이션 폴더 생성aerich migrate - 모델 변경 감지 후 마이그레이션 파일 생성 aerich migrate --name "add-user-table" - 변경사항 감지aerich upgrade - DB 변경사항 적용TORTOISE_ORM = {
"connections": {"default": "sqlite://db.sqlite3"},
"apps": {
"models": {
"models": ["app.models", "aerich.models"],
"default_connection": "default",
},
},
}
$ aerich migrate --name "add_age_column"
$ aerich upgrade
📦 project/
┣ 📂 alembic/
┃ ┣ 📂 versions/ ← 버전별 migration 파일 저장
┃ ┗ 📄 env.py ← 환경 설정 파일
┣ 📄 alembic.ini ← 전체 설정
┗ 📄 models.py ← SQLAlchemy 모델 정의
| 개념 | 설명 |
|---|---|
| Migration | 데이터베이스 구조를 변경하는 작업 (테이블 생성/삭제, 컬럼 추가 등) |
| Revision | 변경 내역을 하나의 버전으로 저장한 파일 (versions/ 폴더에 있음) |
| Upgrade / Downgrade | 스키마를 새로운 버전으로 올리거나(Upgrade), 이전 버전으로 되돌리는(Downgrade) 과정 |
| env.py | Alembic 환경 설정 파일 (DB 연결, 대상 메타데이터 설정 등) |
| alembic.ini | Alembic 전역 설정 파일 (DB URL, 로그 설정 등) |
| 명령어 | 설명 |
|---|---|
alembic init alembic | Alembic 환경 초기화 |
alembic revision -m "Add user table" | 새 migration 파일 생성 |
alembic revision --autogenerate -m "Add user table" | 모델 변경사항 자동 감지 후 migration 생성 |
alembic upgrade head | 최신 버전까지 DB 스키마 업그레이드 |
alembic downgrade -1 | 바로 이전 버전으로 되돌림 |
alembic current | 현재 DB 스키마 버전 확인 |
alembic history | 모든 revision 내역 확인 |


poetry add -D pytest pytest-asyncio# conftest.py
import pytest
from tortoise import Tortoise
@pytest.fixture(scope="session", autouse=True)
async def init_db():
"""
모든 테스트가 시작되기 전에 1회 실행되어
Tortoise ORM 환경을 초기화하고 메모리 DB를 준비함.
"""
await Tortoise.init(
db_url="sqlite://:memory:", # 테스트용 메모리 DB
modules={"models": ["app.tortoise_models.user_model"]},
)
await Tortoise.generate_schemas() # 테이블 자동 생성
yield # 테스트 실행 구간
await Tortoise.close_connections() # 테스트 끝나면 연결 닫기
# test_user.py
import pytest
from app.tortoise_models.user_model import User # class User()
@pytest.mark.asyncio
async def test_user_creation():
user = await User.create(username="kim", email="test@example.com")
assert user.id is not None
fetched = await User.get(username="kim")
assert fetched.email == "test@example.com"
# conftest.py
import pytest
from httpx import AsyncClient
from tortoise import Tortoise
from app.main import app # FastAPI 앱 import
@pytest.fixture(scope="session", autouse=True)
async def init_db():
await Tortoise.init(
db_url="sqlite://:memory:",
modules={"models": ["app.tortoise_models.user_model"]},
)
await Tortoise.generate_schemas()
yield
await Tortoise.close_connections()
@pytest.fixture()
async def client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
# 이제 이렇게 간단하게 사용 가능
@pytest.mark.asyncio
async def test_user_api(client):
response = await client.post("/users", json={"username": "kim", "email": "a@a.com"})
assert response.status_code == 201

| 항목 | mypy | dmypy |
|---|---|---|
| 실행 방식 | 매번 새로 실행 | 백그라운드 데몬이 지속 실행 |
| 속도 | 느림 (모든 파일 재검사) | 빠름 (변경된 부분만 검사) |
| 사용 편의성 | 단순 실행 | 초기 설정 필요 |
| 권장 사용 | 소규모 스크립트 | 중·대형 프로젝트 |
# bash
poetry add --group dev mypy
poetry run dmypy start
poetry run dmypy run app/
# 결과 예시
Daemon started
Success: no issues found in 10 source files
변수 := 표현식print(x := 10)# while 문
line = input()
while line != "quit":
print(f"입력: {line}")
line = input()
# input() 결과를 line에 저장하면서 동시에 비교 조건에 사용.
while (line := input()) != "quit":
print(f"입력: {line}")
# list comprehension
# 길이가 3 이상인 단어만 리스트로 추출
words = ["a", "bee", "dog", "elephant"]
result = [w for w in words if (n := len(w)) > 2]
print(result) # ['bee', 'dog', 'elephant']
# if 조건문에서 계산과 저장을 동시에
if (count := len([1, 2, 3, 4])) > 2:
print(f"{count}개의 항목이 있습니다.")
| 단계 | 이름 | 설명 |
|---|---|---|
| 🔴 Red | 실패하는 테스트 작성 | 기능 요구사항을 기반으로 먼저 테스트를 작성함. 아직 구현이 안 됐기 때문에 반드시 실패해야 함. |
| 🟢 Green | 테스트 통과시키기 위한 최소한의 코드 작성 | 테스트를 “통과시키는 것”만 목표로 최소한의 코드 작성. (예쁘게 짜지 않아도 됨) |
| 🔵 Refactor | 코드 리팩터링 (중복 제거, 구조 개선) | 테스트가 통과하는 상태를 유지하면서, 코드를 개선하고 정리함. |
def test_create_user(client):
response = client.post("/users", json={"username": "kim", "password": "1234"})
assert response.status_code == 201
@app.post("/users", status_code=201)
def create_user(data: dict):
return {"id": 1, "username": data["username"]}
app/
├── api/
│ └── user_router.py # FastAPI 엔드포인트
├── services/
│ └── user_service.py # 비즈니스 로직 (Service Layer)
├── repositories/
│ └── user_repository.py # DB 접근 (Repository Layer)
└── models/
└── user_model.py
- Controller(API): 요청/응답 처리 (입출력 담당) = 요청받음
- Service: 비즈니스 로직 담당 (검증, 계산, 규칙, 정책) = 로직 처리
- Repository: 데이터 저장소 접근 담당 (DB, 외부 API 등) = 데이터 CRUD 수행
# user_router.py
from fastapi import APIRouter
from app.services.user_service import UserService
router = APIRouter()
service = UserService()
@router.post("/users")
async def create_user(data: dict):
return await service.create_user(data)
# user_service.py
from app.repositories.user_repository import UserRepository
class UserService:
def __init__(self):
self.repo = UserRepository()
async def create_user(self, data):
# 비즈니스 로직 예시
if len(data["password"]) < 6:
raise ValueError("비밀번호는 6자 이상이어야 합니다.")
user = await self.repo.insert_user(data)
return {"message": "회원가입 완료", "user": user}
# user_repository.py
from app.models<.user_model import User
class UserRepository:
async def insert_user(self, data: dict):
user = await User.create(**data)
return user
async def find_user_by_id(self, user_id: int):
return await User.get_or_none(id=user_id)
async def delete_user(self, user_id: int):
return await User.filter(id=user_id).delete()
# user_service.py (연동 예시)
from app.repositories.user_repository import UserRepository
class UserService:
def __init__(self):
self.repo = UserRepository()
async def get_user(self, user_id: int):
user = await self.repo.find_user_by_id(user_id)
if not user:
raise ValueError("존재하지 않는 사용자입니다.")
return user
# schemas/user_dto.py
from pydantic import BaseModel, EmailStr
class UserCreateDTO(BaseModel): # Request DTO
username: str
email: EmailStr
password: str
class UserResponseDTO(BaseModel): # Response DTO
id: int
username: str
email: EmailStr
# user_router.py
from fastapi import APIRouter
from app.schemas.user_dto import UserCreateDTO, UserResponseDTO
from app.services.user_service import UserService
router = APIRouter()
service = UserService()
@router.post("/users", response_model=UserResponseDTO)
async def create_user(data: UserCreateDTO):
user = await service.create_user(data)
return user
# user_service.py
from app.repositories.user_repository import UserRepository
from app.schemas.user_dto import UserCreateDTO, UserResponseDTO
class UserService:
def __init__(self):
self.repo = UserRepository()
async def create_user(self, data: UserCreateDTO) -> UserResponseDTO:
# 검증 or 비즈니스 로직
if len(data.password) < 6:
raise ValueError("비밀번호는 6자 이상이어야 합니다.")
user = await self.repo.insert_user(data)
# Repository 결과를 ResponseDTO로 변환
return UserResponseDTO.model_validate(user)