Study/fastapi

fastapi - G6 간단하게 훓어보기 - 1) 설치

bluebamus 2025. 2. 19.

- 그동안의 학습의 여정을 마치고, 더 좋은 코딩 방법을 모아 보고자 벼뤄왔던 g6에 몇가지 기능에 대해서만 간단하게 살펴볼 생각이다. 프로젝트에 포함된 기능이 워낙 많기 때문에, 차후에 조금씩 부분 분석해 볼 예정이다.

- workflow에 대한 분석이 아닌, 코드 스닛펫(code snippet) 위주의 분석을 할 것이다.

- 공식 페이지 : https://sir.kr/main/g6/

- 다운로드 페이지 : https://sir.kr/g6_tags

- github 저장소 : https://github.com/gnuboard/g6

- 메뉴얼 : https://sir.kr/manual/g6/

 

1. 프로젝트 설치

   1) 설치 여부 검사

      - inspect(engine).has_table(prefix + "config"))는 SQLAlchemy를 사용하여 데이터베이스에 특정 테이블이 존재하는지 확인하는 코드이다.

 

      1. inspect(engine):

         - SQLAlchemy의 inspect 함수를 사용하면 데이터베이스의 메타데이터를 조사할 수 있는 Inspector 객체가 반환된다.          - inspect(engine)는 sqlalchemy.engine.reflection.Inspector 인스턴스를 반환하며, 이를 통해 특정 테이블의 컬럼, 인덱스, 제약조건 등의 메타데이터를 조회할 수 있다.

      2. has_table(prefix + "config"):

         - Inspector 객체의 has_table 메서드는 인자로 전달한 이름의 테이블이 데이터베이스에 존재하는지를 불리언 값(True/False)으로 반환한다. 여기서 prefix + "config"는 접두어(prefix)를 포함한 테이블 이름을 동적으로 생성하는 방식이다.

async def validate_install():
    """설치 여부 검사"""

    db_connect = DBConnect()
    engine = db_connect.engine
    prefix = db_connect.table_prefix

    if (os.path.exists(ENV_PATH)
            and inspect(engine).has_table(prefix + "config")):
        raise AlertException(
            "이미 설치가 완료되었습니다.\\n재설치하시려면 .env파일을 삭제 후 다시 시도해주세요.", 400, "/")

 

   2) dependant 와 dependencies의 차이

      - dependencies를 이용해 view 스코프가 아닌, router 스코프(view의 집합) 혹은 global app 스코프에 각각 dependencies를 설정할 수 있다.

 

Dependencies in path operation decorators

In some cases you don't really need the return value of a dependency inside your path operation function. Or the dependency doesn't return a value.
But you still need it to be executed/solved.
For those cases, instead of declaring a path operation function parameter with Depends, you can add a list of dependencies to the path operation decorator.

Origin

 

# App Scope Dependencies: 모든 요청에 대해 Depends를 실행합니다.
app = FastAPI(dependencies=[Depends(verify_key)])

@app.post(
	"/hello-world5/{path}",
	# View Scope Dependencies: View로 접수되는 Request에 대해서만 Depends를 실행합니다.
	dependencies=[Depends(verify_token)]
)
...

 

      - dependant 와 dependencies의 차이는 다음과 같다.

         - dependant는 view 관련 정보를 들고 있는 객체이다.

         - dependencies는 dependant의 attr로, view와 연관된 Depends 들을 들고 있는 list라 할 수 있다.

 

      - 주요 차이점

         1. 적용 범위:

            - dependencies:

               - 라우트 수준에서 전체적인 의존성을 지정하며, 모든 해당 라우트 내의 엔드포인트 함수에 적용된다.

            - Depends:

               - 함수 매개변수 수준에서 개별적으로 각 매개변수에 대한 의존성을 선언하며, 엔드포인트 함수 내의 각 매개변수에 대해 특화된 처리를 할 수 있다.

 

          2. 사용 방법:

            - dependencies: 

               - 데코레이터의 매개변수로 리스트 형태로 여러 의존성을 지정하여 한 번에 관리할 수 있다.
            - Depends:

               - 함수 매개변수의 기본값으로 사용되며, 필요에 따라 다양한 의존성을 선언할 수 있다.

 

          3. 기능:

            - dependencies: 

               - 모든 의존성을 해결한 후에 해당 라우트 내의 엔드포인트 함수들이 실행된다.

            - Depends: 

               - 각 매개변수의 의존성을 충족해야만 해당 매개변수에 대한 값을 받을 수 있으며, 이를 통해 코드의 모듈화와 재사용성을 높일 수 있다.

 

   3) 토큰 검증

      - 세션과 쿼리로 넘어온 토큰을 비교하여 확인을 한다. csrf 구조인데 이름을 ss_token으로 해서 세션 토큰으로 이해가 된다. csrf의 동작에 대해 제공해 주는 기능을 사용만 하다보니 직접 구현을 몰라서 헤메었다.

      - 토큰을 세션에 넣고, 폼을 만들때 히든으로 넣어 서버에 요청시 사용하게 만든다. 서버는 세션의 토큰과 폼에 있는 토큰을 비교해 변조가 있는지 확인한다.

 

      - 쿼리에서 토큰 가져오기

