발표자료

Architecture patterns with python (2)

발표 전달력이 매우 좋지 않았음. 그에 따른 자기 피드백

  • 한 챕터만 자세히 하기(3챕터를 너무 얕게 설명함)
  • 코드를 주로 설명하기 (도식보다 예제 코드가 더 이해시키기 쉬움)
  • 파이썬 내용을 빼야겠다. 너무 아는게 다름. (소공 관점의 얘기만 담기.)
architecture patterns with python, Python

Architecture Patterns with Python – 5

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

4장. 서비스 계층

이번 장에선 시스템의 유즈 케이스를 정의하는 서비스 계층 패턴을 소개하고 이전에 배운 저장소 추상화와 연계해서 테스트를 작성하는 법을 배운다.

그리고 서비스 계층을 만드는 데 가장 헷갈리는 오케스트레이션 로직, 비즈니스 로직, 인터페이스 사이의 차이를 설명한다.

End-to-end Test(E2E)

실제 데이터베이스와 API 엔드포인트를 사용해서 데이터베이스부터 API 까지 테스트하는 걸 엔드투엔드 테스트라고 한다.

서비스 계층

이 테스트를 통과하기 위해선 도메인 모델과 추상화된 저장소 이외에도 데이터를 저장소로부터 가져와서 입력을 검증하고, 성공일 경우 데이터를 커밋하는 과정도 필요하다. 이런 코드들은 웹 API에 종속되지 않는다. 웹 대신 CLI로 구현한다고 해도 똑같이 존재한다. 따라서 이 코드들이 커진다면 웹 API 코드가 복잡해지기 때문에 구분해줄 필요가 있다.

이런 코드들은 외부에서 온 요청을 받아서 내부를 제어하기 위해 있다. 그래서 오케스트레이션 계층 또는 유즈 케이스 계층, 서비스 계층이라고 한다. 이들이 하는 역할의 예는 다음과 같다.

  • 데이터베이스에서 데이터를 얻는다.
  • 도메인 모델을 업데이트 한다.
  • 변경된 내용을 영속화 한다.

서비스 계층을 사용할 때의 장점

서비스 계층으로 묶을 때의 장점은 다음과 같다.

API에서 오케스트레이션 로직을 신경 쓰지 않아도 되므로 코드가 줄어들고 작성하기 쉬워진다. 오케스트레이션 부분이 복잡해지더라도 API에선 특정 유즈 케이스에 맞는 서비스 계층을 호출하고 결과에 대한 예외 처리만 하면 된다.

오케스트레이션 부분에 대해서만 테스트 할 수 있다. 웹 API는 실제 HTTP 통신을 사용해서 E2E 테스트를 해야하지만, 오케스트레이션 부분만 테스트 할 땐 통신으로 인한 지연 없이 더 빠른 테스트가 가능하다. 여기서 추상화된 저장소에 의존한다면, 가짜 저장소를 만들어서 데이터베이스에 대한 지연도 배제할 수 있다.

또한 같은 유즈 케이스에 대해서 정상인 경우와 비정상인 경우를 테스트하더라도 반복되는 코드의 양을 줄일 수 있다.

서비스 계층 덕분에 API와 도메인 모델 사이의 의존도 사라졌다. 서비스 계층에서 API가 요청한 결과로 가공해서 반환하기 때문이다.

서비스 계층을 사용할 때 고려할 점

서비스 계층은 유즈 케이스 내에서 도메인 모델, 저장소, 웹 API 등의 복잡한 상호작용에 추상화를 적용하는 것에 불과하다. 그래서 반대로 복잡하지 않다면 서비스 계층을 넣는 것이 불필요하거나 오히려 안티 패턴이 될 수 있다. 예를 들어 정해진 웹 페이지만 띄우는 순수한 웹 앱이거나 도메인 모델이 컨트롤러가 요구하는 대부분의 로직을 처리할 수 있는 구조를 들 수 있다. 그러므로 서비스 계층은 기존의 컨트롤러-도메인 구조가 점점 복잡해질 때에 고민하는 게 좋다.

이후 5장에선 서비스 계층과 도메인 모델 간의 커플링을 해결하는 것을 다루고 6장에선 저장소와 서비스 계층 사이에 활용하는 작업 패턴을 다룬다.

프로그래밍방법론, 학습노트

읽기 좋은 코드가 좋은 코드다 – 1

