들어가며
UoW 패턴은 저장소 패턴과 더불어 어플리케이션의 DB 접근에 대한 효과적인 추상화 방안을 마련해줍니다. 이번 포스팅에서는 UoW 패턴의 개념과 적용 예시를 정리해보겠습니다.
다음과 같은 개념을 알고 있어야 포스팅을 좀 더 쉽게 이해할 수 있습니다.
- 계층형(Layered) 아키텍쳐
- 저장소(Repository) 패턴
- Python 컨택스트 매니저(Context manager)의 사용법
UoW(Unit of Work)?
UoW는 작업단위로 해석되는 소프트웨어 디자인 패턴입니다. 애플리케이션(ex: API 서버)의 특정 비지니스 로직에서 발생하는 여러번의 변경사항을 한번에 처리합니다. 이를 통해 DB 일관성과 DB 연산의 원자성을 유지할 수 있도록 합니다.
애플리케이션의 비지니스 로직은 복잡해지기 마련입니다. 로직이 복잡해지면 서비스 로직에서 연관된 데이터를 한번에 처리해야하는 경우가 생기게 됩니다. 기존의 서비스 레이어 <-> 저장소 레이어 간 로직 흐름에서는 데이터 처리를 위한 무결성과 일관성을 보장하기가 힘들어집니다. 아래의 그림을 살펴보면 서비스 레이어가 여러개의 저장소와 협력하는 과정에서 작업들이 하나라도 실패한 경우, 이전 작업을 모두 돌려놓는(rollback) 작업을 처리하기 힘들어집니다.
UoW는 서비스 레이어(비지니스 계층)에서 트랜잭션의 개념을 도입한 패턴입니다.
UoW 패턴 적용
코드로 UoW 패턴을 알아보겠습니다. 다음은 파이썬으로 알아보는 아키텍처 패턴에서 나오는 allocation 서비스 예시를 응용해서 만든 학생 allocation 서비스 코드입니다. 해당 코드는 학생(student)을 학급(class)에 배치하는 샘플 코드입니다.
def allocation_service(
student_id: str,
uow: SqlAlchemyUnitOfWork) -> str:
with uow: # 1
classes = uow.classes # 2
class_id = allocate(classes, student)
uow.commit() # 3
return class_id
- 컨텍스트 매니저로 UoW 세션을 시작한다
- UoW로 부터
class
에 접근할 수 있는 repository를 제공받는다. - 작업이 끝나면 commit을 호출해 영속화를 진행한다. with 블럭 내에서 에러가 발생할 경우 rollback 하여 작업의 일관성을 유지한다
SqlAlchemy를 사용해 UoW를 구현한 SqlAlchemyUnitOfWork는 다음과 같이 구현됩니다.
class SqlAlchemyUnitOfWork:
def __init__(self, session_factory):
self.session_factory = session_factory
def __enter__(self):
self.session = self.session_factory() # 1
self.classes = repository.ClassRepository(self.session) # 2
def __exit__(self, *args): # 3
self.session.close()
self.rollback()
def commit(self):
self.session.commit()
def rollback(self):
self.session.rollback()
- 컨택스트 진입 시 sqlalchemy의 sessionmaker를 통해 생성된 세션을 시작한다
- session으로 classes를 가져옵니다. 해당 classes 변수는 UoW 인스턴스를 사용하는 서비스에서 학급(class)에 대한 정보(DB)를 제어하기 위한 단일 진입점이 된다.
- 컨택스트 블록(with)에서 나올 때 실행되는
__exit__
블록이다. DB 세션을 닫고 commit 되지 않은 내용을 rollback 한다.
왜 UoW를 사용할까?
UoW는 서비스 레이어와 직접적으로 맞닿아 있던 저장소(Repository)를 대체해 DB의 원자적 연산을 대신 수행합니다. 컨택스트 매니저를 사용했기 때문에 with 블럭 내에서 자유롭게 객체를 사용할 수 있습니다. 로직 내에서 문제가 발생해도 with 블럭을 벗어나면서 rollback 처리가 되기 때문에 데이터의 일관성이 보존되기 때문입니다.
또한, 모든 변경 사항을 마지막에 한번에 commit 할 수 있습니다. 이는 트랜잭션을 명시적으로 제어할 수 있는 수단을 제공함과 동시에 부분 commit(partial commit)을 방지합니다. UoW 단위의 커밋은 원자적 데이터베이스 연산을 보장합니다.
Uow 패턴은 너무 유용하기 때문에 SQLAlchemy가 이미 컨택스트 매니저를 사용한 UoW 패턴을 Session에 적용해서 제공하고 있습니다
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# an Engine, which the Session will use for connection
# resources, typically in module scope
engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/")
# a sessionmaker(), also in the same scope as the engine
Session = sessionmaker(engine)
# we can now construct a Session() without needing to pass the
# engine each time
with Session() as session:
session.add(some_object)
session.add(some_other_object)
session.commit()
# closes the session
https://docs.sqlalchemy.org/en/20/orm/session_basics.html#using-a-sessionmaker
이미 UoW를 SQLAlchemy에서 제공하고 있다면, SQLAlchemy를 사용하고 있는 프로젝트에서 UoW 패턴을 사용할 필요가 있을까? 라는 의문이 들게 됩니다.
다음은 이러한 의문에 대한 나름대로의 해답입니다.
UoW를 Repository 객체에 접근하기 위한 진입 지점으로 사용하는 Repository Facade로 사용할 수 있습니다. 또한 서비스 계층이 얇은 추상화 계층(UoW)에 의존하게 만듭니다. 이런 형태는 서비스 계층이 DB작업의 세부사항과 멀어지는 의존성 역전 원칙(DIP)을 준수할 수 있도록 합니다.
마치며
현재 프로젝트에는 하나의 서비스 계층에서 여러개의 레포지토리를 참조하고 있기 때문에 DB 작업을 하나의 작업 단위로 처리하기 힘든 상황입니다. 따라서 해당 프로젝트에 UoW 패턴을 적용할 계획입니다.
우려되는 점은 새로운 패턴을 추가할 경우 프로젝트 구조가 복잡해질 수 있다는 것 입니다. 하지만 다음과 같은 이유로 UoW 패턴 도입을 결정하게 되었습니다.
- 지저분한 서비스 코드베이스를 간결하게 만들 수 있다.
- 서비스 레이어를 좀 더 얇은 추상화 계층에 의존하게 할 수 있다.
- 테스트 코드 작성이 편해진다.
- 손쉽게 DB 연산의 원자성을 유지할 수 있다.
참고
https://www.cosmicpython.com/book/chapter_06_uow.html
https://www.c-sharpcorner.com/UploadFile/b1df45/unit-of-work-in-repository-pattern/
https://medium.com/@edin.sahbaz/implementing-the-unit-of-work-pattern-in-clean-architecture-with-net-core-53efb7f9d4d
'Computer Science > Python' 카테고리의 다른 글
[Python] FastAPI와 Dependency Injector (3) | 2024.10.27 |
---|---|
[Python] 비동기 테스트를 하려면? (a.k.a pytest-asyncio) (3) | 2024.04.28 |
[Python] SqlAlchemy 1.4 -> 2.0 마이그레이션 단계별 가이드 (0) | 2024.03.31 |
[Python] Tox로 여러 환경에서 테스트하기 (0) | 2023.07.02 |
[Pythonic Code] 파이썬스러운 코드! (0) | 2022.12.27 |