티스토리 뷰

 

AI 솔루션 개발팀으로 이동하게 되면서 개발 환경에 큰 변화가 생겼다. 기존 클라우드 환경에서 온디바이스(On-Device) 타겟으로 작업 영역이 변경되었고, 이에 맞춰 언어는 Python 네이티브로, 백엔드 프레임워크는 FastAPI로 통일되었다.

 

이번 포스팅에서는 4~5년간 사용했던 Spring Boot 환경에서 넘어와 약 한 달간 FastAPI를 실무에서 사용해보며 체감했던 장단점을 정리해보려 한다. 가장 크게 다가왔던 차이점들을 키워드로 꼽자면 다음과 같다.

 

장점: 핫리로드(Hot Reload), Pydantic, 비동기 제어

단점: 의존성 주입(DI)과 객체 생명주기 관리의 어려움, 명시적 트랜잭션 관리, 싱글톤 정의의 모호함

 

이 글에서는 위 내용들을 하나씩 상세하게 비교해 보겠다.

장점 1. 핫리로드

가장 먼저 피부로 와닿은 장점은 개발 생산성과 직결되는 핫리로드다.

uvicorn main:app --reload --host 0.0.0.0 --port 8000

FastAPI는 서버 실행 시 --reload 명령어 한 줄만 추가하면 된다. 코드를 수정하고 저장하는 즉시 변경 사항이 서버에 반영되므로, 변경점을 테스트하고 피드백을 확인하는 속도가 매우 빠르다.

 

물론 Spring에도 DevTools 등을 활용한 비슷한 설정이 존재한다. 하지만 체감상 Spring의 재시작 속도는 FastAPI에 비해 현저히 느리다. 또한, (IntelliJ 설정 문제일 수도 있겠지만) Spring 환경에서는 빌드가 실패했을 때 스스로 재시작을 반복하며 계속 실패 로그를 띄우는 경우가 종종 있었다.

 

반면, FastAPI는 내가 '파일을 저장하는 시점'에만 깔끔하게 재시작된다는 점에서 개발 경험(DX)이 훨씬 쾌적했다.

 

장점 2. Pydantic

Spring을 사용할 때 우리는 클라이언트의 요청 데이터를 받기 위해 DTO(Data Transfer Object)를 만들고, Lombok(@Data or @Getter)을 붙이고, 유효성 검증을 위해 Hibernate Validator(@NotNull, @Size 등) 어노테이션을 덕지덕지 붙여야 했다. 그리고 컨트롤러에서는 @Valid 어노테이션을 잊지 말아야 했다.

 

FastAPI의 Pydantic은 이 모든 과정을 파이썬의 타입 힌트 문법 하나로 통합해서 해결해 준다.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = None

Pydantic의 장점을 정리하면 다음과 같다.

 

1. 별도의 검증 로직을 짤 필요가 없다. 위 코드처럼 타입을 명시해 두면, 요청 들어온 데이터의 타입이 맞지 않을 때(예: price에 문자열을 넣는 등) 알아서 422 Unprocessable Entity 에러를 뱉어준다. Spring에서 BindingResult를 핸들링하던 것에 비하면 엄청나게 직관적이다.

2. 보일러플레이트 객체를 생성할 필요가 없다. Java/Kotlin에서 DTO를 만들 때마다 작성해야 했던 Getter, Setter, 생성자, 그리고 JSON 파싱을 위한 Jackson 설정들이 Pydantic 모델 하나로 깔끔하게 정리된다. 직렬화/역직렬화(Serialization/Deserialization)가 물 흐르듯 자연스럽다.

3. Swagger(OpenAPI) 자동화 가장 감동적인 부분이다. Pydantic으로 모델을 정의하면, 이것이 곧바로 Swagger UI의 스키마 문서로 변환된다. API 문서를 위해 별도의 어노테이션을 달거나 테스트 코드를 짜지 않아도, 내가 작성한 코드가 곧 살아있는 문서가 된다. Spring에서 Swagger 를 만들려고 중복으로 작성해야했던 인터페이스, 어노테이션 지옥에서 빠져 나올 수 있다.

 