출처: 랜각코 스터디 – 책 “읽기 좋은 코드가 좋은 코드다” 1장 일부

코드는 이해하기 쉬워야 한다

가독성의 기본 정리: 이해하는데 들이는 시간을 최소화. 이해한다는 건 디버깅과 수정이 가능한 수준을 말함.

표면적 수준에서의 개선

이름에 정보 담기

  • 특정한 단어 고르기: thread.stop() 보다는 완전 종료면 kill, 일시 정지면 pause가 맞음. 더 구체적인 의미를 담아라.
  • 보편적인 이름 피하기: tmp, result, foo 같은 이름은 책임 회피. 예외도 있지만 최대한 자제하자.
  • 추상적인 이름 대신 구체적인 이름을 사용하기:
    DISALLOW_EVIL_CONSTRUCTORS -> DISALLOW_COPY_AND_ASSIGN
  • 접두사 혹은 접미사로 이름에 추가적인 정보 덧붙이기
  • 이름이 얼마나 길어져도 괜찮은지 결정하기
  • 좁은 범위에서만 사용한다면 짧은 이름도 괜찮다
  • 긴 이름 입력은 IDE의 발전 덕분에 더 이상 귀찮지 않다.
  • 약어와 축약형: 저자 생각은 일반적으로 받아들여지는 약어는 괜찮다.
  • 불필요한 단어 제거하기
  • 추가적인 정보를 담을 수 있게 이름 구성하기: 언더바, 하이픈, 대문자는 좋은 도구들. ex) 구글 C++ 컨벤션의 멤버 변수
  • 오해할 수 없는 이름
  • 경계를 포함하는 한계값: min/max, first/last, begin/end
  • boolean 변수 이름 붙이기
  • 사용자의 기대에 부응하기: getXXX는 값을 가져오는 가벼운 메서드로 보이기 때문에 연산이 필요하면 이름을 바꾸는게 좋음.

미학

시각적으로 좋은 코드가 읽기도 좋다.

  • 일관성과 간결성을 위해서 줄 바꿈을 재정렬하기
  • 줄 바꿈이 어렵다면 메소드를 활용하라
  • 도움이 된다면 코드의 열을 맞춰라: 오타를 찾기 쉬워짐. 하지만 굳이 강제하진 않음. 불편하면 포기해도 됨.
  • 의미 있는 순서를 정해서 일관성 있게 사용하라: ex) 매개변수들의 순서를 정해서 계속 같은 순서대로 초기화하고 참조하고 수정해야 함.
  • 선언문을 블록으로 구성하라: 관련된 부분들끼리 묶어두기
  • 코드를 문단으로 쪼개라: 논리 전개에 맞춰서 나누자.
  • 미학은 개인적인 스타일이지만 팀이라면 일관성을 지켜야 한다.

가상화, 학습노트

랜각코-K8S 공부 내용

정리되지 않음.

이전 발표 수정: VM VS Containers

VM: 하이퍼바이저 위에서 게스트 OS 실행

Containers: 호스트 OS의 커널에서 실행하는 가상 프로세스

이번 주제: 그래서 뭔 기술을 써서 이렇게 주어진 컴퓨터 자원을 가상으로 쪼개고 그룹화 하고 할 수 있는가?

Docker란 무엇인가

컨테이너를 쉽게 배보할 수 있는 프로그램

컨테이너: 호스트 커널을 공유하고 프로세스의 실행영역을 격리

namespace: 프로세스 실행 환경 격리

Cgroups: 리소스 분할 사용

클라우드 환경을 제공하기 위해 컨테이너의 필수 조건

  1. fake root path 제공 – host와 달리 container가 독립된 fake root 가 필요하다
  2. Isolation – 호스트의 파일시스템, 프로세스 트리, 네트워크 등이 분리되어야 한다.
  3. root 권한 사용 – 리눅스 루트 권한 사용 가능 여부와 그에 따른 보안 문제 관리
  4. HW 리소스 제한, CPU, 메모리, I/O, 네트워크 등 호스트의 자원에서 할당된 양만큼만 사용해야 함.

chroot

루트 디렉토리를 바꾸는 명령어

원격 유저에게 특정 루트 디렉토리로 격리하기 위해 사용함. 컨테이너의 시작.

chroot 실습

단순히 새 디렉토리에 chroot를 실행하면 안됨.

오류 메세지를 보면 bash가 없어서 그럼. -> 그래서 새 루트로 bash를 옮겨줘야 함. bin도 옮기고 lib도 옮겨줘야 함.

