개발 패턴에 대한 생각

정성연·2025년 11월 9일
post-thumbnail

개인 프로젝트부터 시작해서 회사 서비스까지 많은 웹서비스를 개발하면서 거의 대부분의 프로젝트에서 백엔드로 선택한 프레임워크가 바로 FastAPI였다.
별 거창한 이유는 아니고 그저 내 전공이 AI였기 때문에 파이썬이 주 언어라는 점과 Django에 비해 가벼워서 빠르게 서비스를 만들수 있다는 점이 주된 이유였다.
다만 특정한 개발패턴을 제시하는 Spring과 같은 프레임워크와는 달리 FastAPI에서는 딱히 그런 것들을 제시하진 않는다.
물론 Full Stack FastAPI Template이 공식적으로 존재하고 다른 커뮤니티들에서도 이런 저런 방식으로 개발을 하자는 논의가 있긴 하지만 그게 프레임워크가 직접 강제하는 방식은 아니라 실제 개발을 진행할 때는 고려할 사항이 한 두개가 아니다.

특히 협업에 있어서 아키텍처의 부재는 큰 문제로 다가왔는데 프로젝트에 참여하는 인원의 언어 및 프레임워크 이해도, 개발 스타일, 심지어 변수 명을 짓는 방식까지 달라서 프로젝트가 진행되면 될수록 불어나는 덩치에 비례해 점점 유지보수에 대한 부담이 올라가기 시작했다.
기본적으로 Full Stack FastAPI Template을 따라 개발을 진행하긴 했다만 템플릿이 제시하는 개발 방향과 실제 필요한 개발 방향은 다르기 때문에 잘 지켜지지 않은 부분도 있었다.
결론적으로 더 이상 공통된 아키텍처가 없이 개발을 지속하는 것은 문제가 많다는 것이 팀의 전체 의견이었다.


기본 아키텍처

고려사항

아키텍처 설계를 고민하기에 앞서 몇 가지 우선적으로 고려할 사항이 있었다.
1. 코드 구조만 보고 바로 개발에 들어갈 수 있을 것
2. 프레임워크에 종속적이지 않을 것
3. 안티패턴이 바로 눈에 띌 것

기본적으로는 MVC 패턴을 따라하는 것으로 결정했다.
대부분의 개발자에게 익숙하기 때문에 협업에 빠르게 도입할 수 있다는게 주된 이유이다.
단, 어디까지나 개발 패턴을 따라하는 것이기 때문에 FastAPI에 맞게 재구성할 필요는 있었다.
그 결과 아래와 같은 구조로 뼈대를 세웠다.

├── app/ 
│ ├── main.py # FastAPI 앱 인스턴스 및 진입점 
│ ├── api/ 
│ │ ├── v1/ 
│ │ │ ├── users.py # 사용자 API 
│ │ │ └── items.py # 아이템 API 
│ │ ├── deps.py # FastAPI 의존성 어댑터 (DI 경계) 
│ │ └── router.py # 라우터 일괄 등록 
│ ├── core/ 
│ │ └── user_service.py # 비즈니스 규칙(프레임워크 비종속) 
│ ├── crud/ 
│ │ ├── repositories/ # Repository 구현(ORM 의존) 
│ │ └── uow.py # UnitOfWork (요청 단위 트랜잭션) 
│ │ └── repo_registery.py # 레포지토리 레지스트리 (선택)
│ ├── database/ 
│ │ └── session.py # 세션/엔진 설정(ORM) 
│ ├── schemas/ 
│ │ ├── dto/ # 내부 계층 간 DTO 
│ │ ├── request/ # API 입력 스키마 
│ │ └── response/ # API 출력 스키마 
│ │ ├── base_schema.py # 기본 스키마 형태 (선택)
│ ├── infra/ 
│ │ ├── config/ # 설정 로딩(Env, 파일) 
│ │ ├── utils/ # 범용 유틸 
│ │ ├── middlewares/ # 미들 웨어 설정 
│ │ └── global_state.py # 전역 싱글톤(프레임워크 비종속) 
│ └── template/ 
│ └── template.html 
├── tests/ 
├── requirements.txt 
└── README.md

api - Controller 레이어

api 폴더는 Controller를 담당한다.
고려사항에서 프레임워크에 종속적이지 않는 것을 강조했지만 이 부분은 실제 유저의 요청과 응답 부분을 담당하므로 어쩔수 없이 종속적일수 밖에 없어진다.
대신, 모든 프레임워크 관련 의존성을 이 레이어 안에 한정시켰다.
deps.py는 이 구조의 핵심으로 FastAPI의 의존성 주입(Dependency Injection) 기능을 활용해, 라우터가 비즈니스 로직을 직접 알지 않고 필요한 객체를 주입받을 수 있게 한다.
즉, 프레임워크 경계에서 다른 계층으로 연결되는 단 하나의 진입점 역할을 수행한다.

core - Service 레이어

