Search

[파이썬 아키텍처 패턴] 2장

저장소 패턴 개요

저장소 패턴은 데이터 저장소를 더 간단히 추상화한 것으로 이 패턴을 사용하면 모델 계층과 데이터 계층을 분리할 수 있다.
애자일 방식으로 작업할 때는 최소 기능 제품(MVP:Minimum Viable Product) 을 만드는 것이 우선이다. 1장에서 만든 도메인 모델을 API 로 만든다고 하면, API 가 MVP 가 된다.
장고의 MVC(모델-뷰-컨트롤러) 구조처럼 표현 계층 -> 비즈니스 로직 -> 데이터베이스 계층으로 구성된 계층 아키텍처가 존재한다. 각 계층이 자신의 바로 아래 계층에만 의존하게 만드는 것이 이 계층 아키텍처의 목표다.
하지만 도메인 모델에는 그 어떤 의존성도 없어야 한다. 즉, 하부 구조와 관련된 문제가 도메인 모델에 지속적으로 영향을 끼치면 안된다. 대신 모델을 마치 계층 내부에 있는 것으로 간주하고, 의존성이 내부로 들어오게 만들어야 한다.
표현계층 -> 비즈니스 로직 <- 데이터베이스 계층
이런 방식을 양파 아키텍처(onion architecture) 라고 부른다.
앞서 1장에서 봤던 이 모델을 데이터베이스로 만들어야 한다.
일반적으로 ORM(object-relational-mapping) 을 사용한다. ORM 이 제공하는 가장 중요한 기능은 도메인 모델이 데이터를 어떻게 적재하는지에 대해 알 필요가 없다는 것이다. 즉, ORM 을 통해서 특정 데이터베이스 기술에 도메인이 직접 의존하지 않도록 할 수 있다.
그러나 일반적인 코드를 보면 아래와 같이 이미 각 모델의 속성이 데이터베이스의 컬럼과 직접적인 연관이 있다.
from sqlalchemy import Table, Column, Integer, String, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class Order(Base): id = Column(Integer, primary_key=True) class OrderLine(Base): id = Column(Integer, primary_key=True) sku = Column(String(250)) qty = Column(String(250)) order_id = Column(Integer, ForeignKey('order.id')) order = relationship(Order)
Python
복사
이는 모델이 전적으로 ORM에 의존하고 있다. 반대로 ORM이 모델에 의존하게 해야 한다.
스키마를 별도로 정의하고, 스키마와 도메인 모델을 상호 변환하는 명시적인 mapper를 정의하는 것이다.
from sqlalchemy import Table, MetaData, Column, Integer, String, Date, ForeignKey from sqlalchemy.orm import mapper, relationship import model metadata = MetaData() order_lines = Table( "order_lines", metadata, Column("id", Integer, primary_key=True, autoincrement=True), Column("sku", String(255)), Column("qty", Integer, nullable=False), Column("orderid", String(255)), ) ... def start_mappers(): lines_mapper = mapper(model.OrderLine, order_lines)
Python
복사
ORM이 도메인 모델을 임포트한다. 반면, 도메인 모델은 ORM을 임포트하지 않는다. 또한 SQLAlchemy가 제공하는 추상화 객체를 사용해 테이블과 열을 정의한다. mapper 함수는 호출될 때, 사용자가 따로 정의한 도메인 클래스와 테이블을 연결한다.
결과적으로 start_mapper 함수를 호출하면 도메인 모델과 데이터베이스에 저장하거나 불러올 수 있다. 그러나 이를 호출하지 않으면 도메인 모델 클래스는 데이터베이스를 인식하지 못한다.
이러한 구조를 사용하면 ORM 의 이점을 취하는 동시에, 도메인 클래스를 사용해 질의를 쉽게 명확하게 할 수 있다.
도메인 모델에서 수행하는 작업에 따라 객체 지향 패러다임으로부터 멀어질수록 ORM이 원하는 대로 작동하게 만들기가 점점 더 어려워집니다. 이로 인해 도메인 모델을 직접 수정할 필요성이 생기게 됩니다.

저장소 패턴 소개

