애플리케이션 팩토리로 테스트 가능하고 구성 가능한 웹 애플리케이션 구축
Grace Collins
Solutions Engineer · Leapcell

소개
강력하고 유지보수 가능한 웹 애플리케이션을 개발하는 것은 종종 기능과 유연성 사이의 균형을 맞추는 것을 포함합니다. 애플리케이션이 복잡해짐에 따라 테스트, 구성 가능한 환경 및 모듈식 설계를 용이하게 하는 구조화된 접근 방식에 대한 필요성도 커집니다. Flask 및 FastAPI와 같은 많은 Python 웹 프레임워크는 강력한 도구를 제공하지만, 프로젝트를 구성하는 방식은 장기적인 생존 가능성에 큰 영향을 미칠 수 있습니다. 개발자가 직면하는 일반적인 과제 중 하나는 개발, 테스트 및 프로덕션 환경에 대한 다양한 구성을 관리하고 부작용 없이 코드를 쉽게 테스트할 수 있도록 보장하는 것입니다. 명확한 분리 및 동적 설정에 대한 이러한 필요성은 "애플리케이션 팩토리" 패턴을 탐구하게 합니다. 이 패턴은 Flask 또는 FastAPI 애플리케이션 인스턴스를 주문형으로 생성하는 매우 효과적인 메커니즘을 제공하여 프로젝트를 본질적으로 더 테스트 가능하고 구성 가능하게 만듭니다.
핵심 개념 설명
구현 세부 사항에 들어가기 전에 애플리케이션 팩토리 패턴과 그 이점을 뒷받침하는 몇 가지 핵심 개념을 명확히 하겠습니다.
애플리케이션 인스턴스: Flask 및 FastAPI와 같은 웹 프레임워크에서 "애플리케이션 인스턴스"는 웹 서비스의 모든 설정, 라우트 및 확장을 캡슐화하는 중앙 객체입니다. 그것은 당신의 애플리케이션의 핵심입니다. Flask의 경우 일반적으로 Flask()
의 인스턴스입니다. FastAPI의 경우 FastAPI()
의 인스턴스입니다.
구성: 구성은 애플리케이션의 작동 방식을 결정하는 설정 및 매개변수를 참조합니다. 여기에는 데이터베이스 연결 문자열, API 키, 디버그 모드, 로깅 수준 등이 포함됩니다. 효과적인 구성 관리는 이러한 설정이 핵심 애플리케이션 코드를 수정하지 않고 쉽게 변경될 수 있음을 의미하며, 다양한 환경(개발, 테스트, 프로덕션)에 맞춰 조정됩니다.
테스트 용이성: 테스트 용이성은 소프트웨어 구성 요소 또는 시스템을 얼마나 쉽고 효과적으로 테스트할 수 있는지를 나타내는 척도입니다. 매우 테스트 가능한 애플리케이션은 개별 부분을 격리하여 테스트할 수 있도록 하고, 예측 가능한 결과를 제공하며, 테스트를 방해할 수 있는 숨겨진 종속성 또는 전역 상태가 없습니다.
의존성 주입: 애플리케이션 팩토리 패턴의 엄격한 부분은 아니지만, 밀접하게 관련된 개념이며 종종 이를 보완합니다. 의존성 주입은 종속성을 해결하기 위해 제어 역전을 구현하는 소프트웨어 디자인 패턴입니다. 이를 통해 클래스 외부에서 종속 개체를 생성한 다음 해당 개체를 클래스에 주입할 수 있습니다. 이렇게 하면 구성 요소가 더 모듈화되고 테스트하기 쉬워집니다.
애플리케이션 팩토리 패턴은 호출될 때마다 새 애플리케이션 인스턴스를 생성하고 반환하는 함수를 제공하여 구성 및 테스트 용이성 문제를 해결합니다. 이 새롭고 독립적인 인스턴스는 테스트를 격리하고 특정 구성을 적용하는 데 중요합니다.
애플리케이션 팩토리 패턴
애플리케이션 팩토리 패턴은 웹 애플리케이션 인스턴스를 생성하고 구성하는 전용 함수가 책임을 지는 아키텍처 방식입니다. 전역적으로 정의된 단일 app
객체 대신, 실행될 때 새 애플리케이션을 구성하는 함수가 있습니다.
애플리케이션 팩토리가 필요한 이유
- 테스트 용이성: 각 테스트는 팩토리를 호출하여 신선하고 깨끗한 애플리케이션 인스턴스를 얻을 수 있습니다. 이렇게 하면 이전 테스트에서 상태가 유출되는 것을 방지하고 안정적이며 재현 가능한 테스트 결과를 보장하기 위해 테스트가 서로 격리됩니다. 테스트별 구성을 팩토리에 쉽게 전달할 수 있습니다.
- 구성 용이성: 팩토리 함수는 구성 객체 또는 환경 이름과 같은 매개변수를 허용하여 특정 설정을 사용하여 애플리케이션을 초기화할 수 있습니다. 이를 통해 여러 환경(개발, 테스트, 프로덕션)에서 코드 변경 없이 고유한 구성을 사용할 수 있습니다.
- 모듈식 디자인: 특히 블루프린트(Flask) 또는 APIRoute(FastAPI) 및 확장 기능을 처리할 때 더 모듈화된 구조를 장려합니다. 이러한 구성 요소는 팩토리 내에서 애플리케이션 인스턴스에 독립적으로 등록될 수 있습니다.
- 전역 상태 문제 방지: 함수 내에서 애플리케이션 인스턴스를 생성하면
app
객체로 전역 네임스페이스를 오염시키는 것을 방지할 수 있으며, 이는 순환 가져오기 문제와 테스트를 더 어렵게 만들 수 있습니다.
Flask 예제
Flask 애플리케이션으로 설명하겠습니다.
1. 프로젝트 구조:
my_flask_app/
├── config.py
├── my_flask_app/
│ ├── __init__.py
│ └── views.py
├── tests/
│ └── test_app.py
├── .env
├── requirements.txt
└── wsgi.py
2. config.py
(다양한 환경에 대한 구성):
import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'a_fallback_secret_key' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///dev.db' DEBUG = False TESTING = False class DevelopmentConfig(Config): DEBUG = True class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db' # 테스트용 별도 DB 사용 class ProductionConfig(Config): # 특정 프로덕션 설정 pass config_map = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'default': DevelopmentConfig } def get_config(environment): return config_map.get(environment, config_map['default'])
3. my_flask_app/__init__.py
(애플리케이션 팩토리):
from flask import Flask from .views import main_blueprint # 블루프린트가 있다고 가정 import os def create_app(config_class=None): app = Flask(__name__) if config_class is None: # 환경 변수를 기반으로 구성 로드 env = os.environ.get('FLASK_ENV', 'development') from config import get_config app.config.from_object(get_config(env)) else: # 제공된 클래스에서 직접 구성 로드(테스트에 유용) app.config.from_object(config_class) # 확장 초기화 (예: SQLAlchemy) # from flask_sqlalchemy import SQLAlchemy # db.init_app(app) # 블루프린트 등록 app.register_blueprint(main_blueprint) # 기타 설정 작업은 여기에 올 수 있습니다 # 예: 로깅, 오류 처리기 return app
4. my_flask_app/views.py
(간단한 블루프린트):
from flask import Blueprint, jsonify, current_app main_blueprint = Blueprint('main', __name__) @main_blueprint.route('/') def hello(): return jsonify(message=f"Hello from Flask! Debug mode: {current_app.config['DEBUG']}") @main_blueprint.route('/db_info') def db_info(): return jsonify(db_uri=current_app.config['SQLALCHEMY_DATABASE_URI'])
5. wsgi.py
(프로덕션 서버용 진입점):
from my_flask_app import create_app from config import ProductionConfig # 또는 FLASK_ENV에서 로드 app = create_app(ProductionConfig) if __name__ == '__main__': app.run()
6. tests/test_app.py
(테스트 용이성 설명):
import pytest from my_flask_app import create_app from config import TestingConfig @pytest.fixture def client(): # 팩토리를 사용하여 테스트 구성으로 앱 생성 app = create_app(TestingConfig) with app.test_client() as client: # 필요한 경우 테스트 데이터베이스 또는 기타 리소스 설정 # with app.app_context(): # db.create_all() yield client # 테스트 데이터베이스 또는 기타 리소스 정리 # with app.app_context(): # db.drop_all() def test_hello_endpoint(client): response = client.get('/') assert response.status_code == 200 assert "Hello from Flask!" in response.get_json()['message'] assert "Debug mode: False" in response.get_json()['message'] # TestingConfig는 DEBUG를 False로 설정 def test_db_info_endpoint(client): response = client.get('/db_info') assert response.status_code == 200 assert response.get_json()['db_uri'] == 'sqlite:///test.db'
FastAPI 예제
애플리케이션 팩토리 패턴은 FastAPI에도 똑같이 유익하지만, FastAPI는 데이터베이스에 대한 의존성 주입, 블루프린트 대신 라우터 개념 등 특정 측면을 약간 다르게 처리합니다.
1. 프로젝트 구조:
my_fastapi_app/
├── config.py
├── my_fastapi_app/
│ ├── __init__.py
│ ├── main.py
│ └── routers/
│ └── items.py
├── tests/
│ └── test_app.py
├── .env
├── requirements.txt
└── main_local.py # 로컬 개발용
2. config.py
(다양한 환경에 대한 구성):
import os from pydantic_settings import BaseSettings, SettingsConfigDict # pydantic-settings 필요 class Settings(BaseSettings): APP_NAME: str = "My FastAPI App" DATABASE_URL: str = "sqlite:///./dev.db" DEBUG_MODE: bool = False SECRET_KEY: str = os.environ.get('SECRET_KEY', 'default_secret') model_config = SettingsConfigDict(env_file='.env', extra='ignore') # .env에서 로드 class DevelopmentSettings(Settings): DEBUG_MODE: bool = True DATABASE_URL: str = "sqlite:///./dev.db" class TestingSettings(Settings): DEBUG_MODE: bool = False DATABASE_URL: str = "sqlite:///./test.db" # 테스트용 별도 DB class ProductionSettings(Settings): # 프로덕션별 설정 pass def get_settings(env: str = os.environ.get('APP_ENV', 'development')): if env == 'development': return DevelopmentSettings() elif env == 'testing': return TestingSettings() elif env == 'production': return ProductionSettings() else: return DevelopmentSettings() # 기본값
(참고: pydantic-settings
는 FastAPI 애플리케이션 구성을 관리하는 훌륭한 방법으로, 검증 및 환경 변수 로딩 기능을 제공합니다.)
3. my_fastapi_app/__init__.py
(라우터 정의, 일반적으로 routers/
에 있음):
# my_fastapi_app/routers/items.py from fastapi import APIRouter items_router = APIRouter(prefix="/items", tags=["items"]) @items_router.get("/") async def read_items(): return {"message": "List of items"} @items_router.get("/{item_id}") async def read_item(item_id: int): return {"item_id": item_id, "message": "Specific item"}
4. my_fastapi_app/main.py
(애플리케이션 팩토리):
from fastapi import FastAPI from my_fastapi_app.routers.items import items_router from config import get_settings, Settings import logging def create_app(settings: Settings = None) -> FastAPI: if settings is None: settings = get_settings() app = FastAPI( title=settings.APP_NAME, debug=settings.DEBUG_MODE, version="0.1.0", ) # 로깅 구성 if settings.DEBUG_MODE: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) # 라우터/APIRoute 등록 app.include_router(items_router) # 전역 의존성 재정의는 여기에 정의될 수 있으며, 종종 테스트용입니다. @app.on_event("startup") async def startup_event(): print(f"Starting up application: {app.title}, DB: {settings.DATABASE_URL}") # 데이터베이스 연결 등을 초기화합니다. @app.on_event("shutdown") async def shutdown_event(): print(f"Shutting down application: {app.title}") # 데이터베이스 연결 등을 닫습니다. return app
5. main_local.py
(로컬 개발용 진입점):
import uvicorn from my_fastapi_app.main import create_app from config import DevelopmentSettings # 개발 설정으로 앱 생성 app = create_app(DevelopmentSettings()) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)
6. tests/test_app.py
(테스트 용이성 설명):
import pytest from httpx import AsyncClient from my_fastapi_app.main import create_app from config import TestingSettings @pytest.fixture(scope="module") async def test_app(): # 팩토리를 사용하여 테스트 구성으로 앱 생성 app = create_app(TestingSettings()) async with AsyncClient(app=app, base_url="http://test") as client: yield client # 이 클라이언트는 테스트 앱에 요청을 보내는 데 사용할 수 있습니다. @pytest.mark.asyncio async def test_read_items(test_app: AsyncClient): response = await test_app.get("/items/") assert response.status_code == 200 assert response.json() == {"message": "List of items"} @pytest.mark.asyncio async def test_read_item(test_app: AsyncClient): response = await test_app.get("/items/1") assert response.status_code == 200 assert response.json() == {"item_id": 1, "message": "Specific item"} @pytest.mark.asyncio async def test_app_debug_mode_in_testing(test_app: AsyncClient): # 클라이언트를 통해 앱 인스턴스에 접근하여 설정 확인 app_instance = test_app._transport.app assert not app_instance.debug # TestingSettings는 debug를 False로 설정
애플리케이션 시나리오
- 다중 환경 배포: 동일한 코드베이스를 개발, 스테이징, 프로덕션 환경에 쉽게 배포하며, 각 환경은 고유한 구성(데이터베이스, API 키, 로깅)을 가집니다.
- 자동화된 테스트: 단위 및 통합 테스트에 필수적이며, 섹션의 예제와 같이 제공됩니다. 각 테스트 사례 또는 스위트는 상태 오염을 방지하는 격리된 애플리케이션 인스턴스를 얻을 수 있습니다.
- CLI 도구: 애플리케이션에 명령줄 도구(예: 데이터베이스 마이그레이션)가 포함된 경우, 팩토리를 사용하여 이러한 스크립트에 대한 특정 구성을 로드할 수 있습니다.
- 모듈식 애플리케이션: 여러 Flask 블루프린트 또는 FastAPI 라우터로 더 큰 애플리케이션을 구축할 때, 팩토리는 이러한 모든 구성 요소를 등록하고 구성하는 중앙 집중식 장소를 제공합니다.
결론
애플리케이션 팩토리 패턴은 테스트 가능하고, 구성 가능하며, 유지보수 가능한 Flask 및 FastAPI 애플리케이션을 구축하는 데 초석이 됩니다. 애플리케이션 생성을 함수로 캡슐화함으로써 전역 상태와 관련된 일반적인 함정을 제거하고 구성 관리 및 테스트에 비교할 수 없는 유연성을 제공합니다. 이 패턴을 채택하는 것은 강력하고 확장 가능한 Python 웹 서비스를 개발하는 데 중요한 단계입니다. 그 우아함은 간단함에 있으며, 애플리케이션이 발전함에 따라 복잡성을 관리하는 강력한 방법을 제공합니다.