그리고 Spring에서는 Snake Case 전달된 JSON 필드를 Camel Case로 치환하려면 @JsonProperty나 NamingStrategy 설정을 해야 했는데, Pydantic은 alias 설정 등으로 더 유연하게 느껴졌다.

장점 3. 직관적인 비동기 제어

Spring에서도 @Async나 CompletableFuture를 사용하면 비동기 처리가 가능하다. 하지만 이를 제대로 쓰려면 별도의 스레드 설정을 신경 써야 하거나, 리턴값을 다루기 위해 .thenApply() 같은 메서드 체이닝을 사용해야 해서 코드가 조금 무거워지는 느낌이 든다. (마치 멀티스레드 프로그래밍을 직접 하는 기분이 든달까?)

 

FastAPI는 훨씬 가볍고 직관적이다.

@app.get("/")
async def read_results():
    results = await some_library()
    return results

비동기 로직임에도 불구하고, 코드가 위에서 아래로 순서대로 읽힌다. 복잡한 설정 없이 async와 await 키워드만 붙이면 끝이다. Spring이 "비동기 기능을 탑재해서 쓰는" 느낌이라면, FastAPI는 "언어 자체가 비동기를 품고 있는" 느낌이라 훨씬 가볍게 다가왔다.

 

단점 1. 의존성 주입(DI)과 객체 생명주기 관리의 어려움

Spring 개발자에게 IoC(제어의 역전) 컨테이너는 기본적으로 제공받는 기능이다. @Component나 @Service 어노테이션만 붙여두면, Spring이 알아서 객체를 생성하고, 싱글톤으로 관리하며, 필요한 곳에 @Autowired로 척척 주입해준다.

 

하지만 FastAPI로 넘어오면서 이 마법이 사라졌다.

 

'알아서' 해주지 않는다: FastAPI의 Depends는 강력하지만, Spring의 컴포넌트 스캔처럼 프로젝트 전체의 빈(Bean)을 자동으로 관리해주진 않는다. 개발자가 명시적으로 의존 관계를 정의해야 한다.

 

계층 간 주입의 번거로움: Controller(Router) 단에서는 Depends로 주입받기가 쉽지만, Service 계층이나 그 하위 계층으로 내려갈수록 의존성 전파가 번거로워진다. Spring에서는 어디서든 @RequiredArgsConstructor 하나면 해결되었던 일들이, FastAPI에서는 파라미터로 계속 넘겨주거나 별도의 DI 패턴(또는 라이브러리)을 고민해야 했다.

 

아래와 같이 복잡하게 구성되는 걸 어떻게 처리해야할지 아직도 고민이다.

# dependencies.py
# Spring의 IoC 컨테이너가 없어서 직접 하나하나 조립해야 하는 상황

from fastapi import Depends, Request

# 1. Global State나 Request에서 가져오는 기초 객체들
def get_external_api_client(request: Request) -> ExternalApiClient:
    return request.app.state.external_api_client

def get_redis_cache(request: Request) -> RedisCache:
    return request.app.state.redis

def get_db_session(request: Request) -> Session:
    return request.app.state.db_session

# 2. Repository 계층 (DB 세션 등을 주입)
def get_user_repository(session: Session = Depends(get_db_session)) -> UserRepository:
    return UserRepository(session)

def get_audit_log_repository(session: Session = Depends(get_db_session)) -> AuditLogRepository:
    return AuditLogRepository(session)

def get_config_repository() -> ConfigRepository:
    return ConfigRepository()

# 3. Service 계층 (Repository와 다른 Service들을 주입 - 여기가 가장 복잡해짐)
def get_notification_service(
    client: ExternalApiClient = Depends(get_external_api_client)
) -> NotificationService:
    return NotificationService(client)

def get_policy_validator(
    config_repo: ConfigRepository = Depends(get_config_repository)
) -> PolicyValidator:
    return PolicyValidator(config_repo=config_repo)