async def get_variety_tokens(
    token_form: Annotated[str, Form(alias="token")] = None,
    token_query: Annotated[str, Query(alias="token")] = None
):
    """
    요청 매개변수의 유형별 토큰을 수신, 하나의 토큰만 반환
    - 함수의 매개변수 순서대로 우선순위를 가짐
    """
    return token_form or token_query

 

      - 토큰 유효성 검사

async def validate_token(
    request: Request,
    token: Annotated[str, Depends(get_variety_tokens)]
):
    """토큰 유효성 검사"""
    if not check_token(request, token):
        raise AlertException("토큰이 유효하지 않습니다", 403)

 

      - 세션과 인수로 넘어온 토큰확인 함수

import secrets

from fastapi import Request

def create_session_token(request: Request):
    """
    토큰 생성 후 세션에 저장&반환

    Args:
        request (Request): FastAPI Request 객체

    Returns:
        str: 생성된 토큰
    """
    token = secrets.token_hex(16)  # 16바이트 토큰 생성
    request.session["ss_token"] = token  # 세션에 토큰 저장
    return token

def check_token(request: Request, token: str) -> bool:
    """세션과 인수로 넘어온 토큰확인 함수

    Args:
        request (Request): FastAPI Request 객체
        token (str): token 문자열

    Returns:
        bool: 토큰 일치 여부
    """
    if not token:
        return False

    token = token.strip()
    if token == request.session.get("ss_token"):
        # 세션 삭제
        request.session["ss_token"] = ""
        return True

    return False

 

 

   4) Base.metadata.tables.values()에 대하여

      1. Base.metadata.tables

         - 정의 및 역할:
            - Base는 보통 SQLAlchemy의 선언적 베이스(declarative base)로 생성된 객체이다.

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

 

            - 이 Base를 사용해 정의한 모델 클래스들은 내부적으로 자신의 테이블 정보를 Base.metadata에 등록한다.
               - Base.metadata는 MetaData 객체로, 데이터베이스 스키마(테이블, 컬럼, 제약조건 등)의 메타정보를 담고 있다.
               - Base.metadata.tables는 이 MetaData 객체가 관리하는 모든 테이블 정보를 사전(dict) 형태로 보관한다.
 

 

         - 반환값:

            - 자료구조: 파이썬의 dict

            - 키(Key): 테이블 이름(문자열, str)

            - 값(Value): 각 테이블에 해당하는 SQLAlchemy의 Table 객체

 

            - 예시:

{
    'users': <sqlalchemy.sql.schema.Table object at 0x...>,
    'addresses': <sqlalchemy.sql.schema.Table object at 0x...>,
     ...
}

 

            - 특징:

               - 모든 모델 클래스에서 선언한 테이블이 여기에 자동으로 등록된다.

               - 만약 아직 테이블이 정의되지 않았다면, 빈 딕셔너리({})가 된다.

 

      2. Base.metadata.tables.values()

         - 정의 및 역할:

            - Base.metadata.tables.values()는 위에서 설명한 딕셔너리의 값들만을 반환하는 메서드이다.
            - 즉, 테이블 이름을 키로 가지는 사전에서, 실제 Table 객체들만 모아서 iterable(반복 가능한 객체)로 반환한다.

 

         - 반환값:

            - 자료구조: dict_values 객체 (파이썬의 dict에서 values() 메서드를 호출하면 반환되는 뷰 객체)

 

            - 내용:

               - 이 객체에는 데이터베이스에 정의된 모든 테이블에 해당하는 Table 객체들이 포함된다.

 

            - 예시:

tables = Base.metadata.tables.values()
# tables는 다음과 유사한 iterable 객체입니다.
# [<Table 'users'>, <Table 'addresses'>, ...]

 

            - 특징:

               - dict_values는 리스트와 비슷하게 동작하며, 반복문 등에서 사용할 수 있다.
               - 만약 전체 테이블 객체를 리스트로 사용하고 싶다면, list(Base.metadata.tables.values())처럼 변환할 수 있다.

 

      3. 요약

         - Base.metadata.tables:

            - SQLAlchemy의 MetaData 객체에 등록된 모든 테이블 정보를 담은 사전이다.

            - 키는 테이블 이름, 값은 Table 객체이다.

 

         - Base.metadata.tables.values():

            - 위 사전에서 값들(즉, Table 객체들)만을 반환하는 뷰 객체이다.

            - 테이블 이름은 포함하지 않고, 테이블 자체만 필요할 때 유용하게 사용된다.

 

   5)동적 게시판 모델 생성 

      - 원본 코드