그러면 bash로 새 루트로 접속이 됨.

하지만 ls같은 기본 명령어도 안됨. -> 역시 새 루트에선 ls를 실행할 수 없기 때문에 bash처럼 bin을 옮겨줘야 함.

chroot 후에 새 루트로 들어간 후에는 아무리 새 루트 위에 상위 디렉토리가 있다 해도 올라갈 수 없음.

chroot가 너무 귀찮아

-> 새 루트를 위한 파일들을 미리 묶어두고 복붙하면 될 것 같아
-> 그래서 tarball로 압축해둔게 image

그래서 docker의 이미지를 tar로 압축 해제하면 chroot가 가능한 디렉토리가 세팅되어 있음.

chroot의 치명적인 문제점

  1. fake root path 제공 가능하지만 탈옥이 됨.
  2. isolation 되지 않음. 호스트 파일시스템에 직접 접근 가능
  3. 루트 권한 사용 불가
  4. resource 무제한

대안점: pivot_root

루트 파일시스템을 변경함.

-> 원래 chroot는 pid 1의 정보를 상속받기 때문에 파일 시스템에 남은 상위 디렉토리로 이동 가능(탈옥)
-> pivot_root는 프로세스가 보고 있는 파일 시스템을 바꿔서 탈옥이 불가능함.

이러면 fake root path에 대한 문제가 완전히 해결됨. 이제 격리와 리소스 관련 문제를 해결해야 함.

Namespace

네임스페이스는 프로세스에 격리된 환경과 리소스를 제공함. 7개가 있음.

time, syslog는 아직 전부 구현되지 않아서 docker같은 컨테이너 구현체마다 다르게 구현되어 있음.

CGroup

프로세스에 할당하는 리소스를 제어하는 파일시스템

그룹별로 여러 장치들을 묶어서 각각 어떻게 자원을 사용할 지 파일로 기록해둠. 그리고 이 그룹에 프로세스를 바인딩함.

실습: stress 명령어로 CGroup 테스트하기

정리

컨테이너는 프로세스다.

  1. tarball에 의해 탄생하고
  2. namespace에 고정되어 있으며
  3. cgroup에 의해 제어되고 있다.

그래서 왜 VM보다 컨테이너가 용량이 적음?

이미지 최적화 문제

ubuntu + nginx + mysql + …

ubuntu + nginx + tomcat + …

이러면 중복된 이미지가 많아져서 공간 문제가 발생하는거 아닌가?

union filesystem

복수의 filesystem을 하나로 마운트하는 기능

두 파일시스템에 동일한 파일이 있는 경우, 나중에 마운트되는 파일시스템의 파일을 오버레이함.

하위 파일시스템에 대한 쓰기 작업 진행시 Copy on Write. 복사본을 생성하여 수행

일명 상속 파일시스템이라고 불림.

현재는 OverlayFS2를 사용.

이미지 -> 병합된 이미지 -> 컨테이너 내의 수정본 으로 계층으로 쌓여있음.

Docker에선 layer DB에서 관리함.

그래서 VM은 위에서처럼 약간의 변화가 있는 각각의 가상머신을 구분해서 저장하기 때문에 차지하는 공간이 크지만, 컨테이너는 중복된 부분을 union filesystem으로 관리하기 때문에 공간을 작게 차지한다.

architecture patterns with python, Python

Architecture Patterns with Python – 4

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

3장. 막간: 결합과 추상화

이 장에선 도메인 모델링이라는 핵심 주제에서 벗어나서 ‘추상화’에 대해 집중한다.

대규모 시스템에서는 시스템의 다른 부분에서 이루어진 결정에 의해 의사결정이 제한될 수 있다. 그래서 만약 A 컴포넌트가 B 컴포넌트를 변경할 수 없는 경우, 서로 ‘결합’되었다고 한다.

작은 범위에서 결합된 코드들이 서로 잘 맞물려서 돌아가는 거는 ‘응집’되었다고 표현하고 이는 바람직하다.

하지만 넓은 범위에서 결합이 생기면 코드를 변경하는 비용이 크고, 아예 변경이 불가능할 수도 있다. 그래서 추상화를 통해 세부 사항을 감추면 시스템 내 결합 정도를 줄일 수 있다. B를 변경하면서 A도 변경하는 것 대신 사이에 놓인 추상화가 B에 의존하는 부분만 수정하면 A는 변경하지 않아도 되기 때문이다.