# 의존성이 꼬리에 꼬리를 무는 메인 비즈니스 로직
# Spring: 그냥 @RequiredArgsConstructor 끝
# FastAPI: 파라미터에 Depends를 줄줄이 명시해야 함
def get_complex_business_service(
    user_repo: UserRepository = Depends(get_user_repository),
    audit_repo: AuditLogRepository = Depends(get_audit_log_repository),
    noti_service: NotificationService = Depends(get_notification_service),
    validator: PolicyValidator = Depends(get_policy_validator),
    cache: RedisCache = Depends(get_redis_cache)
) -> ComplexBusinessService:
    return ComplexBusinessService(
        user_repo=user_repo,
        audit_repo=audit_repo,
        noti_service=noti_service,
        validator=validator,
        cache=cache
    )

단점 2. 명시적 트랜잭션 관리 (@Transactional의 부재)

Spring JPA를 쓸 때 가장 그리운 기능 1순위는 바로 @Transactional이다. 메서드 위에 이 어노테이션 하나만 붙이면 트랜잭션 시작, 커밋, 예외 발생 시 롤백까지 AOP가 알아서 처리해준다.

 

FastAPI(주로 SQLAlchemy 사용 시)에서는 이 모든 것이 명시적(Explicit)이어야 한다.

def create_item(db: Session, item: ItemCreate):
    try:
        db_item = models.Item(**item.dict())
        db.add(db_item)
        db.commit()  # 개발자가 직접 커밋
        db.refresh(db_item)
        return db_item
    except Exception:
        db.rollback() # 예외 처리도 직접 신경써야 함
        raise

물론 Dependency의 yield를 활용해 세션의 생명주기를 관리하긴 하지만, 비즈니스 로직 중간에 트랜잭션 범위를 설정하거나 전파 레벨(Propagation)을 조정하는 등의 고급 기능을 구현하려면 코드가 꽤나 지저분해진다. "혹시 내가 commit을 빼먹었나?" 하는 불안감은 덤이다.

단점 3. 싱글톤 정의의 모호함 (feat. Global Variable)

AI 모델과 같이 무거운 리소스는 애플리케이션 실행 시 딱 한 번만 로딩해서 메모리에 상주시켜야 한다.

 

Spring: @Bean으로 등록하면 기본이 싱글톤(Singleton)이다. @PostConstruct로 초기화 로직을 넣기도 아주 편하다. 체계적이고 안정적이다. @Configuration으로 용도에 맞게 정의도 가능하다.

 

FastAPI: Python 모듈 자체가 싱글톤처럼 동작하긴 하지만, 명시적인 아키텍처 가이드가 부족하다. 결국 global 변수를 선언하거나, app.state에 우겨넣거나, @lru_cache 같은 데코레이터를 활용하는 등 여러 방법이 혼재된다. 어떤 것을 써도 나중에 의존성 주입 과정이 불편해진다.

 

기능상으로는 문제가 없지만, Spring의 정형화된 빈 관리 방식에 익숙한 눈으로 보면 어딘가 모르게 구조가 헐거워 보이고 "이렇게 전역 변수처럼 써도 되나?" 하는 불안감이 든다. 

 

마치며

약 한 달간의 FastAPI를 사용한 경험을 요약하자면, Spring은 행동을 강제하고, FastAPI는 자유도를 준다이다.

 

대규모 엔터프라이즈 시스템이나 복잡한 비즈니스 로직, 그리고 견고한 트랜잭션 관리가 필수적인 도메인이라면 여전히 Spring Boot가 정답일 것이다. 프로젝트에 동원되는 팀원 수가 많고, 팀원의 변경이 잦아도 코드 베이스 유지를 위해서 Spring이 유리 할 것 같다.

 

하지만 현재 우리 팀처럼 AI 모델 서빙이 주 목적이고, 가벼운 리소스(온디바이스) 환경이 중요하기 때문에, FastAPI는 대체 불가능할 것 같다. 스프링의 무거움과 복잡한 설정을 걷어내고 개발 자체에 집중하게 해주는 그 '경쾌함'은 확실히 매력적이다.

 

프레임워크와 개발환경 선택에 있어서 중요한 건 현재 프로젝트의 목적에 무엇이 더 적합한가일 것이다. Spring 개발자로서 해볼 수 있는 걸 거의 다해본 시점에서, FastAPI로의 기술 전환은 꽤 즐거운 경험이었다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함