-
파이썬으로 살펴보는 아키텍처 패턴 총정리📚 개발 도서 2024. 1. 1. 22:24
1장. 도메인 모델링을 지원하는 아키텍처 구축
1장에서는 도메인 모델 패턴으로 비즈니스 계층을 만드는 방법을 보여준다.
대부분 개발자가 새로운 시스템을 설계하라는 요청을 받으면 즉시 데이터베이스 스키마를 그리기 시작하고 그다음 객체 모델을 생각한다. 여기서부터 모든 것이 잘못되기 시작한다. 대신 먼저 행동하고 저장에 대한 요구 사항은 행동에 맞춰 정해져야 한다.
나는 페이지의 새로운 혁신 내용을 재미있게 읽었다. 기존 시스템에서, 모든 배송과 도착 날짜를 추적하여 창고로 배송 중인 상품을 실제 재고로 간주에 창고에 존재하는 제품처럼 취급할 수 있다는 이야기. 이는 재고가 없다고 표시되는 상품이 감소하여 더 많은 상품을 팔 수 있게 된다. 즉, 이 비즈니스는 돈을 절약하게 된다.
이런 시스템을 구현하려면 단순한 모델링에 그치지 않고, 비즈니스를 잘 표현한 도메인 모델링이 필요하다.
2장. 저장소 패턴
저장소 패턴은 데이터 저장소를 더 간단히 추상화한 것으로 이 패턴을 사용하면 모델 계층과 데이터 계층을 분리할 수 있다. 이 추상화를 통해 데이터베이스의 복잡성을 갖춰 테스트하기 더 좋게 만든다.
여기서 의존성 역전의 중요성을 알려준다.
도메인 모델에는 그 어떤 의존성도 없기 바란다. 도메인 모델은 인프라에 대해 걱정할 필요가 없어야 한다. 하부 구조와 관련된 문제가 도메인 모델에 지속적으로 영향을 끼쳐서 단위 테스트를 느리게 하고 도메인 모델을 변경할 능력이 감소되는 것을 원하지 않는다.
3장. 막간: 결합과 추상화
3장에서는 진흙공 패턴의 문제를 언급한다. 애플리케이션이 커짐에 따라 서로 응집되지 않은 요소 사이의 결합을 막을 수 없어서 결합이 요소의 개수가 늘어나는 비용보다 훨씬 더 빨리 증가하는 바람에 시스템을 실질적으로 변경할 수 없게 된다.
4장. 첫번째 유스케이스: 플라스크 API와 서비스 계층
오케스트레이션: 저장소에서 여러가지를 가져오고, 데이터베이스 상태에 따라 입력을 검증하여 오류를 처리하고, 성공적인 경우 데이터를 데이터베이스에 커밋하는 작업을 포함한다.
오케스트레이션 계층이나 유스케이스 계층이라고 부르는 서비스 계층으로 분리하는 것이 타당한 경우가 종종 있다. 서비스 계층으로 분리를 한다면 플라스크 앱의 책임은 표준적인 웹 기능 뿐이다. (ex. 요청 전 상태를 관리하고, POST 파라마티로부터 정보를 파싱하며 상태 코드를 응답하고 JSON을 처리한다.)
이는 E2E 테스트를 무겁게 가져가지 않을 수 있고 단 두개의 테스트로 만들 수 있다. 하나는 정상 경로를 테스트하고, 다른 하나는 비정상 경로를 테스트한다.
애플리케이션 서비스와 도메인 서비스
- 애플리케이션 서비스 (서비스 계층): 외부 세계에서 오는 요청을 처리해 연산을 오케스트레이션한다.
- 데이터베이스에서 데이터를 얻는다.
- 도메인 모델을 업데이트한다.
- 변경된 내용을 영속화한다.
- 도메인 서비스: 도메인 모델에 속하지만, 근본적으로 상태가 있는 엔티티나 값객체에 속하지 않는 로직을 부르는 이름이다
ex) TaxCalculator 도메인의 성격을 잘 나타낼 수 있다.
5장. 높은 기어비와 낮은 기어비의 TDD
테스트에 넣는 코드는 한줄, 한줄이 마치 본드 방울같다. 이 본드 방울은 시스템을 특정 모양으로 만든다. 테스트가 더 저수준일수록 시스템 각부분을 변경하기가 더 어려워진다.
- API 테스트
이는 전체 애플리케이션을 다시 작성해도 URL이나 요청 방식을 바꾸지 않는 한, 앱은 HTTP 테스트를 계속 통과한다.
API 테스트는 훨씬 더 높은 수준의 추상화를 사용하므로 객체의 세부 설계에 대한 피드백을 제공하지 않는다. - 도메인 테스트
도메인 테스트를 하면 테스트가 도메인 언어로 작성되므로 이 테스트는 모델의 살아있는 문서 역할을 한다. 새로운 팀원은 이런 테스트를 읽고 시스템이 어떻게 동작하는지 빠르게 이해하고 핵심 개념이 서로 어떻게 연관됐는지 이해할 수 있다.
도메인 테스트가 있으면 필요한 객체에 대한 이해를 증진시킬 때 동무이 된다.
6장. 작업 단위 패턴
저장소와 서비스 계층 패턴을 하나로 묶어주는 마지막 퍼즐 조각을 작업 단위(UoW)라고 한다. 저장소 패턴이 영속적 저장소 개념에 대한 추상화라면, 작업 단위 패턴은 원자적 연산이라는 개념에 대한 추상화다. Uow 패턴을 사용하면 서비스 계층과 데이터 계층을 완전히 분리할 수 있다.
UoW는 영속적 저장소에 대한 단일 진입점으로 작용한다. UoW는 어떤 객체가 메모리에 적재됐고 어떤 객체가 최종 상태인지를 기억한다.
- 작업 단위 패턴은 데이터 무결성 중심 추상화다.
- 작업 단위 패턴은 저장소와 서비스 계층 패턴과 밀접하게 연관되어 작동한다.
- 작업 단위 패턴은 원자적 업데이트를 표현해 데이터 접근에 대한 추상화를 완성시켜준다 서비스 계층의 유스케이스들은 각각 블록단위로 성공하거나 실패하는 별도의 작업 단위로 실행된다.
- 콘텍스트 관리자를 사용하는 멋진 유스케이스다.
- 콘텍스트 관리자를 사용해 요청 처리가 커밋을 호출하지 않고 끝나면 자동으로 작업을 롤백할 수 있다.
7장. 애그리게이트와 일관성 경계
도메인 모델 객체가 개념적으로나 영속적 저장소 안에서나 내부적인 일관성을 유지하는 방법을 살펴본다.
애그리게이트는 다른 도메인 객체들을 포함하여 이 객체 컬렉션 전체를 한꺼번에 다룰 수 있게 해주는 도메인 객체이다. 애그리게이트에 있는 객체를 변경하는 유일한 방법은 애그리게이트와 그 안의 객체 전체를 불러와서 애그리게이트 자체에 대해 메서드를 호출하는 것이다.
애그리게이트는 자신만의 불변 조건을 유지할 책임을 담당하는 한 동시성 경계다.
애그리게이트는 데이터 변경이라는 목적을 위해 한 단위로 취급할 수 있는 연관된 객체의 묶음이다.
- 에릭 에번스, “도메인 주도 설계”에번스에 따르면, 애그리게이트에는 원소에 대한 접근을 캡슐화한 루트 엔티티가 있다. 원소마다 고유한 정체성이 있지만, 시스템의 나머지 부분은 루트 엔티티를 나눌 수 없는 단일 객체처럼 참조해야한다.
애그리게이트가 될 엔티티를 정의하고 나면 외부 세계에서 접근할 수 있는 유일한 엔티티가 되어야 한다는 규칙을 적용해야 한다. 즉, 허용되는 모든 저장소는 애그리게이트만 반환해야 한다.
1부가 끝났다. 2부에서는 1부에서 배운 기법을 확장해 분산 시스템에 적용하는 방법을 살펴본다.
2부에서는 다음 패턴과 기법을 살펴본다.
- 도메인 이벤트: 일관성 경계를 넘나드는 워크플로를 야기한다.
- 메시지 버스: 각 엔드포인트의 유스케이스를 호출하는 통합된 방법을 제공한다.
- CQRS: 이벤트 기반 아키텍처에서 구현을 이상하게 타협하는 경우를 피하고, 성능과 확장성을 높이기 위해 읽기와 쓰기를 분리한다.
8장. 이벤트와 메시지 버스
핵심 도메인과 정말 아무런 관련 없는 새로운 요구사항이 생긴다면 이런 요구사항을 아무 생각없이 웹 컨트롤러에 넣기 쉽다.
재고가 없으면 구매팀에게 이메일로 통지한다
다음과 같은 핵심 도메인과 정말 아무 관련없는 새로운 요구사항이 생긴 경우 다음 방법들을 살펴보자.
[1] 웹 컨트롤러에 넣기 → BAD
컨트롤러 여기저기에 기능을 끼워넣으면 전체가 빠르게 더러워진다. 이메일을 보내는 일은 HTTP 계층이 처리해야 할 일이 아닐 뿐더러, 이렇게 새로 추가된 기능에 대한 단위테스트를 진행할 수 있어야 한다.
[2] 모델에 위치시키기 → BAD
모델이 email.send_mail과 같은 인프라 구조에 의존하면 바람직하지 않다.
도메인 모델은 단지 “실제 할당할 수 있는 것보다 더 많은 상품을 할당할 수는 없다”라는 규칙에 집중해야 한다.
통지를 보내는 일은 다른 곳에서 한다. 이런 기능은 끌 수도 있고 켤 수도 있다. 또한 도메인 모델의 규칙을 변경하지 않아도 이메일 대신 SMS로도 통지를 보낼 수 있어야 한다.
[3] 서비스 계층에 추가하기 → BAD
“재고를 할당하려고 시도하고 할당에 실패하면 이메일을 보내야 한다”라는 요구사항은 워크플로 오케스트레이션에 속한다. 서비스 계층에 이 기능을 넣어도 겉도는 것 같다.
단일 책임 원칙
서비스 계층에 이 기능을 추가하는 것은 실제로 단일 책임 원칙에 위배된다. 여기서 처리하는 유즈케이스는 할당이다. 이름도 모두 allocate이지 allocate_and_send_mail_if_out_of_stock이 아니다.
📢 간단한 규칙
then이나 and라는 단어를 사용하지 않고 함수가 하는 일을 설명할 수 없다면 SRP를 위반하고 있을 가능성이 높다.메시지 버스에 전부 다 싣자
소개하려는 패턴: 도메인 이벤트와 메시지 버스
- 이벤트는 간단한 데이터 클래스다. 이벤트를 항상 도메인 언어로 이름을 붙여야한다. 항상 이벤트를 도메인 모델의 일부분으로 간주한다.
- 이벤트 수가 늘어나면 공통 애트리뷰트를 담을 수 있는 부모 클래스가 있는 것이 유용하다.
- 애그리게이터는 .events라는 새로운 애트리뷰트를 외부에 노출한다. 이 애트리뷰트에 발생한 일에 대한 사실은 event 객체 형태로 남긴 리스트에 있다.
도메인 이벤트는 시스템에서 워크플로를 다루는 또 다른 방법이다.
9장. 메시지 버스를 타고 시내로 나가기
9장에서는 애플리케이션의 내부 구조에서 이벤트를 더 근본적인 요소로 만드는 것으로 시작한다.
파라미터를 도메인 객체에서 → 원시 타입에서 → 이벤트를 도입하기까지
파라미터를 도메인 객체에서 기본 타입으로 바꾸면 연결을 멋지게 끊을 수 있다. 클라이언트 코드는 더이상 도메인과 직접 묶이지 않고, 그에 따라 모델을 변경해도 서비스 계층은 API를 변경하지 않고 예전과 같이 그대로 제공할 수 있고, 반대로 API가 변경되어도 모델은 그대로 남겨둘 수 있다.
이벤트 도입은 외부 세계와 이벤트 클래스를 연결할 뿐이다. 이벤트도 도메인의 일부지만, 이벤트는 도메인에 비해 훨씬 덜 자주 바뀔 것이라고 예상하면 연결을 해도 어느정도 타당한 대상이라 할 수 있다.
이벤트를 도입하면 애플리케이션의 어떤 유스케이스를 호출할 때 기본 타입들의 특정 조합을 기억할 필요가 없다. 단지 애플리케이션에 대한 입력을 표현하는 단일 이벤트 클래스를 사용하면 된다.
이를 통하여 서비스 계층 함수들은 이제 이벤트 핸들러가 됐다. 이로써 서비스 계층 함수 호출과 도메인 모델에서 발생한 내부 이벤트를 처리하기 위한 함수 호출이 동일해졌다.
이제 이벤트는 시스템 입력을 잡아내는 데이터 구조로 사용한다.
10장. 커맨드와 커맨드 핸들러
이전 장에서 모든 유스케이스 함수를 이벤트 핸들러로 변경했다. API로 새배치를 만드는 POST를 받으면 새로운 BatchCreated 이벤트를 만들어서 내부 이벤트처럼 처리한다. 이 처리 방식을 직관에 어긋나는 것처럼 보인다. 무엇보다 배치는 아직 생성되지도 않아서 API를 호출한다.
커맨드를 도입하여 이 문제를 수정해보자.
커맨드 vs 이벤트
이벤트와 마찬가지로 커맨드도 메시지의 일종이다. 시스템의 한 부분에서 다른 부분으로 전달되는 명령이 커맨드다. 이제 차이를 보자.
- 커맨드: 보내는 행위자는 받는 행위자가 커맨드를 받고 구체적인 작업을 수행하게 된다. 이는 명령형 동사구를 사용하고, 시스템이 어떤 일을 수행하길 바라는 의도를 드러낸다.
- 이벤트: 행위자가 관심있는 모든 리스너에게 보내는 메시지이다. 보내는 쪽은 받는 쪽의 성공이나 실패에 관심없다.
이벤트는 흐름을 방해할 수 없다.
한 이벤트를 여러 핸들러가 처리하도록 위임할 수 있는 디스패처로 이벤트가 처리된다. 하지만 커맨드 디스패처는 커맨드 한 개에 핸들러 한 개만 허용한다. (우리가 정한 관습에 따르면, 한 커맨드에는 핸들러가 하나 밖에 없다.)
사용자가 시스템이 어떤 일을 하기를 원한다면 이 요청은 커맨드로 표현한다. 커맨드는 한 애그리게이트를 변경해야 하고, 전체적으로 성공하거나 전체적으로 실패해야 한다.
11장. 이벤트 기반 아키텍처: 이벤트를 사용한 마이크로서비스 통합
내부적으로 이제 애플리케이션의 핵심은 메시지 처리기다. 이런 구성을 계속 따라서 메시지 처리기가 외부로도 메시지를 처리하도록 하자.
엔지니어들이 본능적으로 하는 일은 시스템을 명사로 나누는 것이다. 이는 간단한 시스템의 경우 이런 구조가 잘 작동하지만 곧 분산된 진흙공으로 악화되기 싶다. 요구사항이 추가될수록 의존성 그래프가 지저분해질 수 있다.
도메인 모델은 비즈니스 프로세스를 모델링하기 위함이다. 도메인 모델은 어떤 물건에 대한 정적인 데이터 모델이 아닌 동사에 대한 모델이다.
(ex. 주문에 대한 시스템과 배치에 대한 시스템을 생각하는 대신, 주문 행위에 대한 시스템과 할당 행위에 대한 시스템을 생각한다.)
애그리게이트와 비슷하게 마이크로서비스도 일관성 경계여야 한다.
분산 진흙 공 안티패턴을 방지하기 위해 시간적으로 결합된 HTTP API 호출하는 대신, 비동기 메시지로 시스템을 통합한다. 왜 이런 구조가 더 좋은 걸까?
- 각 부분이 서로 독립적으로 실패할 수 있어서 잘못된 동작이 발생했을 때 처리하기가 더 쉽다.
(ex. 할당 시스템이 안 좋은 날이라도 여전히 주문을 받을 수 있다.) - 시스템 사이의 결합 강도를 감소시킬 수 있다.
내부와 외부 이벤트의 구분은 명확히 하면 좋다. 일부 이벤트는 밖에서 들어오지만 일부 이벤트는 승격되면서 외부에 이벤트를 발행할 수 있다. 하지만 모든 이벤트가 다 외부에 이벤트를 내보내지는 않는다.
이벤트 소싱을 사용할 경우 이런 특징이 중요하다.
12장. CQRS
작업단위, 애그리게이트, 도메인 이벤트 패턴들은 시스템 상태를 변경할 때 규칙 적용을 강화하기 위해 존재한다. 즉, 데이터를 유연하게 쓰기 위한 도구를 만든 것이다.
쓰기 쪽에서 채택한 멋진 도메인 아키텍처는 시간에 따라 시스템을 진화하는 데 도움이 된다. 하지만 이 복잡도는 데이터를 읽는데 아무 역할도 하지 않는다. 서비스 계층, 작업 단위, 영리한 도메인 모델.. 다 너무 과하다.
도메인 모델은 읽기 연산에 최적화가 되어있지 않다. 쓰기와 읽기에 대한 요구사항과 상당히 다르기 때문이다.
도메인이 복잡할수록 도메인 모델과 CQRS 모두가 더 많이 필요해질 것이다.
완전히 정규화된 테이블은 쓰기 연산이 데이터 오염을 발생하지 않도록 보장하는 좋은 방법이지만, 데이터를 읽어올 때는 수많은 조인 연산을 사용하면 읽기 연산이 느려질 수도 있다.
이때 몇가지 정규화되지 않은 뷰를 추가하거나, 읽기 전용 복사본을 만들거나, 캐시 계층을 추가하면 좋다.
도메인 모델이 풍부해지고 복잡해질수록, 더 간소화한 읽기 모델이 훨씬 더 매력적으로 다가올 것이다.
13장. 의존성 주입(그리고 부트스트래핑)
데이터베이스 의존성의 경우 명시적 의존성을 사용하는 프레임워크를 주의 깊게 만들고 테스트를 위해 수비게 오버라디할 수 있는 옵션을 만들어주었다. 주 핸들러 함수는 UoW에 대해 명시적 의존성을 선언한다.
def allocate( cmd: commands.Allocate, uow: unit_of_work.AbstractUnitOfWork ):
명시적 의존성 정의는 의존성 역전 원칙의 예이다. 구체적인 세부 사항에 대한 암시적인 의존성을 사용하는 대신 추상화에 대한 명시적인 의존성을 사용한 것이 의존성이다.
명시적인 것이 암시적인 것보다 낫다.
- 파이썬의 선의존성 주입을 설정하는 것은 앱을 시작할 때 한번만 수행하면 되는 전형적인 설정/초기화 활동의 일부이다. 이 모두를 부트스트랩 스크립트에 넣는 것이 좋다.
이 책은 시스템이 복잡해질수록 어떻게 풀어나가는지 과정을 코드와 함께 보여준다! 왜 추상화가 중요한지, 왜 도메인 이벤트를 도입했는지 등 납득할만한 배경들을 잘 설명해주어서 함께 설계를 해나가는 느낌을 받았다.
특히 8장에
"핵심 도메인과 정말 아무런 관련 없는 새로운 요구사항이 생긴다면 이런 요구사항을 아무 생각없이 웹 컨트롤러에 넣기 쉽다."
의 문장이 내 정곡을 찔렀다ㅋㅋ
도메인 이벤트를 도입하면, 단일 책임 원칙을 지키도록 도와 주된 유스케이스와 부수적인 유스케이스를 분리해 깔끔히 유지시킬 수 있다. 개발하면서 요 도메인 이벤트를 도입하여 일관된 이벤트 처리를 해보고 싶다!
하지만 나는 Flask로 개발을 해본 적이 없다보니, 코드 보는 것에 어려움이 많았다. 그리고 주제 자체가 쉬운 주제가 아니다보니, 이해하는데에도 시간이 걸렸다. 개발 경험이 쌓이고 한 번 더 읽으면 또 다른 인사이트를 얻을 것 같다.
그래서 이번에는 이론을 많이 익힌 시간이었고, 다시 이 책을 볼 때 코드도 함께 작성해서 보려고 한다.
여러번 읽어 체화를 해보려고 한다!
728x90반응형'📚 개발 도서' 카테고리의 다른 글
[MongoDB] Sharded Cluster (0) 2023.09.11 - 애플리케이션 서비스 (서비스 계층): 외부 세계에서 오는 요청을 처리해 연산을 오케스트레이션한다.