apwp 0302

추상화로 테스트를 쉽게 만들기

책에선 두 파일 디렉터리를 동기화하는 코드를 작성하면서 이를 설명한다.

제시된 요구사항에 맞춰서 한 함수로 구현하면, 파일을 읽고, 같은 파일인지 확인한 후, 동기화를 수행하는 코드를 작성한다. 이러면 함수 내에 도메인 로직과 IO 코드가 결합되어 있다. 단순히 함수를 테스트하고 싶어도 테스트 코드에도 임시 파일을 생성하는 IO 로직이 들어가야 되며, 실제 IO가 발생하지 않고 실행 과정만 출력 시키고 싶다거나, 클라우드 같은 원격 서버와의 동기화로 변경하려고 한다면, 결합된 도메인 로직도 변경해야 한다.
(실패할 수 있는 구간에서 실행 과정을 확인하는 테스트를 dry run이라고 부른다. 소방서에서 실제로 물을 뿌리지 않고 화재 훈련하는 거에서 가져왔다.)

테스트 하기 쉽게 코드를 다시 작성하려면 어떻게 해야 할까?

먼저 코드에서 뚜렷한 책임을 구분한다. 여기선 다음과 같이 구분한다.

  1. 동기화 할 두 디렉토리의 파일들을 읽고, 동기화 방식을 선택하기 위한 데이터를 만듦.
  2. 파일마다 어떤 동기화 방식을 적용할지 선택함. (책에선 new, renamed, duplicated가 있었음.)
  3. 실제로 동기화를 수행함. 파일을 복사하거나 이름을 똑같이 맞춰주거나 제거함.

그 다음, 이 책임들을 각각 추상화를 통해 단순하게 만든다. 어떻게 파일을 읽을지 동기화 시킬 지는 감추고, 비즈니스 로직은 동기화하는 과정에만 초점을 맞출 수 있게 한다. 그러기 위해선 각각의 책임에서 “무엇을 하길 원하고, 어떻게 달성할지”를 구분한다. ‘무엇’이 추상화되어야 하고, ‘어떻게’는 세부 사항으로써 감춰져야 한다. 예로 들어 2번의 경우, 파일마다 각각 동기화 방식을 선택하길 원하고, 어떻게 동기화 방식을 결정할 지는 감춰지게 된다.

선택한 추상화 구현하기

이 과정의 목표는 실제 파일 시스템 없이도 테스트를 할 수 있게 하는 것이다. 그러기 위해서 외부에 아무런 의존성이 없는 코드의 ‘핵’을 만들고 외부에서 이 핵에 어떻게 반응하는지 살펴볼 수 있게 해야 한다.
(게리 번하트는 Functional Core, Imperative Shell 라고 이 접근 방법을 설명했다.)

그래서 핵을 먼저 분리한다. 코드에서 IO 없이 데이터만 주어지면 로직으로 처리 가능한 부분을 찾아서 분리한다. 예를 들어 위에 2번은 이렇게 분리 가능한 핵에 해당되고, 하나의 함수로 분리하면 실제 파일 시스템 없이 2번에 대해서만 테스트가 가능해진다.

의존성 주입으로 해결하기

가짜 IO에 대해서도 테스트 할 수 있도록, 1번과 3번을 구현할 때, IO 대상을 외부에서 받아오도록 수정할 수 있다. 1번 과정의 코드가 실제 파일 시스템이 아닌 함수의 매개변수를 통해 외부에서 파일 시스템 객체를 받아서 파일을 읽게 하는 것이다. 외부의 파일 시스템은 파일을 읽고 쓰는 기능이 인터페이스로 열려 있어야 한다.

이렇게 하면 IO가 테스트를 위해 바뀌더라도 우리가 지금까지 구현한 동기화 코드가 수정되지 않는다. 단, 이제부터 매번 명시적으로 파일 시스템을 주입 시켜줘야 된다. 이를 “테스트가 야기한 설계 손상”이라고 부르기도 한다.

mock.patch는 사용하지 않는 이유

Python에선 mock.patch를 쓰면 코드를 수정하지 않고 단위 테스트를 수행할 수 있다. 하지만 책의 저자들은 이 방법이 코드 냄새를 유발한다고 생각하고, 위의 과정처럼 코드의 책임을 명확하게 구분하고, 테스트 더블로 대체하기 쉽게 책임들을 작은 크기로 만드는 게 도움이 된다고 생각한다.
(테스트 더블은 테스트 시 복잡한 실제 시스템을 고려하지 않도록 만들어주는 객체들이다. stub이나 dummy 같은 것들을 말한다.)