core 폴더는 Service를 담당한다.
앞서 말했든 해당 폴더는 오로지 비즈니스 로직에만 집중하고 전혀 FastAPI를 모른다.
모든 입출력은 DTO를 통해 이뤄지며 Repository를 통해 필요한 데이터를 전달받을 수 있도록 한다.

crud - Repository & UoW

crud 폴더는 Repository를 포함, ORM을 다루는 역할을 한다.
예전 Spring 개발을 할 때는 트랜잭션 경계와 관심사 분리 원칙 때문에 DTO 변환을 Service 레이어가 담당했지만 설계중인 아키텍처에서는 트랜잭션을 UoW(Unit of Works)가 담당한다.
또한 Lazy Loading을 통해 Service 레이어로 프록시 객체를 전달하는 Spring과는 다르게 SQLAlchemy에서는 ORM 객체를 직접 전달하기 때문에 혹시 모를 Service 단에서의 ORM 오염을 막기 위해 Repo에서 직접 DTO로 변환하는 방식을 택했다.

infra - 인프라 계층

나머지 모든 구성요소(설정, 미들웨어, 유틸 등)는 infra 폴더로 모았다.
이 계층은 프로젝트의 운영 환경을 담당하며, 비즈니스 로직과는 무관한 기술적 관심사를 분리하는 역할을 한다.

global_state.py는 전역 싱글턴 객체를 관리하는 모듈로,
Spring의 Bean과 유사한 개념으로 이해할 수 있다.
실제 동작 방식은 조금 다르지만 전역 의존성을 중앙에서 일괄 관리한다는 목적은 동일하다.
이 부분은 이후에 더 자세히 다룰 예정이다.

참조 트리

폴더 구성만 빡세게 해놓고 참조 규약을 안만들면 그것만큼 의미없는 일이 없다고 생각한다.
예를 들어 api폴더 내 라우터들에서 core를 안거치고 직접 crud에 접근한다거나 하는 그런 일들 말이다.
그래서 참조 규약을 만들었다.

의도한 참조 방향

main.py
↓
api/router
↓
api/routes
↓
api/deps
↓
infra/global_state
↓
infra/utils
↓
infra/config

---

api/deps
↓
infra/global_state
↓
database

---

api/deps
↓
uow
↓
crud

---

api/v1/routes
↓
core
↓
schemas

모든 참조는 위에서 아래로만 가능하며 참조의 맨위에는 항상 api가 존재한다.
FastAPI의 종속성은 main.pyapi 내부 경계 안쪽에만 존재해야 한다.

설계 철학

  • 프레임워크 종속은 api 경계 안에 가둔다.
    • FastAPI의 요청, 응답, DI는 오직 main.pyapi 안쪽에서만 다룬다.
  • 글로벌 싱글턴은 한곳에서만 관리한다.
    • 환경 설정, DB 세션, 외부 클라이언트(Qdrant, Redis 등) 등 모든 전역 객체는 이 파일을 통해 관리된다.
    • corecrud가 직접 전역 객체를 import 하는 것은 금지된다.
  • deps.py가 모든 계층 간 연결의 경계다.
    • deps.py는 FastAPI의 DI 레이어이자 경계 관리자 역할을 한다.
    • corecrud, global_state.py가 여기에 주입되며 라우터는 오직 deps.py를 통해 필요한 객체를 의존성으로 전달받는다.

결국 라우터는 비즈니스 로직이 어디에 있는지를 몰라도 되며,
FastAPI를 알고 있는 계층은 deps.py 이하로 한정된다.

금지 참조 예시

다음과 같은 참조는 모두 금지된다.

금지 예시이유
core → api비즈니스 로직이 프레임워크를 알아선 안 됨
crud → core 또는 api데이터 계층이 상위 레이어를 알면 순환 참조 발생
api/* → infra/config 직접 접근설정은 반드시 global_state를 통해 주입
core → FastAPI 객체(Request, Depends, Response) import프레임워크 비종속 원칙 위반

마무리

사실 그동안 프로젝트를 하면서 이렇게 본격적으로 직접 아키텍처를 설계한 경험은 처음이다.
때문에 그 밑바닥의 철학을 단단하게 구성하는게 무엇보다 중요하다는 생각을 했다.
FastAPI, 파이썬 코드 치고는 지켜야할게 많고 그게 오히려 파이썬스럽지 않은 코드가 될 수도 있겠다는 생각도 했지만 돌고 돌아 어떤 언어든 간에 제일 중요한 원칙은 유지보수성이라는 결론이다.

다음 글에서는 이번에 정의한 구조를 실제 코드로 구현하면서

  • deps.py를 통한 의존성 주입 흐름
  • UnitOfWork의 트랜잭션 경계 처리
  • Repository에서 DTO로의 변환 방식
  • global_state를 통한 전역 객체 관리

등을 구체적인 예시 코드와 함께 다뤄보려고 한다.
만관부 -

0개의 댓글