_created_models = {}

# 동적 게시판 모델 생성
def dynamic_create_write_table(
        table_name: str,
        create_table: bool = False,
    ) -> WriteBaseModel:
    '''
    WriteBaseModel 로 부터 게시판 테이블 구조를 복사하여 동적 모델로 생성하는 함수
    인수의 table_name 에서는 table_prefix + 'write_' 를 제외한 테이블 이름만 입력받는다.
    Create Dynamic Write Table Model from WriteBaseModel
    '''
    # 이미 생성된 모델 반환
    if table_name in _created_models:
        return _created_models[table_name]

    if isinstance(table_name, int):
        table_name = str(table_name)

    class_name = "Write" + table_name.capitalize()
    db_connect = DBConnect()
    DynamicModel = type(
        class_name,
        (WriteBaseModel,),
        {
            "__tablename__": db_connect.table_prefix + 'write_' + table_name,
            "__table_args__": (
                Index(f'idx_wr_num_reply_{table_name}', 'wr_num', 'wr_reply'),
                Index(f'idex_wr_is_comment_{table_name}', 'wr_is_comment'),
                {
                    "extend_existing": True,
                    **MySQLCharsetMixin().__table_args__
                },
            ),
        }
    )
    # 게시판 추가시 한번만 테이블 생성
    if create_table:
        DynamicModel.__table__.create(bind=db_connect.engine, checkfirst=True)
    # 생성된 모델 캐싱
    _created_models[table_name] = DynamicModel
    return DynamicModel

 

      - type은 두가지 용도로 사용 가능하다.

         - 객체의 타입 확인

         - 동적 클래스 생성

      

      - 이를 이용해 동적으로 게시판 테이블을 정의하고 생성할 수 있다.

 

      - __table__.create()의 매개변수

         - bind 매개변수:
             - bind를 통해 어느 데이터베이스에 테이블을 생성할 것인지를 지정한다.
         - checkfirst 옵션:

             - checkfirst=True를 사용하면, 테이블이 이미 존재하는지 확인한 후 존재하지 않을 때만 테이블을 생성한다.

 

DynamicModel = type(
        class_name,
        (WriteBaseModel,),
        {
            "__tablename__": db_connect.table_prefix + 'write_' + table_name,
            "__table_args__": (
                Index(f'idx_wr_num_reply_{table_name}', 'wr_num', 'wr_reply'),
                Index(f'idex_wr_is_comment_{table_name}', 'wr_is_comment'),
                {
                    "extend_existing": True,
                    **MySQLCharsetMixin().__table_args__
                },
            ),
        }
    )
    # 게시판 추가시 한번만 테이블 생성
    if create_table:
        DynamicModel.__table__.create(bind=db_connect.engine, checkfirst=True)
    # 생성된 모델 캐싱
    _created_models[table_name] = DynamicModel

 

      - EventSourceResponse는 주로 서버-발송 이벤트(Server-Sent Events, SSE)를 구현하기 위해 사용되는 응답 객체이다. 주로 Starlette나 FastAPI와 같은 비동기 웹 프레임워크에서 사용되며, 서버에서 클라이언트로 지속적으로 데이터를 스트리밍하는 방식으로 실시간 업데이트를 제공할 수 있게 해준다.

 

      - 아래 코드를 통해 비동기 제너레이터(또는 이터레이터)를 받아 이벤트를 순차적으로 클라이언트에 전송한다.

         - yield를 통해 이벤트를 전송한다.

 # 설치 진행 이벤트 스트림 실행
    return EventSourceResponse(install_event())

 

- reference :

https://rumbarum.oopy.io/post/how-fastapi-depends-work

 

FastAPI Depends 톺아보기

How 'Depends()' works in Fastapi Web Frameworks

rumbarum.oopy.io

https://velog.io/@chappi/FastAPI%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-11%EC%9D%BC%EC%B0%A8-Dependency-Injection

 

FastAPI를 배워보자 11일차 - Dependency Injection

https://fastapi.tiangolo.com/tutorial/dependencies/Dependency Injection은 두 가지로 분해해서 의미를 해석할 수 있는데 Dependency는 개발자의 code를 위해 사용하거나, 동작하기를 원하는 것을 선

velog.io

 

댓글