여기선 정확히는 세 가지 이유를 들었다.

  1. 단위 테스트는 가능하지만 원격 서버나 dry run 기능 추가는 할 수 없다.
  2. mock.patch로 수정한다는 거 자체가 테스트 코드가 세부 사항과의 결합이 강해진다.
  3. mock을 과용하면 테스트 스위트의 가독성이 떨어진다. 뭘 위한 테스트인지 알기 어렵다.

3번과 같은 이유를 든 건, 저자들은 TDD가 테스트 이전에 설계를 위한 기법이라고 생각하기 때문에 테스트에서 설계를 알아볼 수 없으면 문제가 있다고 보기 때문이다.

마무리: 올바른 추상화를 찾기 위한 자기 검증

휴리스틱한 방법이지만 책에 적혀 있는 것. 이건 연습을 통해 나아진다고 한다.

  • 지저분한 시스템의 상태를 표현할 수 있는 익숙한 파이썬 객체가 있는가? 그렇다면 이 파이썬 객체를 사용해 시스템 상태를 반환하는 단일 함수를 상상해봐라.
  • 시스템의 구성 요소 중 어떤 부분에 선을 그을 수 있는가? 이 선을 통해 구분되는 각각의 추상화 사이의 이음매를 어떻게 만들 것인가?
  • 시스템의 여러 부분을 서로 다른 책임을 지니는 구성 요소로 나누는 합리적인 방법은 무엇일까? 명시적으로 표현해야 하는 암시적 개념은 무엇인가?
  • 어떤 의존 관계가 존재하는가? 핵심 비즈니스 로직은 무엇인가?
C/C++, Python

Python Pathlib의 / 연산자 오버라이드

/ 연산자를 창의적으로 활용했다고 생각되어서 소개하고 싶다.

Python에서 경로를 합치는 방법은 os.path.join()을 생각하는데, Pathlib에선 연산자 오버라이드로 이렇게 쓴다.

file_path = Path(folder_path) / file_name

C++도 연산자 오버라이드를 지원하는 언어라서 찾아봤는데, 범용적으로 많이 쓰는 boost 라이브러리의 하위의 Boost.Filesystem 에선 /= 연산자를 오버라이드 해둬서 경로 추가를 할 수 있다.

https://www.boost.org/doc/libs/1_78_0/libs/filesystem/doc/reference.html#path-appends

architecture patterns with python, 프로그래밍방법론

Architecture Patterns with Python – 3

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

저장소 패턴

저장소 패턴은 데이터 저장소를 간단한 형태로 추상화한 것이다. 이를 통해 모델과 저장소를 분리할 수 있고, 저장소에 대한 테스트를 용이하게 해준다.

저장소 패턴을 적용하기 전에 먼저 저장소를 사용해야 되는지 확인한다. 우리가 구현할 시스템에 저장소가 필요하다면, 최소 기능 제품(minimum viable product)을 만들기 위해 추상화된 저장소를 구상해야 한다.

예로 들어 웹 서비스라면 웹 API가 최소 기능 제품이 된다. 책에선 할당 기능의 엔드포인트를 보여준다. 여기서 batch를 저장소에서 가져와서 새로 할당한 후에 저장하는 걸로 구상했기 때문에, 추상화된 저장소 또한 batch를 가져오고 저장하는 기능이 필요하다.

데이터 접근에 DIP 적용하기

일반적으로 UI-로직-데이터베이스 가 필요한 시스템을 구상할 때, 계층 아키텍쳐를 적용한다. 하지만 실제로 로직 부분인 도메인 모델에는 어떤 의존성이 생기지 않는게 좋다. 정확히는 어떤 상태가 있는 의존성은 없어야 된다는 말이다. 예로 들어 helper 라이브러리는 괜찮지만 ORM이나 프레임워크에 의존해선 안된다는 말이다. 따라서 UI와 데이터베이스에서 비즈니스 로직을 일방적으로 의존하는 계층 구조를 취한다. 이를 양파 아키텍쳐라 부른다.

양파 아키텍쳐

주의하기: ORM에 의존하는 모델

