Profile picture

[Python] FastAPI와 SQLite를 활용하여 ORM 기반 인증 서버 구현해보기!

JaehyoJJAng2025년 01월 10일

1. 개요

저번주에 요청 받았던 의뢰 작업에 대한 내용을 간단하게 공유해보려고 합니다.


의뢰자분께서는 사내에서 영업부 팀원들이 사용할 수 있는 메일 전송 자동화 프로그램이 필요하다고 하셨어요!


의뢰자님께서 다음과 같은 기능이 필요하다고 하셨어요.

  • 보낼 이메일 주소를 적을 수 있는 칸 (여러 이메일 입력 가능해야함)
    • 또는 엑셀에서 불러올 수 있도록.
  • 이메일 내용 양식 설정
    • 의뢰자 내용에 맞춰서 HTML 형식의 템플릿을 생성하였음.
    • 원하는 템플릿을 선택하여 고를 수 있도록 해야함.
    • 템플릿을 적용하고 작성한 내용에 대한 "미리보기" 기능이 있어야 함.

그다지 복잡하지 않은 기능이었기에 쉽게 작업이 가능했습니다!


그리고 한 달 정도 지난 후에 의뢰자님께서 다음과 같은 부탁을 하셨어요.

Image

의뢰자님이 다음과 같은 기능이 필요하다고 하십니다!

  • 프로그램을 사용할 수 있는 권한을 부여.
    • 배포한 프로그램에 사용자별로 인증된 코드를 입력해야 프로그램 사용이 원활하게 가능함.
  • 사용자를 등록할 수 있는 관리자 프로그램
    • 사용자별 인증 코드, 유효기간 설정, 사용자 이름 지정, 인증 키 등록

의뢰자님이 제시한 내용에 맞춰서 다음처럼 UI를 제시해드렸더니
Image
바로 구현해달라고 하셨습니다 !~


이렇게 인증 계정을 생성하기 위한 부분과

생성한 계정에 대한 인증 코드를 활성화하는 부분으로 분리하여

사용자의 편의성과 가독성을 개선해봤어요!


이러한 서사를 기반으로

이번 포스팅에서 인증 부분을 어떻게 구현했는지 기록해보려고 합니다.


2. 프로젝트 구현 목표

기능

  • 1. 사용자(클라이언트) 등록
    • Activation Code, 만료 기간(Validity), 상태(Inactive/Activated)를 DB에 저장
  • 2. Activation Code 활성화
    • Activation Code를 입력받아 사용자의 상태를 Activated로 변경
  • 3. 코드 검증
    • 다른 프로그램(또는 GUI)에서 Activation Code가 유효(Activated 및 기간 이내)한지 확인

구현 스택

  • FastAPI: 서버(REST API) 구현
  • SQLite + SQLAlchemy: DB 및 ORM (Object Relational Mapping)
  • PySide6: 데스크톱 GUI (탭1: 사용자 리스트 / 탭2: Activation Code 활성화)

해당 예시에서는 “인증 로직은 간단히 Activation Code가 있는지, 활성화 상태인지, 기간은 유효한지 정도만 확인” 하는 정도로 마무리 해보려고 합니다.


추가로, REST API 보안을 위해서는 HTTPS, 토큰 인증(OAuth2) 등의 방법들을 꼭 적용해주어야 합니다 !~


3. 라이브러리 설치

  • fastapi
  • uvicorn: 개발용 서버 실행
  • sqlalchemy
  • pydantic
  • PySide6
pip install fastapi uvicorn sqlalchemy pydantic PySide6

4. 데이터베이스와 ORM

4-1. SQLite 선택

  • SQLite는 별도의 DB 서버 설치 없이 파일 하나(.db)로 데이터를 관리할 수 있기에, 테스트나 소규모 프로젝트에 아주 적합해요!

4-2. SQLAlchemy ORM

  • ORM(Object Relational Mapping): 파이썬 클래스(Model)를 통해 SQL 테이블을 다루는 방식이에요.
    • 장점: SQL 쿼리를 직접 다루지 않고, 파이썬 코드로 DB 연동이 가능해요(조회, 추가, 수정, 삭제)
    • 단점: 복잡한 쿼리나 대규모 트래픽 상황에서는 SQL문을 작성하는 것보다 성능/최적화가 다소 떨어질 수 있어요.

5. FastAPI 서버 코드

아래는 server.py 예시 코드입니다.

(1) ORM 모델 -> (2) Pydantic 모델 -> (3) DB 연결 -> (4) 앤드포인드 라우팅 형태로 이루어집니다.


