Fastapi 를 그냥 Spring처럼 사용하고 있다.
장점
서비스, 컨트롤러, 리포지토리, 파사드, 외부API, core 등 여러 레이어를 나누어 구조화를 잘 해두었다.
Spring에서 제공하는 IoC 컨테이너, DI 등 을 파이썬 라이브러리를 사용하여 직접 구현하고 있다.
대규모 프로젝트에서의 Spring의 유지보수에서의 장점을 가져오면서 Fastapi의 비동기 처리도 가능하므로 좋아보인다.
단점
설정할 것이 너무 많다 (SpringBoot 프레임워크면 자동으로 해주는 것들을 직접 다 설정함)
프로바이더를 사용할 때 문자열로 가져오는데 등록해놓고 코드로 가져오는 것이 더 좋을 것 같다.
SQLAlchemy 세션 관련
세션을 미들웨어, 컨테이너 설정, @Transactional 등 다양하게 설정해서 사용하고 있어 전체 코드가 없어서 파악하기 어려움
위 프로젝트와 마찬가지로 dependency_injector 라는 라이브러리를 사용하여 DI를 관리해주고 있다.
장점
트랜잭션 처리, 레디스 캐싱 등을 코드화하여 쉽게 가져다 사용할 수 있다.
장점이자 단점
위 구조에 인터페이스, DTO, VO, request, response 등 더 세분화하여 사용하고 있다.
SQLAlchemy 세션 관련
트랜잭션 처리를 위해 서비스레이어에서 @Transactional 데코레이터를 별도로 구현하여 사용하고 있다.
쿼리 실행마다 async with 를 사용해 매번 세션을 생성하여 쿼리를 실행시킨다.
request 단위로 async_scoped_session을 두어 미들웨어를 통해 생명주기를 관리하며 데이터변경이 발생하는 경우에 사용되는 것 같다.
fastapi 만든 사람이 추천하는 프로젝트 구조
기능단위로 패키지를 분리하고 이후 레이어별로 분리함
서비스와 엔드포인트 사이에 dependencies.py 를 별도로 두고, db 데이터를 통한 validation, jwt 토큰 파싱 등 다양한 함수들을 구성하여 엔드포인트에서 Depends 를 통해 가져와 사용함 (사실상 서비스 레이어)
위 프로젝트들과는 달리 별도의 설정없이 Depends 체인으로 서비스 레이어를 구현할 수 있음 (현재 프로젝트에서 쉽게 중간 레이어를 끼울 수 있다.)
Depends 체인으로 서비스를 처리하는 방식이 조금 낯설고 서비스 메서드가 길어질 경우 현재 구조와 큰 차이가 없을 것 같음
dependencies.py 위 best-practcies 에서와 달리 DI 를 위한 파일로 사용하는 방법
엔드포인트, 서비스, 리포지토리 레이어로 구성함
dependencies.py에서 해당 도메인의 Dependency 들을 모두 관리함
현재 프로젝트 구조에서 쉽게 서비스 레이어를 추가할 수 있는 방법
Depends 를 사용한 의존성 주입은 테스트 시 dependency_overrides 를 사용하여 쉽게 mocking 할 수 있음
yuppie-backend
├── README.md
├── alembic.ini
├── app
│ ├── __init__.py
│ ├── assets
│ ├── common
│ ├── core
│ ├── middleware
│ ├── domains
│ │ ├── __init__.py
│ │ ├── property
│ │ │ ├── __init__.py
│ │ │ ├── endpoints.py
│ │ │ ├── service.py
│ │ │ ├── repository.py
│ │ │ ├── models
│ │ │ │ └── property.py
│ │ │ ├── schemas
│ │ │ │ ├── requests
│ │ │ │ ├── responses
│ │ │ │ └── dtos
│ │ │ └── dependencies.py
│ │ │
│ │ └── qualification
│ │ ├── __init__.py
│ │ ├── endpoint.py
│ │ ├── facade.py
│ │ ├── service.py
│ │ ├── repository.py
│ │ ...
│ │
│ ...
├── logs
├── scripts
├── config
├── poetry.lock
├── pyproject.toml
...
레이어별로 폴더를 구분하기보다는 도메인별로 폴더를 구분하면 좋을 것 같다.
현재 레이어별로 폴더를 구분하여 complex_service 를 찾고 싶을 때 services 패키지를 찾아도 complex_service를 찾는게 서비스가 많아져서 어렵다.
개발할 때 한 사람이 도메인 별로 기능을 개발하지 레이어별로 끊어서 개발하지 않으므로 도메인별로 패키지를 묶는 것이 더 편리할 것 같다.
스프링처럼 DI를 사용하기 위해 아예 IoC 컨테이너를 직접 구성하고 세션 관리를 별도로 해주는 것 보다는 fastapi에서 제공하는 Depends 를 사용하는 DI 방식이 더 좋을 것 같다.
스프링처럼 완전히 DI를 제어하는 방식을 사용하면 1. 세팅을 건드리지 않고는 구조가 유지되기 때문에 큰 규모에서 변경을 파악하고 유지보수하기 좋으며, 2. 생성자 주입 방식을 사용하므로 쉽게 mocking 하여 unit test 가 가능할 것 같다.
Depends 를 사용한 DI 방식은 1. 스프링에 비해 구조 변경은 한 눈에 파악하기 어려울 수 있다. 2. dependency_overrides 를 통해 쉽게 mocking 하여 unit test 할 수 있다. 3. 현재 프로젝트 구조에서 큰 설정 변경 없이 도입할 수 있다.
서비스 레이어는 분리하여 사용하는 것이 좋을 것 같다.
현재 서비스 레이어에서 쿼리 파트와 비즈니스 로직 파트를 모두 담당하여 서비스 클래스의 크기가 커지고, 각각의 메서드도 장황해졌으며, 메서드가 쿼리 파트를 담당하는 건지 비즈니스 로직을 담당하는 건지 알기 어렵다.
DTO, request, response 는 별도로 분리하면 좋을 것 같다.
현재는 request, response 만 분리하여 사용하고 있는데, 계산 과정에서 값을 전달하기만을 위한 데이터 클래스가 response 패키지쪽에 많이 있다. 분리해서 사용하면 찾기 더 편할 것 같다.
service 레이어에서 다른 service 메서드가 필요한 큰 메서드의 경우 (ex. qualification) 파사드 레이어를 하나 더 두는 것도 좋을 것 같다.
엔드포인트 → 파사드 → 서비스 → 리포지토리 순으로 분리하고, dependencies.py 에 모든 의존관계를 명시해놓는다.
인터페이스 도입은 고민을 해보는 것이 좋을 것 같다.
기능 추가 시 매번 추상 클래스를 구현하고, 구현 클래스를 별도로 구현해주어야하는데 기능 추가가 급한 경우 불필요하게 코드양이 많아질 수 있다.
레이어간 영향을 주는 큰 변경사항이 많을 경우 변경해주어야 하는 부분이 더 많이 늘어난다. 서비스가 어느 정도 안정화되고 최적화되고 나서 도입하는 것도 좋을 것 같다.
데이터베이스 세션은 서비스레이어? 리포지토리레이어? request단위?
서비스 레이어에서 세션을 공유할 경우
트랜잭션 처리하기에 좋다
세션을 공유하므로 커넥션 풀에서 많이 왔다갔다 할 필요가 없다.
하나의 서비스 메서드가 하나의 세션을 사용하므로 리포지토리 메서드를 병렬로 처리할 수 없다.
리포지토리 레이어에서 세션을 사용할 경우
서비스 레이어, request 단위 세션에 비해 커넥션 풀에서 세션을 꺼냈다 넣었다를 많이 한다.
트랜잭션 처리를 하기 위해 별도의 설정을 해주어야 한다.
순서가 상관없는 메서드들을 병렬로 처리하여 응답 시간을 단축할 수 있다.
request 단위로 세션을 사용할 경우
위 보일러플레이트 예제에 나온 경우로 request 단위 세션과 리포지토리 레이어 세션을 함께 사용한다.
request 단위 세션을 통해 트랜잭션 처리를 하고, 리포지토리 레이어 세션을 통해 별도 쿼리를 수행시킨다.
트랜잭션 처리와 병렬 처리가 모두 가능하다. 그러나 하나의 api 가 여러 커넥션을 사용하게 될 것 같다.
장점
다른 방법에 비해 변경사항이 많지 않다. 비즈니스 코드와 쿼리 코드 분리 및 파일 분리, dependencies 추가, 필요 시 facade 추가
unit test 가능, 레이어 분리 가능, 폴더 구조 가독성 향상
예상되는 단점
dependencies.py가 너무 장황해지지는 않을 지