ORM은 SQL 대신 OOP의 방식으로 데이터베이스에 접근해줄 수 있게 해주는 프레임워크이다. 따라서 가장 중요한 기능이 ‘영속성 무지'(persistence ignorance)다. 도메인 모델이 어떻게 영속화되는지 알 필요가 없게 해준다. 이를 통해 특정 데이터베이스 기술에 도메인이 직접 의존하지 않도록 유지할 수 있다.

하지만 ORM의 튜토리얼을 보면 도메인 모델을 ORM의 프로퍼티에 기반해서 도메인 모델을 만들게 된다. 그러면 오히려 ORM에 의존하는 모델을 만들게 된다. 그에 따른 문제점으로 도메인 모델이 객체 지향이 아닌 다른 게 필요한다면 ORM 때문에 벗어나기 어려워진다는 게 있다. (이것도 Python을 사용하기 때문에 고려한 문제라고 본다.) 그리고 도메인 모델을 테이블로 매핑하는 방법을 원하는대로 제어하기 힘들다.

저장소 패턴 적용하기

책에선 ORM이 아닌 조금 예전 방식인 mapper를 사용한다. 이럴 경우, mapper는 도메인 모델에 종속되지만 반대로 도메인 모델에선 mapper를 알지 못한다. 덕분에 만약 우리가 다른 ORM을 사용한다고 하더라도 도메인 모델은 수정되지 않는다.

저장소를 추상화 하기 전에 먼저 데이터베이스에 접근하기 위해 사용하는 도구들이 정상 작동하는지 테스트한다. 예로 들어 mapper가 제대로 작동하는지 SQL 구문을 같이 호출해서 검증하는 테스트 코드를 짤 수 있다.

그 후에 어떻게 추상화할 건지 생각해보자. 위에서 본 batch의 경우, 저장을 위한 add()와 가져오기 위한 save()가 필요하다. 그렇다면 이에 맞춰서 저장소의 인터페이스를 만들면 된다.

Python에선 ABC(추상 기반 클래스)와 duck typing, 프로토콜 로 추상화된 저장소를 구현할 수 있다. ABC는 무시하고 코딩하기 쉬워서 보통은 duck typing에 의존해서 추상화를 구현한다. 프로토콜은 상속을 사용하지 않고 타입을 지정할 수 있는 대안이다. 특히 상속보다 구성(composition)이라는 규칙을 선호한다면 좋은 대안이다.

추상화된 저장소를 만들었다면, 다음은 저장소 구현체에 대한 테스트를 먼저 짠다. 정말로 구현체를 통해 DB와 연결되었는지 확인하기 위해 SQL문의 실행 결과와 구현체의 결과가 같은지 검증한다.

테스트를 만들었다면 이 테스트를 통과하기 위한 구현체를 만든다.

책에서는 ‘포트’와 ‘어댑터’라는 용어를 쓴다. ‘포트’는 애플리케이션과 추상화 대상 사이의 인터페이스를 말하고, ‘어댑터’는 이 인터페이스 뒤에 있는 구현체를 말한다.

트레이드오프

아키텍쳐 패턴을 적용할 때 반드시 뒤따라오는 대가도 고려해야 한다.

위에서 ORM 의존 모델 대신 저장소를 추상화하겠다는 패턴을 적용할 때는 전체적인 복잡도는 줄어들 수 있다. 그 덕분에 여러 종류의 저장소를 쓸 때나 테스트를 위한 가짜 저장소를 만들 때, 도메인 모델이 영향받지 않을 수 있다.

하지만 이 패턴도 단점이 있다. ORM 의존 모델의 경우, 수동으로 매핑한 코드를 만들고 유지보수하는 비용이 발생하지 않는다. 마지막으로 ORM이 지원하는 데이터베이스 범위 내에서의 변경이라면 ORM도 도메인 모델과 저장소 사이의 의존성 문제를 이미 해결해주고 있다.

책에선 그 트레이드오프를 다음과 같은 그래프로 보여준다.

apwp 0206

여기까지가 저장소 패턴에 대한 내용이다. 다음은 서비스 계층으로 넘어갈 것이다.

architecture patterns with python, 프로그래밍방법론

Architecture Patterns with Python – 2.2

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

값 객체

값 객체는 데이터는 있지만 유일한 식별자가 없는 개념을 표현하기 위한 방법이다.