5-1. 코드 자세히 살펴보기

5-1-1. SQLAlchemy ORM 모델

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    activation_code = Column(String, unique=True, index=True)
    client_name = Column(String)
    validity_period = Column(DateTime)
    activation_status = Column(String)
  • __tablename__ = "users": 실제 DB 테이블 이름을 지정.
  • id = Column(Integer, primary_key=True): 고유 ID (자동 증가).
  • activation_code = Column(String, unique=True): 중복 불가 (unique=True).
  • validity_period = Column(DateTime): 만료 시각.
  • activation_status = Column(String): “Inactive” / “Activated” 등을 저장.

5-1-2. Pydantic 모델

class UserCreateRequest(BaseModel):
    """
    - /create_user 엔드포인트에서 클라이언트가 전달하는 데이터 구조
    - client_name (str), validity_days (int)
    """
    client_name: str
    validity_days: int

class UserResponse(BaseModel):
    """
    - /create_user, /users 등에서 서버가 클라이언트에게 돌려주는 응답 구조
    - activation_code, client_name, validity_period, activation_status
    """
    activation_code: str
    client_name: str
    validity_period: datetime
    activation_status: str

class ActivateRequest(BaseModel):
    """
    - /activate_user 엔드포인트에서
      클라이언트가 Activation Code를 보낼 때 사용하는 구조
    """
    activation_code: str
  • UserCreateRequest: API 요청(Request) 바디 형식 정의.
    • -> client_name, validity_days
  • UserResponse: API 응답(Response) 형식 정의.
    • 이 모델로부터 필드 타입이 자동 유추되며, API 문서(Swagger)에서 확인 가능해요.

이처럼 “DB 모델”“Pydantic 모델” 은 별도입니다.

  • DB 모델은 ORM용(실제 DB 테이블 스키마)
  • Pydantic 모델은 FastAPI가 입출력 검증에 사용

5-1-3. DB 세션 종속성 (get_db())

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
  • Depends(get_db) 를 사용하면, 엔드포인트가 실행될 때마다 DB 세션을 열고 (db = SessionLocal()), 함수 완료 후 자동으로 세션을 닫아줍니다.
  • 덕분에 각 엔드포인트 함수(예: create_user) 안에서는 별도의 세션 관리 코드 없이 db를 안전하게 사용할 수 있어요!

5-1-4. 예외 처리 (HTTPException)

if not user:
    raise HTTPException(status_code=404, detail="Invalid Activation Code")
  • FastAPI에서의 표준적인 예외 처리 방식입니다:
    • raise HTTPException(status_code=..., detail="...")
  • 이렇게 하면 “status_code=404”와 함께 JSON 응답 {"detail": "Invalid Activation Code"}가 자동으로 반환됩니다!

5-1-5. response_model

@app.post("/create_user", response_model=UserResponse)
def create_user(...):
    ...
    return UserResponse(...)
  • 엔드포인트가 반환할 데이터 구조(JSON)를 Pydantic 모델(UserResponse)로 명시합니다.
  • 이 경우, FastAPI는 반환되는 데이터가 UserResponse 스키마에 맞는지 자동 검증해요.
  • 스키마에 없는 필드(예: password)가 포함되면 제외되거나(설정에 따라), 타입이 다르면 422 오류(Validation Error)가 발생합니다!
  • 보통 민감 정보 필터링, 출력 형식 통일 등을 위해 꼭 사용하는 것을 권장해요!

6. API 테스트

구현해본 API의 앤드포인트별 테스트를 진행해볼게요!

먼저 test.pyrequests 라이브러리를 임포트 해주세요.

import requests as rq

6-1. 인증 유저 생성하기 (/create_user)

새로운 인증 유저를 생성해볼게요.

테스트 코드는 다음과 같이 작성했어요.

def create_user(client_name: str, validity_days: int) -> None:
    url = "http://127.0.0.1:8000/create_user"

    # CreateUserRequest
    payload = {"client_name": client_name, "validity_days": validity_days}
    resp = rq.post(url=url, json=payload)
    print(resp.content)

# 생성할 client_name, validity_days 지정
client_name = "jaehyo"
validity_days = 7

# 인증 유저 생성
create_user(client_name=client_name, validity_days=validity_days)

응답 값을 확인해봅시다

b'{"activation_code":"a968ea-56ed-870a16b","client_name":"jaehyo","validity_period":"2025-02-18T15:13:40.057911","activation_status":"Inactive"}'

6-2. 인증 유저 조회하기 (/users)

생성한 인증 유저를 조회해볼게요.

