목차
개요
Repository Pattern은 데이터 저장소에 존재하는 데이터들을 마치 Application 메모리상에 존재하는 것처럼 가정하고 데이터 접근과 관련된 구현 사항을 추상화하는 패턴이다.
Spring Boot에는 JPARepository라는 것이 있는데 이 추상화된 Repository를 사용하면 특정 Entity에 대해 어떤 방식으로 데이터를 질의할지 이미 구현된 메서드가 제공된다는 것을 체감했을 것이다. 이미 구현되어 있기에 간단한 기능은 다시 구현할 필요가 없으며 더 복잡한 조건이 필요하다면 네이밍 룰에 따라 메서드를 정의해서 사용하면 된다는 것이 특징이다.
프로젝트를 경험할 때마다 Python에는 JPARepository와 같은 역할을 대체하는 라이브러리가 없었기에 Python에서 Repository Pattern을 사용할 때는 번거로움이 존재했다. 그러나 점점 강력해지는 Python의 Typing 모듈 덕분에 JPARepository의 완전 대체체까지는 아니더라도 General 한 데이터 질의 정도는 Python 코드를 통해 다룰 수 있게 되었다.
이 글은 이러한 내용에 대한 기록이다.
Python에서 Repository Pattern 구현 시 느낀 한계점
앞서 Repository Pattern은 데이터 접근과 관련된 구현 사항을 추상화하는 패턴이라고 언급했다. 그래서 나 스스로는 Repository Pattern 구현 시 2가지 정도의 특징을 담아내려고 하며 이는 나 개인의 코드 규칙이다.
- Repository의 Method는 Return 값이 Entity이거나 데이터를 표현하는 Object여야한다.
- Repository의 Method는 몇 가지 관례들이 있는데 find_by_xxx, get_by_xxx, delete_by_xxx로 시작하며 해당 Method Name을 통해 어떤 CRUD 연산이 수행되는지 드러나야 한다.
위와 같은 규칙을 Python Repository Pattern에 적용할 때 자연스럽게 고민이 되는 부분이 존재했다. 바로 “1번” 규칙이며 이 규칙을 생각하며 코드를 작성한다고 가정하면 다음과 같은 코드를 만들 수 있다.
class MemberRepository(IRepository[MemberEntity]): ...
“IRepository”라는 interface를 선언하고 특정 구현체에서 해당 Interface가 어떤 Type의 Repository인지를 명시하게끔 작성하는 것이다. “IRepository”는 다음과 같이 생긴 코드이다.
from typing import Generic, TypeVar
from abc import abstractmethod
T = TypeVar("T")
class IRepository(Generic[T]):
@abstractmethod
def find_by_id(self, pk:int) -> T: ...
위와 같이 Python에서 사용하는 타입 명시는 코드의 표현력을 증가시키며 코드를 더 명시적으로 드러낸다는 특징이 있다. 이러한 특징만 취하고자 한다면 고려해 볼 것이 있는데 바로 작성해야 한다는 코드가 더 많아진다는 점에 있다. 또한 Python과 같이 동적타입 언어에서 위와 같은 Interface 표현법은 “뭔가 과하다”라는 느낌마저 준다.
Python Repository Pattern의 한계점 개선해 보자.
필자가 집중해서 해결하고 싶은 한계점은 “작성해야 하는 코드 수의 증가”였다.
Spring Boot의 JPARepository는 이러한 부분에 있어서 미리 구현된 기능을 가져다 사용할 수 있지만 필자가 쓴 코드는 스스로 만들어낸 스니펫 정도이기 때문에 Python의 Generic을 활용해서 “일반적인 CRUD 기능을 IRepository에 구현할 수는 없을까”라는 고민에서 시작한 것이다.
이 고민에 대한 답을 찾기까지는 꽤나 오려 걸렸는데 해답은 PEP 560에서 찾을 수 있었다. PEP 560에서는 Generic에 대해 특정한 Magic Method를 언급하는데 바로 다음이다.
이를 간단히 정리해서 말하자면 Python의 Typing 모듈의 Generic의 인자로 사용하는 TypeVar의 “T”가 어떤 객체를 참조하는지 알아낼 수 있는 방법이다.
테스트해 보자.
Generic을 상속받은 클래스는 NameSpace에 “__orig_bases__” 가 들어있다고 한다. 테스트해 사용할 코드는 다음과 같다.
from typing import Generic, TypeVar
T = TypeVar("T")
class IRepository(Generic[T]): ...
class Duck:
name: str
class DuckRepository(IRepository[Duck]): ...
똑같이 Generic을 이용해 “IRepository”를 선언하고 “DuckRepository”에서는 이를 상속받는다. 그런데 이때 타입 변수에 “Duck” 클래스를 명시해 준다.
duck_repository = DuckRepository()
print(dir(duck_repository))
[
'__class__', '__class_getitem__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', **'__orig_bases__'**, '__parameters__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__',
'__subclasshook__', '__weakref__', '_is_protocol'
]
인스턴스화된 변수의 NameSpace를 열어보니 PEP 560에서 확인한 “orig_bases”가 들어있다. 다음과 같이 순서대로 접근하면 타입 변수로 지정한 객체가 무엇인지 알아낼 수 있게 된다.
print(duck_repository.__orig_bases__) # (__main__.IRepository[__main__.Duck],)
print(duck_repository.__orig_bases__[0]) # __main__.IRepository[__main__.Duck]
print(duck_repository.__orig_bases__[0].__args__[0]) # <class '__main__.Duck'>
SQLAlchemy와 함께 사용해 보자.
앞선 내용과 특징을 잘 활용해 SQLAlchemy와 함께 Repository Pattern을 구현할 때는 다음과 같은 코드가 사용 가능했다.
class ISqlalchemyRepository(Generic[T]):
session: Session = Provide[SqlAlchemyConatiner.session]
def __init__(self):
self.model = self.__get_inference_model()
def save(self, model):
self.session.add(model)
self.session.commit()
self.session.refresh(model)
self.session.commit()
return model
def delete_by_id(self, pk: int):
query = self.session.query(self.model).filter(self.model.id == pk).one_or_none()
if query:
return query.delete()
raise Exception("Not Exist Entity Error")
def find_by_id(self, pk: int):
query = self.session.query(self.model).filter(self.model.id == pk).one_or_none()
if query:
return query
raise Exception("Not Exist Entity Error")
def __get_inference_model(self):
return self.__orig_bases__[0].__args__[0]
해당 예시에는 Create, Read, Delete 정도에 대한 내용만 구현했다. 이와 같은 형태는 계속 시도하고 있는 내용이기 때문에 조금 더 발전시킬 여지가 충분할 듯싶다.
마치며
작성하고 싶었던 내용은 "Python에서 Generic을 이용한다면 Repository Pattern에 대해 이런 식으로 작동하는 코드를 만들 수 있다"라는 점이다. 비교적 최근에 알게 된 내용이라 이러한 코드가 가지는 장단점에 대해서는 조금 더 파고들 필요가 있어 보이지만 이 글로 하여금 Python에도 구조화된 BackEnd Architecture의 도입의 붐이 일어나길 바라본다.
'Language > Python' 카테고리의 다른 글
[SQLAlchemy] Pytest를 이용한 Imperative Mapping 테스트 코드 (0) | 2024.04.05 |
---|---|
[SQLAlchemy] Pessimistic/Optimistic Lock (2) | 2024.03.17 |
[SQLAlchemy] Imperative Mapping, One to Many Mapping (1) | 2024.03.15 |
[SQLAlchemy] Imperative Mapping, exclude properties (0) | 2024.03.07 |
[SQLAlchemy] Imperative Mapping, Column 합성 (1) | 2024.03.06 |