예로 들어 사람들에게 지폐는 서로 다른 지폐더라도 같은 값어치가 있으면 같다고 취급한다. 물론 실제로는 지폐마다 식별자가 있긴 하지만, 거래 시스템을 모델링할 때는 그걸 고려하지 않기 때문에 5천원 짜리 지폐 두 장은 서로 같은 것이라고 취급한다.

아무튼 값 객체로 표현되는 대상들은 식별자 대신 안에 있는 데이터가 그 대상을 나타낸다. 따라서 지폐의 경우 그 값어치가 지폐를 구분하는 기준이 되고, 값어치가 바뀌면 다른 지폐로 인식된다. 이 말이 어려우면 이름표를 예로 들어도 된다. 이름표에 이름이 한 글자 바뀌면 다른 사람의 이름표가 되기 때문이다.

그래서 값 객체는 보통 불변 객체로 정의한다. (ex. python에선 @dataclass(frozen=True)) 그리고 내부 속성들의 모든 값들이 같으면 객체가 같다고 정의한다. (이걸 ‘값 동등성’이라 부른다.)

주의점: 값 객체가 안에 있는 데이터가 중요한 객체라고 해서 메서드를 포함해선 안된다고 말한 건 아니다. OOP 언어를 쓴다면, 값 객체와 밀접하게 연관된 연산들은 같이 묶어두는게 좋다.

엔티티

값 객체와 반대로 내부의 데이터가 아닌 객체 자체에 정체성이 있는 걸 엔티티라고 부른다. 예로 들어 위에서 이름은 글자가 바뀌면 다른 이름이 된다고 했는데, 사람은 개명을 하든 성별을 바꾸든 간에 바뀌기 전과 같은 사람이라고 인식한다. 따라서 사람은 이름과 다르게 ‘영속적인 정체성’이 있다고 할 수 있다.

위에서 든 예시처럼 값이 바뀌어도 엔티티는 같다고 인식하는 걸 ‘정체성 동등성’ 이라고 한다. Python의 경우, 정체성 동등성을 보장하기 위해 __eq__()와 __hash__()를 오버라이드할 필요가 있다.

도메인 서비스 함수

도메인 서비스 함수는 비즈니스 개념이나 프로세스를 나타내긴 하지만 값 객체나 엔티티에 포함하기엔 자연스럽지 않는 개념들을 가리킨다. 책에서 든 가구 주문 시스템 예시에선 고객이 가구를 주문한 내역인 ‘주문’과 공장에서 재고를 쌓는 ‘배치’라는 엔티티가 있는데 주문에 배치를 할당하는 프로세스는 양 쪽 어디에 메서드로 포함해도 자연스럽지 않다고 판단했다.

Python의 경우엔 OOP도 되지만 FP도 되는 다중 패러다임 언어이기 때문에 이 개념을 단순히 함수로 정의해서 해결할 수 있다. 이 말은 언어에 따라서 굳이 class로 만들지 말고 가장 적합한 방식으로 구현하라는 뜻이다.

도메인 서비스는 서비스 계층 개념과 혼동될 여지가 있다. 서비스 계층에서 서비스는 애플리케이션을 사용하는 하나의 Use case를 가리키지만, 도메인 서비스는 위에서 말했듯이 비즈니스 개념이나 프로세스를 가리킨다. 하지만 대부분 서비스라고 하면 서비스 계층을 말한다는 것도 주의하자.

예외로 도메인 개념 표현하기

도메인에서 발생할 수 있는 예외도 코드로 구현해야 한다. 당연히 Exception 클래스와 try-except 구문으로 이런 예외를 처리하겠지만, 중요한 건, 이런 예외 사항도 반드시 유비쿼터스 언어로 표현되어야 된다는 것이다.

예시로 가구 주문 시스템을 만들 때, 도메인 전문가와 대화하면서 ‘품절'(out of stock)이 발생해서 주문할 수 없는 경우에 대해 들었다면, 관련된 코드에서 처리할 예외 클래스의 이름은 OutOfStock 이 되어야 된다.

1부 마무리

그 외에 배운 것.

  1. 가장 좋은 OO 설계 원칙 적용하기: “inheritance보다 composition” 같은 원칙을 다시 생각해보기
  2. 일관성 경계나 애그리게이트에 대해서 생각해보기: 이건 7장에서 다룸.

다음 장은 데이터베이스 활용을 위한 저장소 패턴에 대한 얘기를 할 거다.

학습노트

11월 3주차 학습 노트

TCP keep alive

https://en.wikipedia.org/wiki/Keepalive

https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Keep-Alive

