티스토리 뷰

이벤트스토밍으로 커머스 서비스를 모델링했습니다. 주문·상품·브랜드·포인트·좋아요에 관한 요구사항을 읽고, 이벤트와 커맨드를 카드로 정리하는 작업이었는데요.
하다 보니 어느 순간부터 어떤 색의 카드를 어디에 붙일지에 더 신경 쓰고 있었습니다. 카드는 채워지는데, 막상 이런 질문에는 답하기 어려웠고요.
“포인트는 주문할 때 사용하는데, 주문 안에 넣으면 안 될까?”
“좋아요는 상품에 달리는 건데, 상품이 가지고 있으면 되는 것 아닐까?”
객체를 나누기는 했지만 왜 나눴는지는 설명하지 못했습니다. 그래서 구현을 다시 보며, 경계를 정한 이유부터 찾아보기로 했습니다.
관련이 있다고 묶을 수는 없었습니다
처음에는 관련된 객체를 함께 묶으면 된다고 생각했습니다. 상품은 브랜드에 속하고, 좋아요는 상품에 달리며, 포인트는 주문할 때 사용하니까요. 하지만 관계를 따라가면 모두 연결돼 있습니다. 관련이 있다는 사실만으로는 어디까지 묶어야 할지 정하기 어려웠습니다.
이 고민을 하다가 Martin Fowler의 DDD Aggregate 설명을 읽었습니다. 주문과 주문 품목은 서로 다른 객체지만 하나의 애그리게이트로 관리할 수 있다는 내용이었습니다. 클래스를 나누는 것과 여러 객체를 하나의 단위로 관리하는 것은 다른 문제였습니다.
예를 들어 주문에는 최소 한 개의 품목이 있어야 합니다. 품목이 하나만 남았다면 이를 제거할 수 없어야 하죠.
주문이 품목 변경을 관리해야 이 조건을 지킬 수 있습니다.
이처럼 상태가 바뀌어도 유지해야 하는 조건을 불변식이라고 합니다.
관계보다 먼저 살펴볼 것은 어떤 규칙을 지켜야 하고, 그 규칙에 필요한 상태를 누가 가지고 있는지였습니다.
포인트는 누구의 상태일까?
포인트를 차감한 뒤에도 잔액은 0 이상이어야 합니다. 주문이 현재 잔액과 사용할 금액을 비교해도 이 규칙은 지킬 수 있습니다.
그런데 같은 사용자가 여러 번 주문하면 문제가 생깁니다.
잔액이 10,000원인 사용자가 주문 A에서 7,000원을 사용했다면, 주문 B는 남은 3,000원을 기준으로 판단해야 합니다.
잔액은 특정 주문의 상태가 아니라 여러 주문이 함께 사용하는 상태였습니다.
포인트는 주문 없이 충전할 수도 있습니다. 반면 주문 A에서 7,000원을 사용했다는 기록은 이후에 충전하더라도 바뀌지 않아야 합니다.
여기서 두 정보를 구분했습니다.
- 사용자의 현재 잔액
- 특정 주문에서 사용한 금액
현재 구현에서는 PointBalance가 잔액과 충전·차감을 맡고, 주문에는 해당 주문에서 사용한 금액을 남겼습니다.
잔액이 음수가 되면 안 된다는 조건만으로 분리한 것은 아닙니다.
잔액이 여러 주문에서 공유되고 주문 없이도 바뀐다는 점을 함께 봤을 때, 특정 주문이 소유할 상태는 아니라고 판단했습니다.
좋아요는 상품의 일부일까?
좋아요는 같은 사용자와 상품의 조합이 중복되면 안 됩니다.
상품이 좋아요 목록을 가지고 중복을 검사하는 방법도 가능합니다.
따라서 중복 금지 규칙만으로 좋아요를 분리해야 한다고 말할 수는 없었습니다.
이번 요구사항에서 변경하는 대상은 사용자와 상품 사이의 관계 하나였습니다. 다른 사용자의 좋아요나 상품의 가격·재고를 함께 바꿀 필요는 없었습니다. 상품에 달린 모든 좋아요를 한 번에 조정해야 하는 규칙도 없었고요.
등록과 취소의 조건도 달랐습니다. 삭제된 상품에는 새로운 좋아요를 남길 수 없지만, 이미 남긴 좋아요는 상품이 삭제된 뒤에도 취소할 수 있어야 했습니다.
그래서 ProductLike를 별도의 관계로 관리하고, 등록할 때만 상품 상태를 확인하도록 했습니다. 같은 관계가 중복 저장되지 않도록 사용자와 상품 조합에는 DB 유일 제약도 두었습니다.
상품 안에 두는 방식이 틀렸다는 뜻은 아닙니다. 이번 요구사항에서는 상품 전체보다 개별 관계를 변경 단위로 보는 편이 자연스러웠습니다.
나눈 뒤에도 함께 처리할 일은 남았습니다
주문을 확정할 때는 재고와 포인트를 차감하고 주문 상태를 변경해야 합니다.
각 객체가 자신의 규칙을 지키더라도, 포인트 차감에 실패한 뒤 재고만 줄어든 상태가 남아서는 안 됩니다.
세 변경은 함께 성공하거나 실패해야 합니다.
현재 구현에서는 ConfirmOrderFacade가 이 과정을 하나의 DB 트랜잭션으로 처리합니다.
각 객체의 책임은 나눴지만, 주문 확정에서는 여러 애그리게이트를 하나의 트랜잭션 안에서 변경합니다.
경계를 나눈다고 협력까지 사라지는 것은 아니었습니다. 오히려 나눈 객체를 어디에서, 어떤 기준으로 함께 처리할지도 정해야 했습니다.
마무리
처음에는 객체 사이의 관계를 따라 경계를 나누려고 했습니다. 포인트는 주문할 때 사용하고, 좋아요는 상품에 달리니 각각 주문과 상품에 두면 된다고 생각했습니다.
하지만 관계만으로 나누고 나니 왜 그렇게 설계했는지 설명하기 어려웠습니다. 클래스를 나눈 것과 여러 객체를 하나의 단위로 관리하는 것도 제대로 구분하지 못했고요.
애그리게이트를 살펴보면서 관계보다 먼저 확인해야 할 것이 있다는 걸 알게 됐습니다.
변경 후에도 지켜야 하는 규칙은 무엇일까?
그 규칙을 확인하려면 어떤 상태가 필요할까?
그 상태는 누가 소유하고 변경해야 할까?
이 질문을 요구사항에 대입하면서 포인트와 좋아요를 분리한 이유도 조금씩 설명할 수 있었습니다.
지금의 설계가 정답인지는 모르겠습니다. 요구사항이 달라지면 경계도 달라질 수 있습니다.
- Total
- Today
- Yesterday
- java
- 프로토콜
- 자바
- 개발
- 3Way Handshake
- tcp
- Spring Boot
- dto
- 프로그래머스
- aws
- osi7계층
- 회고
- spring
- 계층
- rds
- 네트워크
- 삽질
- ec2
- 스위치
- 회고록
- lambda
- SpringBoot
- 요금
- 초보
- 라우터
- Docker
- 스프링
- 라우팅
- s3
- 개발자
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |