여기서는 도메인 모델을 다시 살펴보면서 불변조건과 제약에 대해 살펴보고, 도메인 모델 객체가 개념적으로나 영속적 저장소 안에서나 내부적인 일관성을 유지하는 방법을 살펴본다. 그리고 일관성 경계를 설명하고, 일관성 경계가 어떻게 유지보수 편의를 해치지 않으면서 고성능 소프트웨어를 만들 수 있게 해 주는지 살펴본다
스프레드 시트로 처리하면 안돼?
스프레드 시트 사용하면 여러 조건들을 즉 시스템의 복잡한 제약을 유지하기 어려워 규모 확장이 힘듦.
제약, 불변조건
그래서 도메인 로직의 일부는 이런 제약을 강제로 지키게 해서 시스템의 불변조건을 유지하려는 목적으로 작성됨. 불변조건을 보니까 실용주의 프로그래머가 떠올랐다. 아무튼. 제약과 불변조건은 비슷해 보이지만 비교해 보자면 제약은 모델이 취할 수 있는 상태의 수를 제한. 불변조건은 항상 참이어야 하는 조건이라고 정의할 수 있다. 그래서 이것도 다른 책에서 본 거 같은데, 내가 해결해야 하는 문제가 있다면 제약을 잘 살펴서 어디까지 허용이 가능한지를 먼저 체크하는 게 우선이다.(이것도 실용주의 프로그래머였나?)
동시성
이러한 제약과 불변조건이. 동시성이란 아이디어를 도이하면 상황이 더 복잡해진다. 보통은 DB 테이블에 락을 적용해서 문제를 해결함. 하지만 이게 규모가 커지면 모든 배치에 라인을 할당하는 모델은 적용하기가 힘듦. 시간당 수만 건의 주문과 수십만 건의 주문 라인을 처리하려면 전체 batches 테이블의 각 행에 락을 추가할 수는 없음. 이러면 데드락 걸리거나 성능 저하생김.
그래서 애그리게이트란
주문 라인을 할당하고 싶을 때마다 데이터베이스에 락을 걸 수 없다면 어떻게 해야 할까? 시스템의 불변조건을 보호하고 싶지만 동시성을 최대한 살리고 싶다. 불변조건을 유지하려면 불가피하게 동시 쓰기를 막아야 한다. 여러 사용자에게 같은 물건을 동시에 할당하게 된다면 과혈당할 위험이 생김. 근데 다른 제품 물건들은? 동시에 할당해도 됨. 애그리게이트 패턴은 이런 긴장을 해소하기 위해 DDD 커뮤니티에서 공유하는 설계 패턴임. 애그리게이트는 다른 도메인 객체들을 포함하며 이 객체 컬렉션 전체를 한꺼번에 다룰 수 있게 해주는 도메인 객체임.
애그리게이트에 있는 객체를 변경하는 유일한 방법은 애그리게이트와 그 안의 객체 전체를 불러와서 애그리게이트 자체에 대해 메서드를 호 추한 것이다.
(내 생각)라고 하는데 이것만 보면 그냥 전체를 묶는 계층을 추가해서 처리한다는 건데 너무 느릴 거 같은 느낌이 든다. 일단 계속 보자
느린 걸 떠나서 이 구조의 핵심은 모델이 더 복잡해지고 엔티티, 값 객체가 늘어나면 누가 어떤 객체를 변경할 수 있는지 추적하기 어려워지는데, 이렇게 모든 객체를 변경할 수 있는 단일 진입점이 있으면 시스템이 개념적으로 더 간단해지고 추론하기가 쉬워짐.
책에서는 쇼핑몰의 장바구니를 예시를듬. 즉 애그리게이트가 동시성 경계가 됨.
애그리게이트에는 원소에 대한 접근을 캡슐화한 루트 엔티티가 있음(장바구니) 원소마다 고유한 정체성이 있지만, 시스템의 나머지 부분은 장바구니를 나눌 수 없는 단일 객체처럼 참조해야 함.
(내 생각) 그니까 의도적으로 애그리게이트로 묶는 게 아니라 서비스 내에서 동시성 문제가 발생할만한 곳 그리고 묶일 만한 부분을 자연스럽게 애그리게이트로 설계하는 게 맞음. 나는 그냥 모든 구조를 저런 식으로 한다고 보니까 굳이 무거워질 거 같다는 생각이 듦.
그래서 경계를 잘 두는 게 중요한 포인트라고 책에서 나옴
그래서 책에서 결국 Product라는 애그리게이트에 batch를 할당하는데 이렇게 보니까 뭔가 당연했던 거처럼 느껴지기도 하고 batch부터 있던 게 어색하기도 하다. 애초에 설계를 할 때 Product부터 만들 거 같긴 한데, 이런 모든 사정을 생각해서 만들진 못했을 거 같다.
제한된 콘텍스트
자세하게는 길어서 나오진 않고 잠깐 나오는데 굉장히 흥미로웠고, 여러 역할을 하는 모델을 다루는 방식이다.
그니까 전체 비즈니스를 한 모델에 넣는 게 아니라 콘텍스트 마다 다르게 경계를 설정하고 콘텍스트를 변환할때 명시적으로 변환하는게 낫다고 함. 예전에 이렇게 구현된 유사한 코드를 봤을때 아무 생각 없었는데 지금 생각해보니까. 이유를 알겠더라. 아무튼 이런 개념이 마이크로서비스의 세계로 잘 변환될 수 있다고 함.
그래서 애그리게이트를 선택할 때는 어떤 제한된 컨텍스트 안에서 애그리게이트를 실행할지 선택하는 것이 중요하다고 한다. 컨텍스트를 제약하면 관리하기 좋은 크기로 유지할 수 있다고 한다.
성능은 어떨까?
내가 궁금해했던 부분이 여기서 나온다.
첫 번째. 의도적으로 데이터베이스에서 읽기 질의를 한 번만 하고, 변경된 부분을 한 번만 영속화하여 데이터를 모델링하는 중임. 이런 식으로 처리하는 시스템은 여러 번 다양한 질의를 던지는 시스템보다 훨씬 더 성능이 좋은 경향이 있다. 이런 방식을 택하지 않는 시스템에서는 소프트웨어가 진화함에 따라 트랜잭션이 점차 느려지고 더 복잡해지는 경향이 있음.
둘째. 데이터 구조를 최소한으로 사용하며 한 행당 소수의 문자열과 정수만 만듦. 이렇게 하면 몇 밀리초 만에 몇 심 개에서 심지어는 백여 개의 배치를 메모리로 읽어올 수 있음.
셋째 어느 시점에서는 상품마다 20개 정도의 배치만 있으리라 예상됨. 배치를 모두 사용하고 나면 이 배치를 계산에서 배제할 수 있음. 즉 , 시간이 지나도 가져오는 데이터 양은 제어를 벗어나지 않음
만약에 상품당 몇천 개의 배치가 있으리라 예상한다면, 몇 가지 다른 방식을 사용할 수 있다. 한 가지 방법은 상품의 배치를 지연 읽기 하는 것임. 코드 관점에서 보면 바뀌는 부분이 없음. 하지만 ORM에서 사용자 대신 데이터를 페이지 단위로 읽어옴 근데 이렇게 하면 적은 수의 행을 가져오는 데이터베이스 요청이 더 많아짐. 하지만 주문을 처리하기 적합한 배치를 단 하나만 찾으면 되므로 이렇게 점진적으로 가져오는 게 잘 동작함.
또한 초기 설정한 애그리게이트가 맞지 않는다면 다른 애그리게이트를 살펴보면서 맞는 걸 찾아야 함. 언제든 설정한 경계가 성능을 저하시킨다면 바꿔서 설계해야 함
구체적으로 비교한 부분 내가 헷갈려서
1. "Product"를 사용하지 않는 경우:
- "Batch" 테이블에 직접적인 락이 발생할 수 있습니다. 이는 "Batch" 데이터에 대한 동시 접근이 많은 경우 성능의 병목이 됨
- 동시에 여러 트랜잭션이 "Batch" 테이블을 액세스 하려 할 때, 원하는 성능을 얻기 어렵.
2. "Product" 어그리게이트를 사용하는 경우:
- "Product" 단위로 락을 관리하면, 동일한 "Product"에 대한 동시 요청에만 락이 적용됩니다. 다른 "Product"에 대한 요청은 동시에 처리될 수 있음.
- 이 방식은 동일한 "Product"에 대한 동시 접근이 적고, 다양한 "Product"에 대한 동시 접근이 많은 시나리오에서 더 효율적임.
- "Product" 어그리게이트 내에서 "Batch"를 관리하면, 해당 "Product"에 대한 작업이 완료되기 전까지는 다른 작업이 대기하게 되어 동시성이 보장됨.
버전번호와 낙관적 동시성
이제는 데이터베이스 수준에서 데이터 일관성을 강제할 수 있는 방법에 대해 조금 더 살펴보자
낙관적 동시성은 충돌하는 경우가 드물다고 가정하고 제어하는 방식임. 이 방식에서는 충돌이 났을 때 어떻게 처리할지를 명시적으로 해야 함. 비관적은 애초에 충돌 발생 못하게 뭐 테이블 전체에 락을 걸던가. write lock을 쓰는 거 같다. 근데 이 부분은 뒤에서도 나오는데 좀 데이터베이스 고립레벨 문제랑도 엮여 있어서 생각난 김에 정리를 다시 해야겠다. 일단은 여기 책에서 나온 버전 번호 구현을 이어서 보자
여기서 제시한 낙관적 동시성 제어 방식
낙관적 제어 방식으로 버전번호를 product에 줘서 각 프로세스가 진행될 때 그 버전 변수를 통해 구분가능하기 하는 거임 근데 이걸 어디서 업데이트해주냐 차이가 있음. 결론적으로 말하면 Product allocate 함수내부에서 업데이트하는데 나머지 두 방식은 서비스계층에서 하거나, UoW에서 하는 방식이 있다. 근데 UoW에서 하는 방식은 다른 repository가 쓰이거나 (예를 들면 csv ) 모든 제품이 변경 됐다고 가정하지 않고서는 구현할 방법이 없어서 탈락, 서비스계층에서 해주는 건 도메인이랑 서비스 계층 로직 중간에 하기 때문에 지저분해 보여서 탈락.
데이터 베이스 수준에서 동시성 지키도록 강제 + select for update
(거의 내 생각)둘다 db수준에서 처리하는 거여서 orm사용할 때 처리를 하는 거임 write lock 같은 경우에는 한 트랜잭션이 쓰면 그냥 접근을 못함. 근데 원래 write 락만 쓴다고 트랜잭션 제어가 완벽해지는 게 아님. 간단한 로직은 설명이 되는데 복잡해지면 락 잘 걸어도 값이 달라지는 경우도 생기고 데드락 걸리는 경우도 많고 일단 느려짐. 그래서 mvcc를 사용하는데 이건 또 길어지니까. 나중에 따로 정리해야겠다.
그리고 책에서도 방식에 대한 이야기만 나와있다. 여기서는 애그리게이트로 어떻게 동시성을 제어하는지에 중점을 두고 기억해야겠다.
마지막 정리
애그리게이트는 모델의 일부 부분집합에 대한 주 진입점 역할을 하고 모든 모델 객체에 대한 비즈니스 규칙과 불변조건을 강제하는 역할을 담당하도록 객체를 명시적으로 모델링함. 올바른 애그리게이트를 선택하는 것이 핵심.(내 생각) 그니까 모델 설계를 할 때 일관성 경계를 염두에 둬야겠다.
현실은 그렇다
마지막에 저자가 쓴 글이 있는데 그 부분을 그대로 가져오지 못해서 아쉽지만, 아무튼 뭐 지금까지 작업이 분명히 코스트가 드는 작업이라는걸 강조하고 우리들의 앱이 만약 단순히 db를 감싸는 CRUD 래퍼이고 미래에 별일 없으면 그냥 하지말고 장고 써라 뭐 이런식으로 얘기하는데 맞는말이다. ㅎㅎ
그래도 공부할 때는 지금까지의 내용을 지키면서 했으면 참 좋았겠다는 생각이 든다. 나도 역시 저번 프로젝트는 CRUD래퍼에 그쳤고 최대한 수정이 있을 때마다 구조를 바꿔 보려고 했지만 급한 마음에 넘어가고 일단 기능 추가하고 하는 거 같다. 끊임없이 노력해야겠다.
참고 및 출처: <파이썬으로 살펴보는 아키텍처 패턴: TDD, DDD, EDM 적용하기)> (해리 퍼시벌, 밥 그레고리 지음, 오현석 옮김, 한빛미디어 , 2021)
'Book' 카테고리의 다른 글
메시지 버스로 통합하기 (2) | 2023.10.22 |
---|---|
오브젝트. Chapter 01 객체,설계 (0) | 2023.10.18 |
작업 단위 패턴 (0) | 2023.10.05 |
MySQL 퍼포먼스 최적화 (1~3장) (0) | 2023.08.17 |
실용주의 프로그래머(7장 ~마지막) (0) | 2023.07.31 |