-
파이썬으로 알아보는 아키텍처 패턴기술 2022. 2. 14. 12:32
책의 목적
여러가지 고전적 아키텍처 패턴을 소개하고 이 패턴들이 어떻게 DDD, TDD, 이벤트 기반 서비스를 지원하는지 보여주는 것이다.
도입
캡슐화와 추상화
- 캡슐화는 행동의 단순화와 데이터 은닉이라는 두 가지 특징을 가진다.
- 캡슐화해주는 객체나 함수를 추상화라고 한다.
params = dict(q='Sausages', format='json') handle = urlopen('http://api.duckduckgo.com' + '?' + urlencode(params)) raw_text = handle.read().decode('utf-8') parsed = json.loads(raw_text)
params = dict(q='Sausages', format='json') parsed = requests.get('http://api.duckduckgo.com', params=params).json()
두 코드는 같은 일을 하지만 두번 째 방식이 더 잘 읽히고 이해하기 쉽다. 두 번째 코드가 더 높은 수준의 추상화 아래서 동작하기 때문이다.
행동을 추상화로 캡슐화하는 것은 코드의 표현력을 더 높이고 테스트와 유지보수를 더 쉽게 만드는 강력한 도구다.
계층화
의존성이 꼬여있으면 어느 한 곳을 변경하기 어렵다. 계층화한 아키텍처는 이 문제를 해결하는 방법 중 하나이다. 계층화한 아키텍처는 코드를 서로 구분하는 범주나 역할로 분할하고, 어떤 코드 범주가 어떤 코드 범주를 호출할 수 있는지에 대한 규칙을 부과한다.
의존성 역전 원칙
의존성 역전 원칙의 엄밀한 정의는 다음과 같다.
- 고수준 모듈은 저수준 모듈에 의존해서는 안된다. 두 모듈 모두 추상화에 의존해야 한다.
- 두 모듈 모두 독립적으로 변경되길 원하기 때문에 추상화에 의존해야 한다.
- 추상화는 세부사항에 의존해서는 안 된다. 반대로 세부사항은 추상화에 의존해야 한다.
고수준 모듈은 정말 중요한코드다. 실세계의 개념을 처리하는 함수, 클래스, 패키지를 말한다.
저수준 모듈은 세부사항이다. 인프라, 네트워크, 프레임워크 등 상대적으로 덜 중요한 것들이다.
도메인 모델링을 지원하는 아키텍처 구축
대부분의 개발자는 시스템을 설계할 때 스키마부터 정의한다. 대신에 먼저 행동하고 저장에 대한 요구사항은 행동에 맞춰 정해져야 한다.
도메인 모델링
배우는 것
- 비즈니스 프로세스를 코드로 모델링하는 것
- TDD 와 호환이 잘 되는 방식
- 도메인 모델링이 왜 중요한지 알아보기
- 도메인 모델링을 하기 위한 핵심인 엔티티, 값 객체, 도메인 서비스에 대해 살펴보기
도메인 모델이란?
- 도메인: 소프트웨어로 해결하고자 하는 문제
- 모델: 무언가를 추상화한 지식
- 도메인 모델: 도메인 개념을 추상화 시킨 지식
- 즉, 개발자가 도메인의 전문용어를 이해하고 개념적으로 지식화 한 것이다.
도메인 언어 탐구
도메인 모델을 이해하기 위해서는 비즈니스의 전문가와 대화를 통해 최소한의 도메인 모델에 사용할 용어와 규칙을 몇가지 정해야 한다.
여기서 정해진 도메인 규칙이 TDD 로 사용되게 된다. 전문가들의 언어를 사용하고 합의한 내용을 그대로 코드로 만들어야한다.
값 객체로 사용하기 적합한 클래스
식별자의 여부에 따라 값 객체, 엔티티로 분류되어 사용된다.
모든 것을 객체로 만들 필요는 없다: 도메인 서비스
지금까지는 모델을 만들었다. 하지만 실제로 해야하는 것은 구체적인 배치 집합에서 주문 라인을 할당하는 것이다. 값 객체나 엔티티로 표현할 수 없는 도메인 서비스라는 연산이 존재한다.
저장소 패턴
저장소를 더 간단히 추상화 한것으로 이 패턴을 사용하면 모델 계층과 데이터 계층을 분리할 수 있다. 이런 간략한 추상화가 어떻게 데이터베이스의 복잡성을 감출 수 있다.
데이터 접근에 DIP 사용하기
도메인 모델에는 그 어떤 의존성도 없기를 바라니, 의존성이 내부로 들어오게 만들어야한다. 이런 방식을 양파 아키텍처라고 한다.
포트와 어댑터, 육각형 아키텍처는 양파 아키텍처와 거의 같은 의미다. 높은 계층 모듈이 저수준의 모듈에 의존해서는 안된다는 의존성 역전 원칙이다.
일반적인 ORM 방식: ORM 에 의존하는 모델
ORM 이 제공하는 가장 중요한 기능은 영속성 무지다. 도메인 모델이 데이터를 어떻게 적재하는지 또는 어떻게 영속화하는지에 대해 알 필요가 없다는 의미다. 영속성 무지가 성립하면 특정 데이터베이스 기술에 도메인이 직접 의존하지 않도록 유지할 수 있다.
의존성 역전: 모델에 의존하는 ORM
스키마를 별도로 정의하고, 스키마와 도메인을 상호 변환하는 매퍼를 정의하여 모델에 의존하는 ORM 을 생성할 수 있다.
저장소 패턴
ORM 을 사용하여 저장하던 것을 한번 더 래핑한 것을 저장소 패턴이라고 한다.
ORM 대신 저장소 패턴을 통해 코드에서 사용된다. 이렇게 하면 도메인 모델과 데이터베이스 사이의 결합을 끊을 수 있다.- 장점
- 영속성 저장소와 도메인 사이의 인터페이스를 간단히 유지할 수 있다.
- 모델과 인프라를 완전 분리했기 때문에 단위 테스트를 위한 가짜 저장소를 쉽게 만들 수 있고 저장소 해법을 변경하기도 쉽다.
- 영속성에 대해 생각하기 전 도메인 모델에 더 잘 집중하여 처리할 수 있다.
- 객체를 테이블에 매핑하는 과정을 원하는 대로 제어할 수 있어 데이터베이스 스키마를 단순화할 수 있다.
- 단점
- 외래키를 변경하기는 어렵지만, 저장소 자체를 변경하는건 어렵지 않다.
- ORM 매핑을 수동으로 하기위해서는 코드가 더 필요하다.
- 간접 계층은 유지보수 비용이 증가한다.
결합과 추상화
- 좋은 추상화를 만드는 조건
- 무엇을 원하는가와 어떻게 달성할지를 분리한다.
- 추상화로부터 얻을 수 있는 것
- 추상화를 통해 세부 사항을 감추면 시스템의 결합 정도를 줄일 수 있다.
- 의존성을 직접적으로 가지지 않아 변경으로 부터 보호받을 수 있다.
- 테스트와 추상화의 관계
- 추상화를 통해 테스트를 더 쉽게 해준다.
Mock 을 선호하지 않는 이유
- 단위 테스트는 가능하지만 설계를 개선하는데 도움이 되지 않는다.
- Mock 을 사용한 테스트는 구현 세부사항에 더 밀접히 결합된다.
- 과용하면 테스트가 복잡해진다.
유스 케이스: 플라스크 API 와 서비스 계층
무엇을 설명하는가
- 오케스트레이션 로직, 비즈니스 로직, 연결 코드 사이의 차이점
- 서비스 계층 패턴의 소개
오케스트레이션
저장소에서 여러가지를 가져오고, 데이터베이스 상태에 따라 입력을 검증하며 오류를 처리하고, 성공적인 경우 데이터를 데이터베이스에 커밋하는 작업을 포함한다.
플라스크 앱
책임은 표준적 웹 기능뿐이다. 즉, 요청 전 상태를 관리하고 POST 파라미터로부터 정보를 파싱하며 상태 코드를 응답하고 JSON 을 처리한다. 모든 오케스트레이션 로직은 유스 케이스/서비스 계층에 들어가고, 도메인 로직은 도메인에 그대로 남는다.
서비스 계층
- 장점
- 애플리케이션의 모든 유스 케이스를 넣을 유일한 위치가 생긴다.
- 정교한 도메인 로직을 API 뒤에 감췄다. 이로 인해 자유롭게 리팩터링이 가능하다.
- HTTP와 말하는 기능을 할당을 말하는 기능으로부터 깔끔히 분리했다
- 통합 테스트를 사용하지 않아도 워크 플로 중 상당 부분을 테스트할 수 있다.
- 단점
- 서비스 계층에 너무 많은 기능을 넣으면 빈약한 도메인 안티패턴이 생길 수 있다. 컨트롤러에서 오케스트레이션 로직이 생길 때 서비스 계층을 만드는 게 낫다.
테스트
도메인 계층 테스트를 서비스 계층으로 옮겨야 하나?
서비스 계층의 테스트를 하면 더는 도메인 모델 테스트가 필요 없다. 하지만 도메인 수준의 테스트를 서비스 계층에 대한 테스트로 재작성한다.
왜 이렇게 해야할까?
도메인 모델에 대한 테스트가 너무 많으면 코드베이스를 바꿀때마다 많은 테스트를 변경해하는 문제가 생긴다. 그래서 나중에 서비스 계층 기반 테스트로 대신할 수 있다면 주저말고 바꾸자.
어떤 종류의 테스트를 작성할지에 대한 트레이드 오프
- API 테스트
- 피드백 적음
- 변경 장벽 낮음
- 더 넓은 시스템 테스트
- 도메인 테스트
- 피드백 많음
- 변경 장벽 높음
- 한정된 영역 테스트
도메인 테스트는 도메인 코드와 밀접하므로 설계에 대한 피드백을 받을 수 있지만 API 테스트는 높은 수준의 추상화를 사용하므로 객체의 세부 설계에 대한 피드백을 제공할 수 없다.
테스트는 도메인 언어로 작성되므로 살아있는 문서 역할을 한다. 새로운 팀원은 테스트를 읽고 시스템이 어떻게 동작하는지 빠르게 이해하고 핵심 개념이 어떻게 연관되어있는지 이해할 수 있다.
서비스 계층 테스트를 도메인으로부터 완전히 분리하기
도메인으로부터 서비스계층을 완전히 분리하기 위해서는 API 를 원시 타입만 사용하게 해야한다. 그리고 도메인을 가져오거나 관리하는 전용의 서비스를 따로 만들어 호출하게 한다.
이렇게 되면 서비스 계층 테스트는 오직 서비스 계층에만 의존하게 된다.
작업 단위 패턴
저장소 패턴이 영속적 저장소 개념에 대한 추상화라면 작업 단위 패턴은 원자적 연산이라는 개념에 대한 추상화이다. 이 패턴을 사용하면 서비스 계층과 데이터 계층을 완전히 분리할 수 있다.
복잡한 비즈니스 로직으로부터 무결성을 확보하기 위해 django 의 transaction.atomic 을 구현한거라 보면 될 것 같다.
애그리게이트와 일관성 경계
- 도메인 모델을 다시 살펴보면서 불변조건과 제약에 대해 살펴본다.
- 도메인 모델 객체가 개념적으로나 영속적 저장소 안에서나 내부적으로 일관성을 유지하는 방법을 살펴본다.
- 일관성 경계를 설명하고, 일관성 경계가 어떻게 유지보수 편의를 해치지 않으면서 고성능 소프트웨어를 만들 수 있게 도와주는지 살표본다.
도메인 모델 대신 스프레드 시트를 사용하면 안될까?
초기 복잡성이 낮지만 로직을 적용하기 힘들고 일관성을 유지하기 힘들다.
도메인 모델은 시스템의 제약을 강제로 지키게 해서 시스템이 만족하는 불변조건을 유지할 수 있다.
불변 조건, 제약, 일관성
- 제약은 모델이 취할 수 있는 상태의 수를 제한한다.
- 불변조건은 항상 참인 조건이다.
- 경우에 따라 일시적으로 규칙을 완화해야할 수 있으며, 모든 작업이 끝나면 도메인 모델은 불변조건을 모두 만족하는 일관성 있는 상태로 끝난다는 사실을 보장해야한다.
동시성
애그리게이트 패턴은 다른 도메인 객체들을 포함하여 객체 컬렉션 전체를 한꺼번에 다룰 수 있게 해주는 도메인 객체다.
외부에서 애그리게이트의 객체를 변경하는 유일한 방법은 애그리게이트의 메서드를 호출하는 방법뿐이다. 즉, 단일 진입점이된다. 이렇게 하면 개념적으로 더 간단해지고 어떤 객체가 다른 객체의 일관성을 책임지게 하면 시스템에 대해 추론하기 쉬워진다.
애그리게이트 선택
애그리게이트는 모든 연산이 일관성 있는 상태에서 끝난다는 점을 보장하는 경계가 된다. 서로 일관성 있어야 하는 소수의 객체 주변에 경계를 설정하고, 성능을 위해서는 경계가 더 작을 수록 좋다. 소수의 객체들을 개념적으로 묶을 수 있는 단어를 선택해 애그게이트로 설정한다.
이벤트 기반 아키텍처
위의 내용은 도메인 모델을 하나만 작성하면 아무 문제가 없지만, 여러 도메인을 사용하게 되었을 때 문제가 발생한다.
- 도메인 이벤트 : 일관성 경계를 넘나드는 워크플로를 야기한다.
- 메세지 버스 : 각 엔드포인트의 유스 케이스를 호출하는 통합된 방법을 제공한다.
- CQRS: 이벤트 기반 아키텍처에서 구현을 이상하게 타협하는 경우를 피하고 성능과 확장성을 높이기 위해 읽기와 쓰기를 분리한다.
이벤트 메세지와 버스
만약 한 곳에서 여러 관심사를 다뤄야 할 때 ( 특정 상황에 메일을 보내야한다던가, 이런 예외적인 케이스 ) 이벤트는 단일 책임 원칙을 도울 수 있다.
유스케이스와 부수적인 유스케이스를 분리해 모든 것을 깔끔하게 유지할 수 있다.
모든 것을 이벤트로
- 이런 아키텍처 패턴을 채택한 목적은 애플리케이션의 크기가 커지는 속도보다 복잡도가 증가하는 속도를 느리게 만들기 위해서이다.
- 비즈니스 언어로 잘 번역된다. ( 실 세계의 행동이 즉 이벤트이기 때문 )
커맨드와 커맨드 핸들러
커맨드도 메세지의 일종이지만 시스템의 한 부분에서 다른 부분으로 전달되는 명령이 커맨드이다. 보통 커맨드는 아무 메서드도 들어있지 않는 데이터 구조로 표현하고 이벤트와 거의 같은 방식으로 처리한다.
커맨드는 한 행위자로부터 다른 구체적인 행위자에게 전달된다. 보내는 해위자는 받는 행위자가 커맨드를 받고 구체적인 작업을 수행하길 바란다.
커맨드는 의도를 잡아낸다. 시스템이 어떤 일을 수행하길 바라는 의도를 드러낸다. 그 결과, 커맨드를 보내는 행위자는 커맨드 수신자가 커맨드 처리에 실패했을 때 오류 정보를 돌려받기를 바란다.
이벤트는 행위자가 관심있는 모든 리스너에게 보내는 메세지이다. 이벤트를 발행해도 발행하는 행위자는 누가 이 이벤트를 받는지 모른다. 이벤트를 보내는 쪽은 받는 쪽의 성공이나 실패에 관심 없다.
즉, 이벤트는 흐름을 방해할 수 없고, 커맨드는 에러를 전파한다.
'기술' 카테고리의 다른 글
Nginx 를 통해 Cloudfront 를 사용하는 방법 (0) 2021.09.24 터미널에서 특정 포트를 Kill 하는 방법 (0) 2021.09.24 TAG