Keepalive는 두 장치간의 연결이 계속 유지되고 있는지 주기적으로 확인하기 위해 보내는 메세지를 말한다. keepalive를 보낸 후에 응답이 돌아오지 않는다면, 링크가 끊어졌다고 추정한다. 따라서 keepalive는 연결 확인 말곤 역할이 없기 때문에 최소한의 크기를 가져서 네트워크 대역폭을 많이 차지하지 않게 해야한다.

TCP에선 기본적으로 꺼져있고, 선택적으로 켤 수 있다. keepalive는 세 가지 매개변수를 설정할 수 있다.

  1. keepalive time: 유휴 상태일 때, keepalive를 보내는 시간 간격을 정한다.
  2. keepalive interval: keepalive 응답이 없을 시 다시 보내는 간격을 정한다.
  3. keepalive retry: 연결이 끊겼다고 확정하기 전까지 재시도하는 횟수를 정한다.

HTTP keepalive

위의 개념과 전혀 관련없다. HTTP에서의 keepalive는 추가 메세지 전달을 위해 연결을 열어둬야 할 때에 사용한다. 두 가지 매개변수를 가진다.

  1. max: 이 연결로 보낼 수 있는 최대 요청 메세지 수를 정한다.
  2. timeout: 연결 유휴 상태를 유지할 시간 제한을 정한다.
    (TCP timeout 보다 크다면, HTTP가 윗 계층인 이상 당연히 무시됨.)

Multi zone region

Available zone (가용 영역): 하나의 데이터 센터를 말함. 네트워크가 연결되어 있고 장애 대처가 가능한 전력 공급망, 서버 랙, 라우터, 스위치, 하드웨어 방화벽 등등, 온도 제어 등이 갖춰진 물리적 시설

Multi Zone Region: 물리적으론 떨어져 있지만 서로 연결된 가용 영역들의 집단. 이 각각의 가용영역들과 전부 연결된 PoP(Network Point of presence, network hotel)를 통해 외부 사용자들이 Region에 접근한다.

위와 같이 구성하는 이유는 가용성이 높은 클라우드 네이티브 아키텍쳐를 구성하기 위해서다. 여러 가용 영역에 동일한 애플리케이션을 배포해서 가용 영역 중 하나가 장애가 발생하더라도 다른 가용 영역에 접근할 수 있게 구성할 수 있다.

Fork-Join model

https://en.wikipedia.org/wiki/Fork%E2%80%93join_model

https://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html

https://www.baeldung.com/java-fork-join

병렬 컴퓨팅에서 작업을 분할정복 방식으로 처리하는 걸 Fork-Join model이라고 한다. 의사코드로 다음과 같다.

solve(problem):
    if problem is small enough:
        solve problem directly (sequential algorithm)
    else:
        for part in subdivide(problem)
            fork subtask to solve(part)
        join all subtasks spawned in previous loop
        return combined results

중요한 건 작업의 크기에 threshold를 두고 그보다 작으면 단일 쓰레드에서 처리하고, 그렇지 않으면 작업을 분할하고 모든 작업 결과를 병합할 수 있을 때까지 기다리면 된다.

work-stealing 알고리즘

https://hamait.tistory.com/612

Fork-Join 모델을 사용하는 Java의 ForkJoinPool은 쓰레드 풀에서 효율적인 작업 분배를 위해서 work-stealing 알고리즘을 사용한다. 각각의 쓰레드는 작업을 대기시키기 위한 deque를 가지고 있고 다음과 같이 동작한다.

  1. 작업 중이 아니라면 자신의 작업 deque의 head를 확인한다.
  2. 자신의 deque가 비어있다면, 다른 작업 중인 쓰레드의 deque의 tail 에서 가져오거나 전역 queue에서 가져온다.

애초에 쓰레드풀로 들어온 작업들이 전부 균등한 크기라면 ForkJoinPool이 최고의 선택은 아니다. 굳이 작업을 쪼개서 나눠줄 필요도 없고 오버헤드가 되기 때문. 하지만 만약 작업의 크기가 다르다면, 큰 작업을 받은 쓰레드가 자신의 작업을 쪼개서 deque에 넣어두면, 다른 여유가 생긴 쓰레드가 가져갈 수 있기 때문에 효율적인 분배가 가능하다. 그리고 현실에선 균등하지 않은 작업이 들어오는 경우가 많기 때문에 work-stealing 알고리즘이 유효하다.