테스트 코드는 다음과 같이 작성했어요.

def load_user() -> None:
    url = "http://127.0.0.1:8000/users"

    resp = rq.get(url=url)
    print(resp.content)

load_user()

응답 값을 확인해봅시다.

b'[{"activation_code":"a968ea-56ed-870a16b","client_name":"jaehyo","validity_period":"2025-02-18T15:13:40.057911","activation_status":"Inactive"}]'

6-3. 인증 활성화하기 (/activate_user)

특정 인증 코드를 가지고 있는 유저의 인증을 활성화 해볼게요.

테스트 코드는 다음과 같이 작성했어요.

def activate_user(activate_code: str) -> None:
    url = "http://127.0.0.1:8000/activate_user"
    data = {"activation_code": activate_code}
    resp = rq.post(url=url, json=data)
    print(resp.content)

# load_user() 함수에서 출력된 유저의 activation_code를 복사한 후 아래 변수에 넣으세요.
activation_code = "a968ea-56ed-870a16b"

# 인증 활성화
print("=== 유저 인증 활성화 ===")
activate_user(activate_code=activation_code)

# 유저 정보 조회하기
print("=== 유저 정보 ===")
load_user()

응답 값을 확인해봅시다!

=== 유저 인증 활성화 ===
b'{"result":"success","activation_code":"a968ea-56ed-870a16b","status":"Activated"}'
=== 유저 정보 ===
b'[{"activation_code":"a968ea-56ed-870a16b","client_name":"jaehyo","validity_period":"2025-02-18T15:13:40.057911","activation_status":"Activated"}]'

유저의 activation_status가 정상적으로 Activated로 변경되었네요!


6-4. 유저 인증 비활성화 (/deactivate_user)

이번에는 유저의 인증을 비활성화 해볼게요!

테스트 코드는 다음과 같이 작성했어요.

def deactivate_user(client_name: str) -> None:
    url = "http://127.0.0.1:8000/deactivate_user"
    data = {"client_name": client_name}
    resp = rq.post(url=url, json=data)
    print(resp.content)

# 비활성화할 유저 이름
client_name = "jaehyo"

# 유저 인증 비활성화
deactivate_user(client_name=client_name)

# 유저 정보 조회
load_user()

응답 값을 확인해봅시다!

b'{"result":"success","user":"jaehyo","status":"Inactive"}'
b'[{"activation_code":"a968ea-56ed-870a16b","client_name":"jaehyo","validity_period":"2025-02-18T15:13:40.057911","activation_status":"Inactive"}]'

6-5. 유저 삭제하기 (/delete_user)

이번에는 유저를 삭제 해볼게요!

테스트 코드는 다음과 같이 작성했어요.

def delete_user(client_name: str) -> None:
    url = f"http://127.0.0.1:8000/delete_user/{client_name}"
    resp = rq.delete(url=url)
    print(resp.json())

# 삭제할 유저명 지정
client_name = "jaehyo"

# 유저 삭제
delete_user(client_name=client_name)

# 유저 정보 조회
load_user()

응답 값을 확인해봐요!

b'{"result":"success","message":"User (client_name=jaehyo) has been deleted."}'
b'[]'

6-6. Activate Code 유효성 검증 (/verify_code/{activation_code})

  • 해당 테스트는 관리자 프로그램이 아닌, 배포된 GUI 프로그램에서 진행하는거에요 !~

이번에는 유저의 인증 코드 활성화 여부를 검증 해볼게요!

테스트 코드는 다음과 같이 작성했어요.

def verify_code(activation_code: str) -> None:
    url = f"http://127.0.0.1:8000/verify_code/{activation_code}"
    resp = rq.get(url=url)
    print(resp.content)

# 인증된 activation_code를 아래 변수에 지정해주세요.
activation_code = "49d459-2ad5-8d93835"

# 인증 여부 검증
verify_code(activation_code=activation_code)

응답 값을 확인해봐요!

b'{"valid":false,"reason":"Not Activated"}'

해당 유저는 인증 코드가 활성화 되지 않았기 때문에 위와 같은 응답이 나오고 있어요!


관리자 프로그램에서 해당 유저의 인증을 활성화하고 다시 실행해봅시다!

# 인증 활성화
activate_user(activate_code="49d459-2ad5-8d93835")

# 인증 여부 검증
verify_code(activation_code=activation_code)

응답 확인 해볼까요?

b'{"result":"success","activation_code":"49d459-2ad5-8d93835","status":"Activated"}'
b'{"valid":true}'

정상적으로 Activate 되었습니다!


Loading script...