[StoreOps] 2. POS/PDA/고객 앱/관리자 WEB을 잇는 도메인 설계
StoreOps Platform 시리즈
- 1편: 프로젝트 배경
- 2편: 도메인 설계
- 3편: 재고 트랜잭션
- 4편: 대규모 Inventory 확장
- 전체 글 목록
1. 설계를 시작하며
이전 글에서는 편의점 운영 시스템를 분석하고, 그에 맞춰 StoreOps Platform이라는 프로젝트를 기획한 이유를 정리했다.
이번 글에서는 본격적인 구현에 들어가기 전, 편의점 운영 흐름을 어떻게 도메인 모델로 바꿨는지 정리하려고 한다.
처음에는 단순히 다음과 같은 API를 만들면 된다고 생각했다.
상품 등록 API
점포 등록 API
재고 조회 API
판매 처리 API
입고 처리 API
하지만 이렇게 API 목록부터 나열하면 프로젝트가 단순 CRUD에 가까워질 수 있다. 내가 이 프로젝트에서 보여주고 싶은 것은 API 개수 자체가 아니라, 편의점 운영에서 어떤 데이터가 중심이 되고, 여러 시스템이 그 데이터를 어떻게 공유하는지 이해했다는 점이었다.
그래서 먼저 편의점 운영 흐름을 단순화하고, 그 흐름을 도메인 모델로 옮기는 방식으로 설계를 시작했다.
이번 프로젝트에서 가장 중요하게 본 질문은 다음과 같았다.
POS에서 상품이 팔리면 어떤 데이터가 바뀌어야 할까?
PDA로 입고가 처리되면 어떤 데이터가 증가해야 할까?
고객이 앱에서 보는 재고는 어디에서 조회되어야 할까?
점주나 관리자는 어떤 데이터를 보고 발주를 판단할까?
이 질문들을 따라가다 보니, 결국 프로젝트의 중심에는 Inventory, 즉 점포별 상품 재고가 있어야 한다고 판단했다.
2. 편의점 운영 흐름 단순화하기
실제 편의점 운영은 매우 복잡하다. 상품 발주, 물류센터, 입고, 검수, 진열, 판매, 폐기, 정산, 회계, 프로모션, 멤버십, 모바일 앱, 배달/픽업 등 다양한 업무가 연결되어 있다.
하지만 학습용 미니 프로젝트에서 모든 흐름을 구현할 수는 없다. 따라서 이번 프로젝트에서는 편의점 운영과 가장 직접적으로 연결될 수 있는 흐름만 추렸다.
내가 단순화한 편의점 운영 흐름은 다음과 같다.
상품 등록
→ 점포 등록
→ 점포별 재고 등록
→ PDA 입고 처리
→ POS 판매 처리
→ 고객 재고조회
→ 관리자 재고 확인
→ 결품 위험 판단
→ 발주 추천
이 흐름을 프로젝트 관점에서 다시 표현하면 다음과 같다.
PDA 입고 → Inventory 증가
POS 판매 → Inventory 감소
고객 앱 → Inventory 조회
관리자 WEB → Inventory 조회 + Sale 이력 기반 분석
발주 추천 → Inventory + Sale 이력 기반 계산
이 구조에서 중요한 점은 POS, PDA, 고객 앱, 관리자 WEB이 서로 다른 클라이언트지만, 결국 모두 Inventory라는 같은 데이터를 중심으로 동작한다는 것이다.
3. 핵심 도메인 도출하기
사용자와 운영 흐름을 정리한 뒤, 핵심 도메인을 도출했다.
이번 프로젝트의 주요 도메인은 다음 여섯 가지다.
Store
Product
Inventory
Sale
Receipt
OrderRecommendation
각 도메인의 역할은 다음과 같다.
| 도메인 | 의미 | 주요 역할 |
|---|---|---|
| Store | 점포 | 점포명, 주소, 운영 상태 관리 |
| Product | 상품 | 상품명, 카테고리, 가격, 판매 상태 관리 |
| Inventory | 점포별 상품 재고 | 현재 재고 수량 관리, 재고 증가/감소 |
| Sale | POS 판매 이력 | 판매 수량, 판매 일시 기록 |
| Receipt | PDA 입고 이력 | 입고 수량, 입고 일시 기록 |
| OrderRecommendation | 발주 추천 결과 | 현재 재고, 평균 판매량, 추천 발주 수량 기록 |
이 중 가장 중요한 도메인은 Inventory다.
Store와 Product는 기본 정보에 가깝다. Sale과 Receipt는 재고가 변화한 이유를 기록하는 이력 데이터다. OrderRecommendation은 Inventory와 Sale 데이터를 바탕으로 계산되는 결과다.
반면 Inventory는 고객 재고조회, POS 판매, PDA 입고, 관리자 조회, 발주 추천이 모두 의존하는 중심 데이터다.
그래서 이번 프로젝트의 도메인 모델은 Inventory를 중심으로 구성했다.
4. 현재 재고와 이력 데이터를 함께 둔 이유
재고 시스템을 설계하면서 또 하나 고민한 부분은 “현재 재고만 저장할 것인가, 판매/입고 이력도 함께 저장할 것인가”였다.
선택지는 다음과 같았다.
1. Inventory에 현재 재고만 저장한다.
2. Inventory와 Sale 이력만 저장한다.
3. Inventory, Sale, Receipt 이력을 모두 저장한다.
4. 모든 재고 변동을 StockMovement라는 하나의 이력 테이블로 관리한다.
가장 단순한 방식은 1번이다. Inventory에 현재 재고 수량만 저장하면 고객 재고조회와 관리자 재고조회는 구현할 수 있다.
하지만 이 방식은 재고가 왜 변했는지 알기 어렵다.
예를 들어 재고가 30개에서 8개로 줄었다면, 그것이 실제 판매 때문인지, 데이터 오류 때문인지, 폐기 때문인지 추적하기 어렵다.
그래서 이번 프로젝트에서는 3번을 선택했다.
Inventory → 현재 재고 상태
Sale → POS 판매 이력
Receipt → PDA 입고 이력
이렇게 나누면 현재 재고를 빠르게 조회할 수 있고, 동시에 재고가 변한 이유도 추적할 수 있다.
완전한 재고 원장 방식으로 가려면 StockMovement 같은 통합 이력 모델을 두는 것이 더 적절할 수 있다. 예를 들어 판매, 입고, 폐기, 조정 등 모든 변동을 하나의 테이블에 기록하는 방식이다.
하지만 이번 프로젝트의 MVP 범위에서는 POS 판매와 PDA 입고가 핵심이므로, Sale과 Receipt를 분리해 명확하게 표현하는 방식을 선택했다.
이 결정은 개발운영 관점에서도 의미가 있다. 운영 중 재고가 예상과 다르게 변했을 때, 판매 이력과 입고 이력을 통해 원인을 추적할 수 있기 때문이다.
5. 도메인 관계 설계
이번 프로젝트의 기본 관계는 다음과 같다.
Store 1 ─ N Inventory
Product 1 ─ N Inventory
Store 1 ─ N Sale
Product 1 ─ N Sale
Store 1 ─ N Receipt
Product 1 ─ N Receipt
Store 1 ─ N OrderRecommendation
Product 1 ─ N OrderRecommendation
하나의 점포는 여러 상품 재고를 가질 수 있고, 하나의 상품은 여러 점포에 존재할 수 있다.
예를 들어 참치마요 삼각김밥이라는 상품은 한양대점, 왕십리점, 서울숲점에 모두 존재할 수 있다. 하지만 각 점포의 재고 수량은 다르다.
그래서 Store와 Product 사이의 관계를 Inventory라는 도메인으로 풀었다.
여기서 중요한 규칙이 하나 있다.
하나의 점포에는 같은 상품의 재고가 중복으로 존재하면 안 된다.
즉, store_id와 product_id의 조합은 유일해야 한다.
그래서 Inventory 테이블에는 다음과 같은 유니크 제약을 두었다.
unique(store_id, product_id)
이 제약을 두면 한양대점 + 참치마요 삼각김밥 재고가 중복으로 생성되는 문제를 막을 수 있다.
6. 도메인 규칙 정리
도메인을 설계하면서 함께 정리한 핵심 규칙은 다음과 같다.
6.1 Inventory 규칙
재고 수량은 0 이상이어야 한다.
판매 시 재고는 판매 수량만큼 감소한다.
입고 시 재고는 입고 수량만큼 증가한다.
판매 수량이 현재 재고보다 크면 판매할 수 없다.
같은 점포와 같은 상품의 재고는 중복 생성될 수 없다.
이 규칙은 Inventory 도메인 안에서 최대한 관리하려고 했다.
예를 들어 재고 감소는 단순히 Service에서 quantity 값을 직접 빼는 것이 아니라, Inventory.decrease() 같은 메서드로 처리하도록 설계했다.
public void decrease(int quantity) {
validatePositiveQuantity(quantity);
if (this.quantity < quantity) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK);
}
this.quantity -= quantity;
}
이렇게 하면 재고 음수 방지라는 규칙이 여러 Service에 흩어지지 않고 Inventory 안에 모일 수 있다.
6.2 Sale 규칙
판매 수량은 1 이상이어야 한다.
판매 처리는 반드시 재고 차감과 함께 이루어져야 한다.
판매 이력은 재고가 줄어든 이유를 추적하기 위해 저장한다.
POS 판매 처리에서는 판매 이력 저장과 재고 차감이 하나의 트랜잭션으로 처리되어야 한다. 둘 중 하나만 성공하면 운영 중 재고 정합성이 깨질 수 있기 때문이다.
6.3 Receipt 규칙
입고 수량은 1 이상이어야 한다.
입고 처리는 반드시 재고 증가와 함께 이루어져야 한다.
입고 이력은 재고가 늘어난 이유를 추적하기 위해 저장한다.
PDA 입고도 POS 판매와 마찬가지로 트랜잭션 처리가 중요하다.
입고 이력만 저장되고 재고가 증가하지 않거나, 재고만 증가하고 입고 이력이 남지 않으면 운영 중 원인을 추적하기 어려워질 수 있다.
6.4 OrderRecommendation 규칙
발주 추천은 최근 판매량과 현재 재고를 기반으로 계산한다.
MVP에서는 다음과 같은 단순 규칙을 사용했다.
최근 7일 평균 판매량 = avgDailySales
목표 재고 = avgDailySales * 3
안전 재고 = avgDailySales * 2
추천 발주량 = max(0, 목표 재고 + 안전 재고 - 현재 재고)
처음부터 AI 기반 수요 예측을 적용하지 않은 이유는, 이번 프로젝트의 목표가 예측 모델의 정확도 자체가 아니기 때문이다.
이 프로젝트에서 보여주고 싶은 것은 POS 판매 이력이 현재 재고와 결합되어 발주 의사결정으로 이어지는 운영 흐름이다. 따라서 설명 가능하고 테스트하기 쉬운 규칙 기반 로직을 우선 적용했다.
7. 대규모 운영 시스템으로 확장한다면
이번 프로젝트는 학습용 미니 프로젝트이기 때문에 범위를 작게 잡았다. 하지만 실제 대규모 운영 시스템이라면 추가로 고민해야 할 지점이 많다.
예를 들어 재고 변동을 Sale과 Receipt로만 나누는 방식은 MVP에서는 명확하지만, 실제 운영에서는 폐기, 조정, 반품, 이동, 행사 재고 등 더 다양한 변동 사유가 생길 수 있다.
이 경우에는 StockMovement 같은 재고 원장 모델을 두고, 모든 재고 변동을 하나의 기준으로 기록하는 방식이 더 적절할 수 있다.
또한 POS 판매가 동시에 많이 발생하는 환경에서는 동시성 제어도 필요하다. 같은 상품의 재고가 1개 남았는데 두 POS에서 동시에 판매 요청이 들어오는 상황을 고려해야 하기 때문이다.
이번 프로젝트에서는 우선순위를 고려해 재고 원장과 동시성 제어까지 완성 구현하지는 않았다. 대신 현재 범위 안에서 재고 정합성, 이력 추적, 도메인 규칙을 명확히 잡는 데 집중했다.
8. 정리
이번 글에서는 StoreOps Platform의 도메인 설계 과정을 정리했다.
핵심은 다음과 같다.
편의점 운영 흐름을 Inventory 중심으로 단순화했다.
현재 재고는 Inventory에 저장하고, 변화 이유는 Sale과 Receipt 이력으로 남겼다.
Store와 Product의 관계는 Inventory로 연결했다.
재고 음수 방지와 재고 증감 규칙은 Inventory 도메인 안에 두었다.
발주 추천은 Inventory와 Sale 이력을 기반으로 계산하도록 설계했다.
다음 글에서는 이 설계를 바탕으로 POS 판매와 PDA 입고를 구현하면서, 재고 데이터 정합성을 어떻게 지키려고 했는지 정리할 예정이다.
시리즈 이어보기
이전 글: 1. 편의점 운영 시스템 프로젝트를 시작한 이유 | 전체 글 목록 | 다음 글: 3. 재고 정합성을 위한 POS 판매·PDA 입고 트랜잭션 구현