DOMAIN
ㄴ service.py --> 비즈니스 로직이 작성되는 스크립트
ㄴ controller.py --> 엔드포인트들이 작성되는 스크립트
ㄴ utils.py --> 비즈니스 로직에서 사용되는 함수들이 작성되는 스크립트
ㄴ schema.py --> 입출력 형태를 검증하는 class들이 작성되는 스크립트
위의 형태로 각 도메인들을 채워나갈 것이다.
from fastapi import FastAPI
from pydantic import BaseModel
class ItemsReturn(BaseModel):
start: int
limit: int
class AddItem(BaseModel):
name: str
price: int
# FastAPI 앱 설정
app = FastAPI()
# app에 'get' 메소드로 http://{host}:{port}/hello에 접근하겠다.
@app.get("/hello")
async def print_hello():
return {"message": "Hello, World!"}
# app에 'get' 메소드로 http://{host}:{port}/{id}에 접근하겠다.
@app.get("/{id}")
async def print_id(id: int):
return {"id": id}
# app에 'post' 메소드로 http://{host}:{port}/items에 접근하겠다.
# 반환되는 상태 코드를 201로 하겠다.
@app.post("/items",status_code=201)
# 받아오는 Body 형태를 AddItem에 맞추겠다.
async def add_item(item: AddItem):
return {"name": item.name, "price": item.price}
# app에 'get' 메소드로
# http://{host}:{port}/items/?start={start}&limit={limit}에 접근하겠다.
# 반환 형태를 ItemsReturn에 맞추겠다.
# 반환되는 상태 코드를 200으로 하겠다.
@app.get("/items/", response_model=ItemsReturn, status_code=200)
async def print_item(start: int, limit: int):
return {"start": start, "limit": limit}
이를 main.py로 저장하고
콘솔창에서 아래 구문을 실행시켜보자.
uvicorn main:app --host 0.0.0.0
정상적으로 작성했다면, FastAPI 서버가 실행되었을 것이다.
http://localhost:8000/docs 로 들어가면 자동으로 Swagger UI가 생성되어있는 것을 볼 수 있다.
API
ㄴ __init__.py
ㄴ v1
ㄴ __init__.py
이 __init__.py
에다가 APIRouter로 router를 만들어 main에서 선언한 app에 추가하는 방식으로 엔드포인트를 선언하여 유지보수를 더 쉽게 할 수 있다.
__init__.py
from fastapi import APIRouter
from . import v1
router = APIRouter()
router.include_router(v1.router, prefix="/v1")
__init__.py
from fastapi import APIRouter
from .user import controller as user
router = APIRouter()
router.include_router(user.router, prefix="/users", tags=["User"])
prefix --> router에 포함되어 있는 엔드포인트 앞에 붙여줄 전치사
tags --> Swagger에서 tag 아래로 정렬해준다
경상도 사투리를 번역해서 보여주는 웹사이트에 회원가입 및 로그인 기능이 반드시! 필요하냐라고 물어보면 자신있게 예쓰라고 대답하긴 힘들지만, 추가하고자 하는 기능 중에 하나(
아직 구현하지 않았다)가 사용자의 피드백을 통해서 추가 학습까지 가능한 파이프라인을 구축하는 것이기 때문에 익명으로 받아오는 정보를 사용하기보다는 회원가입으로 추적 가능한 데이터를 사용하는게 더 나을 것 같아서 구현해보았다.
APP/
ㄴ models.py --> 작업할 공간
# 이전 상태에서 추가로 import 해줘야 할 라이브러리들.
from sqlalchemy import String, Boolean
from sqlalchemy.orm import relationship
class User(BaseMin, Base):
__tablename__ = "user"
email = Column(String(30), nullable=False, unique=True)
password = Column(String(255), nullable=False)
is_provider = Column(Boolean, default=True)
items = relationship("TsItem", back_populates="owner")
guestbooks = relationship("GuestBook", back_populates="book_owner")
def as_dict(self):
return {column.name: getattr(self, column.name) for column in self.__table__.columns}
User 테이블은 BaseMin도 상속받았기 때문에, id, created_at, updated_at은 자동으로 생성된다.
nullable=False --> 반드시 존재해야 하는 값
unique=True --> 테이블에서 하나만 존재해 하는 값
default=True --> 별도로 지정해주지 않았을 때 기본으로 저장되는 값
APP/
ㄴ API/
ㄴ USER/ --> 작업할 공간
ㄴ service.py
ㄴ controller.py
ㄴ utils.py
ㄴ schema.py
from fastapi import APIRouter, status, Depends
from sqlalchemy.orm.session import Session
from app.database import get_db
from .schema import UserAdd, UserAddReturn, DuplicatedEmail
from .service import userAdd, userLogin, emailDuplicated
router = APIRouter()
# 회원가입
@router.post("/register", response_model=UserAddReturn, status_code=status.HTTP_201_CREATED)
async def add_user(data: UserAdd, db: Session = Depends(get_db)):
return await userAdd(data, db)
# 로그인
@router.post("/login")
async def issue_token(data: UserAdd, db: Session = Depends(get_db)):
return await userLogin(data, db)
# 이메일 중복 여부 확인
@router.get("/duplicated", status_code=status.HTTP_200_OK)
async def is_duplicated(data: DuplicatedEmail = Depends(), db: Session = Depends(get_db)):
return await emailDuplicated(data, db)
Depends()
엔드포인트에서 매개변수로 Depends로 함수를 전달해주면,
FastAPI가 자동으로 함수를 실행시켜 엔드포인트에 전달해준다.
database.py의 get_db를 살펴보도록 하자.
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
database.py의 윗줄에서 생성한 SessionLocal의 인스턴스를 생성해서 넘겨준다. 우리는 이 세션을 통해서 MySQL과 통신할 것이다!
### 회원가입 ###
@router.post("/register", response_model=UserAddReturn, status_code=status.HTTP_201_CREATED)
async def add_user(data: UserAdd, db: Session = Depends(get_db):
return await userAdd(data, db)
@router.post("/login")
async def issue_token(data: UserAdd, db: Session = Depends(get_db)):
return await userLogin(data, db)
@router.get("/duplicated", status_code=status.HTTP_200_OK)
async def is_duplicated(data: DuplicatedEmail = Depends(), db: Session = Depends(get_db)):
return await emailDuplicated(data, db)
pip install bcrypt
import bcrypt
from fastapi.responses import JSONResponse
from .utils import (
add_user,
find_user_by_email,
create_access_token,
is_password_correct,
is_duplicated,
)
async def userAdd(data, db):
salt_value = bcrypt.gensalt()
pw = bcrypt.hashpw(data.password.encode(), salt_value)
row = await add_user(data.email, pw, db)
return row.as_dict()
async def userLogin(data, db):
user = await find_user_by_email(data.email, db)
if await is_password_correct(data.password, user.password):
token, user_id = await create_access_token(user)
return JSONResponse(content={"user_id": user_id}, headers={"access_token": token})
async def emailDuplicated(data, db):
return {"duplicated": await is_duplicated(data.email, db)}
async def userAdd(data, db):
salt_value = bcrypt.gensalt()
pw = bcrypt.hashpw(data.password.encode(), salt_value)
row = await add_user(data.email, pw, db)
return row.as_dict()
async def userLogin(data, db):
user = await find_user_by_email(data.email, db)
if await is_password_correct(data.password, user.password):
token, user_id = await create_access_token(user)
return JSONResponse(content={"user_id": user_id}, headers={"access_token": token})
find_user_by_email
함수에서 body에 담겨온 email로 models.User 객체를 반환한다.is_password_correct
함수로 비밀번호가 일치하는지 확인한다.create_access_token
함수로 JWT 토큰과 user_id를 반환한다.JWT 토큰에 관련된 설명은 https://velog.io/@chuu1019/%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-JWTJson-Web-Token 이곳에 잘 되어 있으니 확인해보길 바란다.
async def emailDuplicated(data, db):
return {"duplicated": await is_duplicated(data.email, db)}
is_duplicated
함수에서 body에 담겨온 email로 중복 여부를 판단해 boolean값을 딕셔너리에 담아 반환한다.import bcrypt
from datetime import datetime, timedelta
from fastapi import HTTPException, status
from jose import jwt
from app.config import settings
from app.models import User
from .schema import UserPayload
async def add_user(email, pw, db):
try:
row = User(**{"email": email, "password": pw})
db.add(row)
db.commit()
return row
except Exception as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{e} occured while registering",
)
async def find_user_by_email(email, db):
row = db.query(User).filter_by(email=email).first()
if row:
return row
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No Email Found",
)
async def is_password_correct(data, user):
if bcrypt.checkpw(data.encode(), user.encode()):
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Incorrect Password",
)
async def create_access_token(user):
user_schema = user.as_dict()
expire = datetime.utcnow() + timedelta(days=1)
user_info = UserPayload(**user_schema, exp=expire)
return (
jwt.encode(user_info.dict(), settings.SECRET_KEY, algorithm=settings.ALGORITHM),
user_schema["id"],
)
async def is_duplicated(email, db):
if db.query(User).filter_by(email=email).first():
return True
else:
return False
async def add_user(email, pw, db):
try:
row = User(**{"email": email, "password": pw})
db.add(row)
db.commit()
return row
except Exception as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{e} occured while registering",
)
async def find_user_by_email(email, db):
row = db.query(User).filter_by(email=email).first()
if row:
return row
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No Email Found",
)
async def is_password_correct(data, user):
if bcrypt.checkpw(data.encode(), user.encode()):
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Incorrect Password",
)
async def create_access_token(user):
user_schema = user.as_dict()
expire = datetime.utcnow() + timedelta(days=1)
user_info = UserPayload(**user_schema, exp=expire)
return (
jwt.encode(user_info.dict(), settings.SECRET_KEY, algorithm=settings.ALGORITHM),
user_schema["id"],
)
지난번 .env 파일에는 SECRET_KEY와 ALGORITHM이 정의되어 있지 않으므로 추가해줘야 할 것이다.
async def is_duplicated(email, db):
if db.query(User).filter_by(email=email).first():
return True
else:
return False
from datetime import datetime
from pydantic import BaseModel
class DuplicatedEmail(BaseModel):
email: str
class UserAdd(DuplicatedEmail):
password: str
class UserAddReturn(DuplicatedEmail):
id: int
class UserPayload(UserAddReturn):
exp: datetime
위의 내용들을 잘 따라왔다면 사진처럼 Swagger에서 확인이 될 것이다.
필자는 리팩토링을 거치고 난 뒤의 모습이라 UserV2 태그의 v2로 보이지만, 게시글들을 잘 따라왔다면 User 태그의 /v1 prefix로 보일 것이다.
테스트 코드는 API들을 작성한 뒤 마지막에 한 번에 쭉쭉 업로드하겠다.