FastAPI 구조 변경
SQLAlchemy session 사용방식 변경
기존 service 레이어를 service, repository레이어로 분리
기존 response schema 들을 response schema 와 dto로 분리
BaseRepository 사용방법
new_app 실행방법
기존에는 endpoint method parameter 에서 다음과 같이 사용했었다.
session: AsyncSession = Depends(get_session),
엔드포인트 단위로 세션을 묶어 사용하는 방식으로 request 단위 세션을 사용했다.
단점
Transaction 처리를 해야할 때 request 단위 세션으로 트랜잭션을 걸거나, 세션을 중첩하여 중첩된 세션에 트랜잭션 처리를 해야하는 등 추가적인 소요가 발생함
파라미터를 통해 주입받아 하위 레이어까지 넘겨야하는데 매번 중복되는 파라미터를 사용해야함
request 단위 세션과 repository method 단위 조회용 세션을 사용한다.
request session
sqlalchemy 미들웨어
uuid 로 세션 id 를 할당한다.
할당한 세션 id 를 context 에 저장한다 (context 는 전역적으로 저장됨)
request 가 끝날 때 context 에서 할당한 세션 id 를 삭제한다.
core/session.py
context의 세션 id 를 사용하여 session을 async_scoped_session으로 생성한다.
context 를 통해 세션 생명주기를 컨트롤하도록 설정하였으며, context 를 middleware 에서 설정하므로 request 단위 세션이 만들어진다.
repository method 단위 조회용 세션
core/session.py
session_factory 함수로 AsyncSession 제너레이터를 설정
async with 로 제너레이터를 사용하게 되면 with 문 내에서 세션을 할당하고 with 문 밖으로 나가면 알아서 세션이 닫힘
core/query.py
session_factory 함수를 매번 임포트하여 async with 하는 동작이 반복되므로, select 쿼리만 파라미터로 넘기면 내부에서 세션을 할당하고 쿼리를 수행한 뒤 세션을 닫고 쿼리 결과를 반환하는 execute_query 함수가 구현되어 있음
write session
request 단위 세션을 write session 으로 사용함
기본적으로 어디에서도 commit이 일어나지 않도록 세션을 설정해두었기 때문에 (SQLAlchemy 가 그렇게 하도록 만들어짐) update, insert 를 설정없이 그냥하면 rollback 되어 db에 반영되지 않음
db에 write 작업이 발생하는 서비스 메서드는 @Transactional() 데코레이터를 메서드에 붙여 사용해야 commit 이 일어남.
@Transactional 데코레이터는 core/db/transactional.py 에서 구현하고 있으며, 데코레이터가 붙은 메서드를 호출할 경우 해당 메서드가 불리기 전에 프록시 함수로 감싸져 서비스 메서드가 종료되면 알아서 커밋 또는 롤백을 한 번에 처리하도록 구현하였음. [트랜잭션 처리]
read session
repository 에서 select 쿼리를 날리는 경우 execute_session() 함수를 통해 조회 쿼리 단위 세션을 조회용 세션으로 사용함
조회 쿼리를 수행하기 전에 할당되어 수행하고 바로 닫아버리므로 생명주기가 매우 짧음
write session, read session 분리한 이유
트랜잭션을 사용하기 위해서 상위 단계에서 세션을 할당해야하는데(request 세션에 해당), 세션은 메모리에 엔티티의 상태를 캐싱해두고 사용하므로 select 쿼리를 실행할 때 현재 DB의 데이터를 가져오기 전에 메모리에 남아있는 데이터를 사용하여 의도하지 않은 결과를 가져올 수 있음
이후 데이터베이스를 write instance, read instance 를 별도로 분리하게 될 경우 read session 의 engine만 교체해주면 편하게 사용할 수 있음
참고.
async_scoped_session?
AsyncSession 과 유사하게 데이터베이스 작업을 위해 커넥션을 맺고 세션을 할당함
AsyncScopedSession 은 AsyncSession의 기능에 추가로 세션의 스코프를 정해두고 해당 스코프 내에서 알아서 생명주기를 관리함.
service 레이어
비즈니스 로직만 수행
트랜잭션 관리
repository 레이어 메서드를 사용함
dto 를 반환
repository 레이어
하나의 쿼리당 하나의 메서드
메서드가 하는 일이 명확해지고 상위 레이어에서 메서드 이름만 보고 사용하기 편함
domain 객체 또는 dto를 반환
dto?
Data Transfer Objcet - 데이터 전달만을 위한 객체
pydantic 설정을 통해 불변, 타입안정객체이며, 도메인 객체에서 바로 변환할 수 있음
엔드포인트, 서비스 레이어만 존재할 때는 레이어간 주고 받기 위한 dto 가 굳이 필요하지 않았으나, 레이어가 늘어남에 따라 도메인 객체를 그대로 엔트포인트까지 올리는 등 객체를 분리해서 사용하지 않으면 객체 변경 시 변경지점이 늘어나 유지보수성이 떨어짐.
response schema
엔드포인트에서 클라이언트에게 반환할 때만 사용하는 객체
클라이언트에게 필요한 설명들을 추가하고 example 를 정의해주어야함
dto
레이어간 데이터 전달에 사용하는 객체
model_dump, response.model_validate 등 pydnatic에서 지원하는 기능을 통해 스키마간 변환이 자유로움
내부적으로 사용하는 객체이므로 불필요한 설명을 달 필요가 없음
BaseRepository
기본적인 CRUD를 id(pk) 값을 통해 구현하지 않고 사용할 수 있도록 미리 구현해둠
get_one, get_one_or_none, get_many, create, update_by_id …
BaseRepository 사용방법
구현한 Repository 가 BaseRepository 를 상속받아야함.
이때, BaseRepository 는 제네릭으로 도메인 타입을 받으므로 상속 시 도메인 타입을 명시해주어야 타입 힌트를 통해 에디터가 잘 동작함
ex.
class PropertyAreaSummaryRepository(BaseRepository[PropertyAreaSummary]):
구현한 Repository 에 model_type 필드를 두고, 도메인 모델 타입을 값으로 할당해야함.
내부에서 BaseRepository 가 해당 타입을 가져와 실제 쿼리를 날릴 때 사용함
ex.
class PropertyAreaSummaryRepository(BaseRepository[PropertyAreaSummary]):
model_type = PropertyAreaSummary
참고.
model_type 을 필드에 선언했는데 어떻게 BaseRepository가 가져다 씀?
파이썬의 metaclass 기능
파이썬 객체는 init 을 통해 초기화하는데 이 메서드를 통해 객체 인스턴스가 생성되는 것은 아니고 이미 생성된 객체를 초기화만 해주는 메서드임
파이썬 객체는 new 메서드를 통해 인스턴스를 생성함
따라서, new 메서드에서 인스턴스를 생성하기 전에 클래스 정보를 조작하여 model_type 정보를 BaseRepository 인스턴스에 주입하고 생성하도록 할 수 있음
base/repositories/base_repository_meta.py에서 확인 가능
기존 코드는 그대로 놔두고 new_app 에서 리팩토링을 진행한 뒤에 한번에 옮길 필요가 있음
base_model (SQLAlchemyBase) 무효화하기
app/models/_base_model.py 에 선언되어 있는 SQLAlchemyBase 는 모든 모델들이 상속하여 DB 테이블과 매핑을 자동화해주는데, new_app 에도 모델을 중복으로 선언할 경우 테이블과 매핑에서 에러가 발생하여 애플리케이션이 실행되지 않음
app/models/_base_model.py 에 선언되어 있는 SQLAlchemyBase 는 DeclarativeBase 클래스를 상속함으로써 매핑 자동화 기능을 제공받는데, 이 상속을 없애고 일반 클래스로 바꿔주면 app/models 는 동작하지 않게 됨
from sqlalchemy.orm import DeclarativeBase
# class SQLAlchemyBase(DeclarativeBase):
class SQLAlchemyBase:
pass
make temp 로 실행하기
make run 은 기존 app 패키지의 main.py 을 실행시켜 기존 코드가 동작하게 됨
scripts/start_new_app.sh 를 새로 정의하여 make temp 명령어를 사용할 경우 new_app 패키지의 main.py를 실행시켜 리팩토링중인 코드가 동작됨
다시 app 을 띄우고 싶다면?
new_app/base/models/base_model.py 무효화하기
app/models/_base_model.py 다시 DeclarativeBase 상속시키기
make run