저장소 패턴은 영속성 저장소를 추상화 한 것이다. 저장소 패턴은 모든 데이터가 메모리상에 존재하는 것 처럼 가정하여 데이터 접근과 관련된 지루한 세부 사항을 감춘다.
모든 객체가 메모리 어딘가에 있더라도 아래 코드처럼 어딘가에서 가져옵니다.
가장 간단한 저장소에는 메서드가 두 가지 밖에 없다. add()는 새 원소를 저장소에 추가하고, get()은 이전에 추가한 원소를 저장소에서 가져온다.
from collections import abc import model class AbstractRepository(abc.ABC): @abc.abstractmethod def add(self, batch: model.Batch): raise NotImplementedError @abc.abstractmethod def get(self, reference) -> model.Batch: raise NotImplementedError class SqlAlchemyRepository(AbstractRepository): def __init__(self, session): self.session = session def add(self, batch): self.session.add(batch) def get(self, reference): return self.session.query(model.Batch).filter_by(reference=reference).one() def list(self): return self.session.query(model.Batch).all()
Python
복사
플라스크에서는 위와 같이 사용이 될 것이다.
도메인 서비스 계층에서 데이터에 접근할 때는 엄격하게 이 두가지 메서드만 사용할 수 있다. 이렇게 단순성을 강제로 유지하면 도메인 모델과 데이터베이스 사이의 결합을 끊을 수 있다.
따라서 이런 형태의 코드가 나올 것이다. 스프링에서 JPA를 사용할 때 repository 코드와 매우 닮아 있다.
참고
사실, 프로덕션 코드에서 ABC를 제거하는 경우가 많습니다. 파이썬에서 ABC를 무시하기가 너무 쉬워, 때때로 관리가 어렵거나, ABC 때문에 잘못된 코드를 작성할 수 있습니다. 실무에서는 파이썬의 덕 타이핑에 의존하여 추상화하는 경우가 많습니다. 파이썬 개발자에게는 add(thing)과 get(id) 메서드를 제공하는 모든 객체가 저장소가 될 수 있습니다. ABC나 덕 타이핑 외에도 PEP 544 프로토콜(https://oreil.ly/q9EPC)을 살펴볼 만합니다. 프로토콜은 상속 없이 타입을 지정할 수 있습니다. 그래서 '상속보다는 구성을 사용하라'는 원칙을 선호하는 경우에는 프로토콜을 더 선호할 수 있습니다. 이 부분은 fluent python에 더 자세히 설명이 되어 있습니다.

트레이드오프

DDD와 의존성 역전이라는 경로를 택한 이상 저장소 패턴은 이 책에서 나열한 패턴 중에서도 가장 채택하기 쉬운 패턴이다.
이 그림에서 도메인 모델 객체가 분리됨에 따라, ORM을 쉽게 교체할 수 있게 됩니다.
코드만 고려한다면 저장소 패턴은 단지 SQLAlchemy 추상화 (session.query(Batch))를 우리가 직접 설계한 다른 추상화 (batches_repo.get)로 바꿔치기 한 것 밖에 되지 않는다.

테스트에 사용하는 가짜 저장소를 쉽게 만드는 방법

class FakeRepository(AbstractRepository): def __init__(self, batches): self._batches = set(batches) def add(self, batch): self._batches.add(batch) def get(self, reference): return next(b for b in self._batches if b.reference == reference) def list(self): return list(self._batches) fake_repo = FakeRepository([batch1, batch2, batch3])
Ruby
복사
이 클래스가 set()을 감싸는 간단한 래퍼이므로 모든 메서드는 한줄로 끝난다.
fake_repo = FakeRepository([batch1, batch2, batch3])
추상화를 대신하는 가짜 객체를 만드는 것은 설계에 대한 피드백을 얻는 아주 좋은 방법이다.
가짜 객체를 만들기 어렵다면 추상화를 너무 복잡하게 설계했기 때문일 것이다.
포트(port)는 애플리케이션과 통신하려는 대상(그 대상이 무엇이든) 사이의 인터페이스(interface)이며, 어댑터(adapter)는 이 인터페이스나 추상화 뒤에 있는 구현(implementation)이라는 정의를 채택한다.
구체적으로 이번 장에서 Abstract Repository는 포트고, SQLAlchemy Repository와 Fake Repository는 어댑터입니다.

저장소 패턴 정리

ORM에 의존성 역전을 적용하자
도메인 모델은 인프라에 대해서 걱정할 필요가 없어야 한다. ORM은 모델을 임포트해야 하며 모델이 ORM을 임포트해서는 안된다.
저장소 패턴은 영속성 저장소에 대한 단순한 추상화다. 저장소는 컬렉션이 메모리 상에 있는 개체라는 환상을 제공한다. 저장소를 사용하면 핵심 애플리케이션에는 영향을 미치지 않으면서 인프라를 이루는 세부 구조를 변경하거나 Mock 저장소를 쉽게 작성할 수 있다.