최근 적용한 FastAPI 아키텍처
목차
개요
프로젝트 경험동안 FastAPI 기반의 BackEnd API 서버를 개발해왔다. 처음엔 Django를 사용하면서 백엔드 개발을 시작했었는데 이제는 FastAPI가 더 익숙하다. 아무래도 Java 와 SpringBoot를 접하면서 새롭게 알게된 객체에 대한 개념들을 Python에도 적용하기 위한 최선의 방법인듯 하다.
적지않은 연습 끝에 FastAPI 기반의 프로젝트를 진행할 떄 스스로 코드 아키텍처를 구성해낼 수 있게 되었다. 하지만 부족하다 싶었던점은 FastAPI가 Python 기반이다보니 유용하다고 생각된 개념들을 어떻게 적용할 수 있을까에 대한 부분이었다.
최근 이러한 부분을 해소시킬만한 요소를 직접 적용하고 발전시킬 수 있었는데 이 글은 그 내용에 관한 부분들을 기록해놓은 것이다.
Abstract
: 최근 회사에 적용한 DIP 구조의 FastAPI Code Architecture
최근 회사에 적용한 FastAPI 기반의 코드 아키텍처는 Python의 “Abstract”를 활용한 DIP 적용에 의의를 두었다.
그간 회사나 개인 프로젝트에서 경험한 Python Project는 “Interface”를 중심으로 생각하는 “구현과 구현체 분리”를 적용한 모델의 사례가 없었다. 이유를 생각해 보건대 Python의 간결한 문법으로 인한 빠른 구현에 중점을 둔 것이 원인이라 생각된다.
간결한 문법으로 인해 산출물이 나오는 사이클이 단축되다 보니 시스템에 필요 요소인 “모듈”, “패키지”, “컴포넌트”의 개념을 적용하지 않게 되는 것이다. 이로 인해 “트랜잭션 스크립트 패턴”을 유발한다던가, “Fat Model”을 유지하게 되는 현상이 다반사다.
그러한 배경에서 최근 회사에 적용한 FastAPI 기반의 코드 아키텍처는 Python의 “Abstract”를 활용한 “구현”과 “구현체”를 분리시켜 요구사항을 개발함으로써 “서술적” 구조가 쌓인 코드 베이스를 만드는 것을 목표로 했다.
Background
: 최근 회사에 DIP 구조의 FastAPI Code Architecture를 적용할 수 있었던 배경
Architecture를 적용할 때는 협업하는 동료가 이에 대한 배경지식을 얼마나 가지고 있는지와 이 배경 지식이 양자 간 어느 정도 공유되는지가 중요하다 생각한다. 최근 회사의 코드 베이스를 통해 유추해 봤을 때 Architecture에 대한 배경지식을 공유하긴 어려운 상황이라는 판단을 내렸다.
최근 회사의 기존 관행대로 코드베이스를 유지하여 개발해 나가기엔 나 개인의 생산성과 추후 유지보수성이 떨어질 것을 고려해 “탄력성”, “생산성”, “유연성”의 3가지 요소를 기준으로 Architecture를 적용했다.
다음은 이 3가지 요소를 정의하고 이를 도입할 수 있을 성싶은 작은 생각과 결론을 정리한 표다.
탄력성 | 요구사항이 자주 변경되는 상황에서 얼마만큼의 일정한 워킹타임을 유지할 것인가? | 하나의 job이 sub-job으로 파생되는 형태인듯하다. 목표로한 일도 중간에 변경이 된다면 탄력성을 가져가기 힘들다 | 탄력성을 가져가는 구조는 힘들다. |
생산성 | 요구사항의 구현 복잡성에 있어 작업시간 대비 산출물을 뽑아내기 얼마나 쉬운 구조로 운용되는가 ? | 최근 회사은 20년된 회사이며 Resource들을 묶어서 표현하기 적합한 구조이다. 이를 Model 중심으로 접근하는 구조로 쌓아올린다면 생산성을 차근차근 올리기에 적합한 상황일듯 싶다. | 처음 생산성을 다소 떨어지겠지만 차근차근 높일 수 있다. |
유연성 | 요구사항의 구현에 있어 코드에 수정을 가하더라도 변경 범위를 어느 정도까지 확산시키지 않을 것인가? | 기존 구조에서 무언가를 변경/적용한다는 것은 난해한일이지만 처음부터 코드베이시를 작업할 수 있기에 유연성을 챙길 수 있다. | 처음부터 코드베이스를 작업한다면 유연성을 신경쓰면서 작업할 수 있다. |
3가지 요소에 대한 “생각”과 “결론” 부분을 정리하자면 “처음부터 주도적인 개발이 가능한 상황이다” 정도로 정리할 수 있겠다.
Discussion
: 최근 회사의 기존 코드베이스에 대한 문제점
최근 회사에서 사용 중인 기존 코드베이스는 다음과 같은 Architecture를 가지고 있었다.
├── _app
│ ├── __init__.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── auth
│ │ ├── db
│ │ ├── error
│ │ ├── log
│ │ └── response
│ ├── engine
│ │ ├── __init__.py
│ │ ├── const
│ │ ├── evaluation
│ │ └── user
│ ├── main.py
│ ├── models
│ │ ├── Const_TM.py
│ │ ├── __init__.py
│ │ ├── request
│ │ └── response
│ └── utils
│ ├── __init__.py
│ ├── const.py
│ ├── const_err.py
│ └── util.py
위 구조에 대한 주요 패키지의 “core”, “engine”, “models”, “utils”에 대한 정의는 다음과 같다.
- core/: 핵심 기능을 담당하는 모듈들이 위치합니다.
- engine/: 비즈니스 로직을 처리하는 모듈들이 위치합니다.
- models/: 데이터 모델과 요청/응답 스키마를 정의합니다.
- utils/: 유틸리티 함수들이 위치합니다.
- main.py: 애플리케이션의 진입점입니다.
위 구조는 구글에 “FastAPI라는 키워드”로 검색하면 프로젝트 개발에 있어 사용되는 통용적인 구조들의 모음이다. 개발에 정답은 없으니 해당 구조가 잘못되었다는 관점보다 이러한 구조가 가지는 한계점이 명확하다.
- “engine” : 비즈니스 로직을 담아두기 위한 공간을 하나의 거대한 패키지로 만들면 “트랜잭션 스크립트”가 탄생하며. 이는 트랜잭션의 길이가 길어짐을 의미한다.
- “core” : “핵심 기능”은 추상적인 말이며 이에 대한 정의가 구체적이지 않다면 개인의 개발 성향에 따라 코드가 추가되어 유지보수가 힘들어진다.
- “models” : orm 모델을 직접적으로 가져다 사용하면 데이터 베이스 라이브러리에 의존하는 구조가 생기며 “개념”과 “기술”이 혼합되어 SRP를 충족하지 못하는 구조로 편향된다.
Enhancement
: 최근 회사의 기본 구조 개선을 위한 개념들
Interface & Implements 적용하기
앞서 언급했듯 구조 개선을 위해 도입한 개념은 “Python의 “Abstract”를 활용한 DIP”이다. DIP는 SOLID 원칙의 5번째 원칙에 해당하며 정의는 다음과 같다.
어떤 class를 참조해서 사용해야 한다면 직접 참조하기보다 추상화 요소(abstract, interface)를 참조하라
단적인 표현으로는 “저수준 모듈이 고수준 모듈에 의존하게 되는 것”을 뜻한다. 대략 다음과 같다.
classDiagram
CLASS_A --> SomeInterface
SomeInterface <|.. ImplA
SomeInterface <|.. ImplB
class SomeInterface{
<<Interface>>
}
class ImplA{ }
class ImplB{ }
그러나 파이썬 일각에서는 이와 같은 구조가 “굳이 필요한가?”라는 질문을 던진다. 이런 질문은 타당하다. 파이썬 자체는 공식적으로 “Interface”라는 개념을 지원하지 않는다. 또한 간결한 문법으로 빠른 개발을 지향하는 파이썬이 굳이 “Interface”를 선언하는 번거로운 절차로 개발할 이유가 없다.
이에 대한 내 생각은 “개발” 한다는 행위를 “파이썬”에 맞춘 결과라 생각한다. “개발”이라는 관점에서 구현과 구현체의 분리는 상위 요소와 그 하위 요소들을 식별하고 이 요소를 코드를 통해 전달해 협업이 가능한 구조로 이끄는 것에 있다고 분다.
지금 당장 돌아가는 코드보다 “내일”도 안전하게 돌아가는 코드가 더 가치가 높지 않을까라는 코멘트를 남긴다.
DI Container로 의존성 관리하기
“구현”과 “구현체”를 분리하면서 따라온 이슈다. 스프링에서는 IoC가 인스턴스를 생성해 넣어주지만 파이썬은 아직 그런 역할을 프레임워크 레벨에서 해주지 않는다. 따라서 어떤 “구현”을 쓰려면 “구현체”라는 객체의 인스턴스를 생성해 직접 넣어줘야 하는 일이 발생한 것이다.
이는 2편에 쓸 내용인 실 코드로 내용을 추가 설명할 예정이다.
Layerd Architecture로 분할하기
“엔터프라이즈 애플리케이션 아키텍처” 에서는 “트랜잭션 스크립트”, “갓 클래스”와 같은 개념들이 나온다. 각각의 개념들은 다음과 같이 정리할 수 있다.
- 트랜잭션 스크립트
- 비즈니스 레이어에 위치하는 서비스 컴포넌트에서 발생하는 안티패턴이다. 트랜잭션 스크립트는 서비스 컴포넌트의 구현이 사실상 어떤 “트랜잭션이 걸려있는 스크립트”를 실행하는 것처럼 보일 때를 말한다,
- 갓 클래스
- 하나의 클래스가 너무 많은 책임과 기능을 맡고 있는 패턴을 말한다. 이는 테스트, 유지보수를 어렵게 만들고 클래스를 재사용하기 어렵게 만든다.
“Discussion”에서 다뤘던 최근 회사의 코드 베이스에는 이러한 개념들이 스며들어있는데 “engine.py” 가 바로 그것이다.
│ ├── engine
│ │ ├── __init__.py
│ │ └── evaluation
│ │ ├── __init__.py
│ │ ├── engine.py
│ │ └── view.py
engine.py 에는 Business Logic을 처리하는 메서드가 작성되어 있고 이러한 단위로 트랜잭션이 걸려있다. 간략한 호흡으로 구현해 내는 기능이라면 이러한 처리도 상황에 따라서는 맞다고 생각하지만 이미 여러 Resource들이 생성되고 Resource 간 관계들이 맺어져 있기 때문에 “engine.py”에 모든 걸 구현하는 방식에서는 머지않아 한계점을 맞이할 것이다.
즉, Domain이나 Resource 단위로 분할해 기능을 책임, 기능, 역할을 나누지 않는 코드는 유지보수가 어렵기에 이를 다음과 같은 디렉터리 구조를 베이스로 계층을 나눴다.
└── src
├── __init__.py
├── application
│ ├── __init__.py
│ ├── domain
│ ├── infrastructure
│ └── interfaces
├── main.py
각 계층에서 할 일들을 다음과 같이 구분했다.
domain | 서비스에서 사용하는 여러 도메인을 구현하는 곳이다. 정확히 domain의 개념보다는 관계가 짙은 Resource를 패키지 단위로 관리하고 주요 로직을 작성하기 위한 영역이다. |
infrastructure | 데이터 영속성을 담당하는 계층이다. Repository가 정의되는 곳이며 domain에서 “데이터”를 어떻게 가져올지 정의한 interface의 구현체를 작업한다. |
interfaces | Endpoint를 노출할 영역이다. 어떤 API가 있는지를 작업하는 영역이며 쉽게 말해 FastAPI의 APIRouter가 있는 부분이다. |
Adapter 개념 추가하기
코드 베이스는 외부 서비스를 공통적으로 사용해야 할 일이 많다. 예를 들어 서로 다른 프로젝트가 카카오 인증이 필요할 때 같은 라이브러리를 가져다 사용하는 형국이다. 그러나 문제는 이러한 유형의 라이브러리를 직접 코딩해서 작업해야 하는 경우다.
모듈을 놓는 위치를 정하지 못하면 프로젝트마다 상이한 형태로 사용되기 때문에 src 외부에 “adpater”라는 패키지를 두고 공통적으로 사용해야 하는 것들을 위치시켰다
├── adapter
│ ├── __init__.py
│ ├── database
├── src
│ ├── __init__.py
│ ├── application
│ ├── main.py
│ ├── settings
│ └── test
이로 인해 주요 business는 src 안에서만 다루게 하고 외부 의존성은 adapter로 분리시키면서 새로운 프로젝트에서 공통 모듈이 필요할 때 adapter를 그대로 가져다 사용할 수 있을 만 효과를 기대해 봤다.
Transaction Decorator 추가하기
트랜잭션의 범위가 곧 주요 비즈니스 로직은 아니지만 대부분 비즈니스 로직은 트랜잭션을 갖는다. 개인적인 생각은 비즈니스 로직 단위로 Transaction을 걸어 해당 메서드가 주요 비즈니스 로직임을 명시하자라는 것이다. 단순 조회를 하는 코드로 예를 들면 다음과 같이 생긴 코드를 이용하는 것이다.
@Transaction
def get_profile(self, user_id: str) -> xxxInfo: ...
Decorator의 효과는 자명하다. 관점지향 프로그래밍이라고 불리는 그것과는 미묘하게 다를 테지만 Transaction이 걸리는 부분에서 어떤 요청이 들어왔는지 혹은 왜 실패했는지를 하나의 코드 안에서 다룰 수 있게 되니 응집력이 높아지는 효과를 누릴 수 있을 것이라 기대한다.
하지만 이 Transaction 데코레이터 사용에는 데이터베이스 연결코드가 싱글톤으로 만들어져 세션이 관리되어야 한다. 이는 2편에서 작성할 코드 예제를 통해 더 보강해야겠다.
Makefile을 통한 프로젝트 명령어 모아두기
Makfile을 사용함으로써 프로젝트 실행 간 또는 작업 간 유용한 명령어들을 모아둘 수 있도록 해놨다. 예를 들어 uvicorn으로 실행한다던지 gunicorn으로 실행한다던지 하는 옵션들이다. 프로젝트에 Makefile이 추가되면 협업자 간에 명령어를 공유할 수 있기 때문에 생산성이 더 높아진다.
필자가 겪은 어떤 프로젝트는 SSL 터널링을 통해 데이터베이스 서버에 접근한 뒤 FastAPI를 실행시키도록 되어있었다. 즉 Shell을 열어 SSL 터널링을 잡아두고 FastAPi는 따로 실행시키는 것이다. 이러한 상황에서 Makefile 파일을 이용해 프로젝트 실행을 좀 더 수월하게 가능하도록 개선한 적이 있었다.