YAPS Pet Store

← 쇼핑몰로 돌아가기

Pet Store - 업무를 DSL 로

YAPS Pet Store 는 애완동물을 파는 온라인 가게입니다 (Java EE 7 pet store 를 ElastiCORE 3.0 으로 다시 만든 것). 손님은 분류(물고기·개·파충류·고양이·새) → 상품(불독·푸들…) → 품목(수컷 강아지·암컷 강아지처럼 실제로 사는 단위) 순으로 둘러보고 장바구니에 담습니다. 결제하려면 회원 가입·로그인하고, 배송지와 카드를 넣으면 주문이 접수됩니다. 금액은 품목 가격 합계에 VAT 5.5% 를 더하고, VAT 포함 금액에서 2.25% 할인합니다. 손님은 자기 주문을 보고 취소할 수 있고(배송 전까지), 가게 관리자는 백오피스에서 주문을 배송 처리하고 상품을 관리합니다.

이 문서는 그 업무를 업무 단위로 나눠, 각 규칙이 DSL 의 어디에 적혔는지, 무엇이 생성되고 무엇을 손으로 썼는지, 그리고 DSL 로 아직 표현할 수 없는 것까지 적습니다. "확인한 결과" 열은 실행 중인 애플리케이션에 HTTP 로 요청해서 본 응답입니다. DSL 과 생성 코드 발췌는 빌드 결과물에서 그대로 읽어 왔습니다.

149DSL 줄 (4개 파일)
89 · 8,316생성된 Java 파일 · 줄
34 · 1,144생성된 TypeScript 파일 · 줄
7 · 256손으로 쓴 Java 파일 · 줄

사람과 권한

방문자

로그인 전. 상품을 보고 검색하고, 가입합니다.

고객 (USER)

가입하면 이 역할. 자기 정보와 자기 주문만 보고 고칩니다.

관리자 (ADMIN)

모든 것을 봅니다. 배송, 상품 관리, 고객 역할 변경, 삭제.

DSL 은 엔티티마다 @access(연산=수준) 으로 말합니다. 수준은 public(방문자도) · user(로그인) · owner(본인 것만, @owner(필드) 로 주인을 정함) · admin. 적지 않은 연산은 관리자만 할 수 있습니다.

할 일방문자고객 (본인 것)고객 (남의 것)관리자DSL
상품 보기·검색200200-200read=public
상품 만들기·고치기·지우기401403-200(적지 않음 → 관리자)
회원 가입201---create=public
고객 정보 보기401200404200read=owner
고객 정보 고치기401200404200update=owner
고객 역할 바꾸기-무시됨-200role … @access(admin)
탈퇴(고객 삭제)401403403200(적지 않음 → 관리자)
주문하기401200-200create=user
주문 보기401200404200read=owner
주문 취소401200404200cancel=owner
배송401403403200(적지 않음 → 관리자)
주문 삭제401403403200(적지 않음 → 관리자)

남의 것은 403 이 아니라 404 입니다 - 남의 주문이 있다는 사실도 알리지 않습니다. 검사는 재생성되는 서비스(…CoreService)에 들어 있어서, 어느 진입점으로 와도 같습니다. 요청 본문 형식 검사(400)가 먼저, 그다음 로그인(401)·권한(403/404) 순입니다.

업무 지도

업무누가DSL 로 표현손으로 쓴 것 / DSL 밖
1. 상품 둘러보기누구나DSL 로 전부-
2. 상품 관리관리자대부분 DSL"품목이 있는 상품은 못 지움" 은 DB 외래 키로만. 관리 화면 없음
3. 회원 가입방문자대부분 DSL가입 후 바로 로그인(세션) · 비밀번호 최소 길이는 넣지 않음 (데모 계정)
4. 로그인·로그아웃누구나DSL + 손으로로그인 API 3개(세션)는 손으로. 비밀번호 확인은 생성
5. 내 정보고객DSL 로 전부(현재 비밀번호 확인 없이 비밀번호 변경)
6. 장바구니누구나DSL 밖브라우저에만 있음 (의도)
7. 주문(결제)고객대부분 DSL가격 정책(VAT·할인)·기본 배송지·주문일 · 결제 승인·재고는 모델에 없음
8. 주문 조회고객·관리자DSL 로 전부-
9. 주문 취소고객DSL 로 전부환불은 모델에 없음
10. 배송관리자DSL 로 전부-

1. 상품 둘러보기 DSL 로 전부

손님은 로그인 없이 분류 → 상품 → 품목 순으로 둘러보고, 이름으로 품목을 찾습니다. 품목 카드에는 상품 이름과 가격이 함께 나옵니다.

업무 규칙DSL확인한 결과
누구나 본다@access(read=public)방문자 검색·조회 200
분류 → 상품 → 품목category: Category @ManyToOne
product: Product @ManyToOne
{"categoryId":1001} 로 개(Dogs) 상품만
이름 일부로 찾기name: string(30)! @search(like){"name":"%Puppy%"} → 2건 (화면이 % 를 붙여 보냄)
품목에 상품 이름도dto.yml productName: string @ref(product.name)응답에 productName
가격은 금액unitCost: money(10.2)!JSON 숫자 10.00, Java Money
배송 국가 목록Country … @access(read=public) @expose(300)결제 화면의 국가 선택
잘못된 검색은 요청 잘못(생성)없는 정렬 컬럼 → 400 "No property 'nope' found for type 'Item'"

      

다른 애그리거트는 객체가 아니라 식별자로 참조합니다(productId). env.yml 의 referenceColumn: short 로 컬럼 이름이 product_id 가 됩니다.

2. 상품 관리 대부분 DSL

관리자가 분류·상품·품목·국가를 등록·수정·삭제하고 가격을 바꿉니다.

업무 규칙DSL확인한 결과
관리자만 바꾼다@access(read=public) 에 create·update·delete 를 적지 않음방문자 401, 고객 403, 관리자 200
가격은 0 보다 크다@min(0.01, 'price must be positive')0 → 400 "unitCost: price must be positive" (화면 검증 함수에도 같은 문구)
값을 바꿔도 지난 주문은 그대로주문 줄 unitCost … @snapshot(item.unitCost)10.00 → 11.50 으로 바꾼 뒤에도 기존 주문 줄은 10.00
품목이 있는 상품은 못 지운다DSL 에 규칙 없음 - DB 외래 키409 "The row is still referenced by another…"
DSL 밖: "품목이 남아 있으면 삭제할 수 없다" 는 업무 규칙이지만 DSL 에 쓸 곳이 없어서 DB 외래 키 오류(409)로만 지켜집니다. 관리 화면은 만들지 않았습니다 - Swagger 에서 관리자로 로그인한 세션으로 API 를 부릅니다.

3. 회원 가입 대부분 DSL

방문자가 로그인 ID·비밀번호·이름·집 주소(필수)·연락처로 가입하면 일반 고객이 되고 바로 로그인됩니다.

업무 규칙DSL확인한 결과
누구나 가입@access(create=public)201
로그인 ID 는 하나뿐@Index(name="uk_ps_customer_login", columnList="login", unique="true")같은 ID → 409 "A unique value is already taken."
ID 10자 이하, 이름 필수login: string(10)!, firstName: string(50)!11자 이상 → 400
이메일 형식, 생일은 과거@email, @past400 "Email format invalid", "Date must be past"
집 주소 필수homeAddress: Address! @Embedded + Address 의 street1: string(50)! …주소 없음 → 400 "homeAddress: …"
비밀번호는 해시로만, 돌려주지 않음password: string(255)! @password응답에 password 없음, DB 에는 PBKDF2 해시
스스로 관리자가 될 수 없다role: UserRole @default(UserRole.USER) @access(admin)"role":"A" 로 보내도 U
가입하면 바로 로그인손으로 - AccountController.signUp가입 응답의 세션으로 /customer/me 200

      

생성: 가입·수정 때 역할 보호와 해시

손으로: 가입하고 바로 로그인 (세션 시작)

이 샘플에 없는 것: 비밀번호 최소 길이. password: string(255)! @password @length(min=8) 이면 보낸 비밀번호를 해시하기 전에 검사해 7자는 400 입니다(DTO 의 @Size(min=8, max=255)). 이 샘플은 원본의 데모 계정 user/user 를 쓰므로 넣지 않았습니다. 복잡도(문자 종류)는 @pattern 으로 씁니다.

4. 로그인·로그아웃 DSL 로 전부

고객은 로그인 ID 와 비밀번호로 로그인하고, 로그아웃합니다. 화면은 접속할 때 "지금 누구인지" 를 묻습니다.

업무 규칙DSL확인한 결과
ID 와 비밀번호가 맞아야login … @login + password … @password → 생성 authenticate(login, password)틀린 비밀번호·없는 ID 모두 401 (같은 시간이 걸리게 - 어느 쪽인지 알리지 않음)
로그인 유지엔티티 줄의 @signin → 생성 POST /customer/signin: 세션에 고객 ID 와 역할(role ... @access(admin) 필드)을 넣음200, 이후 요청은 그 고객으로
로그아웃생성 POST /customer/signout204, 이후 /customer/me 401
지금 누구인가생성 GET /customer/me방문자 401, 고객 200
로그인 안 했으면 방문자런타임 SessionCurrentUser (요청 밖에서는 아무도 아님)-

생성: 비밀번호 확인

생성: 로그인 API (CustomerController)

로그인 API 는 생성됩니다: @login·@password 가 있는 Customer 의 줄에 @signin 을 쓰면 그 컨트롤러에 signin·signout·me 가 붙습니다. 화면은 생성된 TS 클라이언트의 signIn·signOut·me 를 씁니다. 역할은 관리자만 쓰는 role 필드라서 세션에 들어갑니다 - 그렇지 않은 필드면 가입자가 스스로 관리자가 될 수 있어 빌드가 경고하고 넣지 않습니다.

5. 내 정보 DSL 로 전부

고객은 자기 이름·연락처·집 주소·비밀번호를 고칩니다. 관리자는 모든 고객을 보고 역할을 바꿉니다.

업무 규칙DSL확인한 결과
자기 것만 보고 고친다@owner(customerId) @access(read=owner, update=owner)고객 검색 결과는 본인 1건, 남의 정보 GET·PUT → 404
역할은 관리자만role … @access(admin)본인이 "role":"A" → 무시(U 유지), 관리자가 바꾸면 A
비밀번호를 바꾸면 다시 해시@password새 비밀번호로 로그인 200, 옛 비밀번호 401
탈퇴는 관리자만delete 를 적지 않음본인 DELETE → 403
업무에 따라 부족한 것: 비밀번호 변경이 일반 수정(PUT)이라 현재 비밀번호를 확인하지 않습니다. 필요하면 commands: changePassword: current, next 로 명령을 선언하고 확인 로직은 행위 클래스에 씁니다. 고객이 스스로 탈퇴하게 하려면 @access(…, delete=owner) 한 단어면 됩니다.

6. 장바구니 DSL 밖

손님은 품목을 담고, 수량을 바꾸고, 비웁니다. 로그인하지 않아도 담을 수 있고, 결제할 때 로그인합니다.

장바구니는 브라우저(localStorage) 에만 있습니다 (web/src/cart.ts, 손으로). 서버에는 결제 순간에 품목 ID 와 수량만 갑니다. 화면에 보이는 가격은 안내용이고, 실제 가격은 서버가 결제 때 다시 매깁니다(7번 @snapshot).

DSL 밖 (의도): 결제 전의 장바구니는 업무 기록이 아니라고 보고 서버 모델에 넣지 않았습니다. "기기를 바꿔도 장바구니 유지" 가 업무라면 Cart·CartLine 엔티티와 @owner 로 DSL 에 쓰면 됩니다.

7. 주문 (결제) 대부분 DSL

로그인한 고객이 장바구니를 결제합니다. 배송지(비우면 집 주소)와 카드를 넣으면 주문이 접수(PLACED) 됩니다. 금액 = 줄 합계(단가 × 수량의 합) + VAT 5.5% − 할인 2.25%(VAT 포함 금액에서), 센트 단위 반올림.

업무 규칙DSL확인한 결과
로그인한 고객만, 자기 이름으로@owner(customerId) @access(create=user)방문자 401. "customerId":1 을 보내도 주인은 로그인한 고객(2)
한 줄 이상lines … @minsize(1, 'the cart is empty')빈 장바구니 → 400 "the cart is empty"
수량은 1 이상quantity: quantity! @min(1)0 → 400
카드 필수, 카드 번호 필수creditCard: CreditCard! @Embedded, creditCardNumber: string(30)!카드 없음·번호 없음 → 400
카드 번호는 돌려주지 않는다creditCardNumber … @writeonly주문 응답·조회에 번호 없음 (종류·만료일만)
단가 = 주문 당시 가격unitCost: money(10.2) @snapshot(item.unitCost)"unitCost":0.01 을 보내도 10.00. 나중에 품목 가격이 바뀌어도 그대로
없는 품목은 주문 불가(생성된 참조 확인 requireReferences)404 "Item 99999 does not exist"
소계·합계subTotal … @compute(unitCost * quantity)
totalWithoutVat … @compute(sum(lines.subTotal))
2 × 10.00 + 10.00 = 30.00
상태·금액은 요청으로 못 정함states:, @behavior, @compute → 읽기 전용"status":"SHP","total":1 을 보내도 PLC · 30.94
새 주문은 접수 상태states: status: initial: PLACEDPLC
VAT 5.5% 더하고 할인 2.25%손으로 - PurchaseOrder.place(), 요율은 application.ymlVAT 1.65, 할인 0.71, 합계 30.94
배송지 없으면 집 주소손으로 - place()배송지 비우면 집 주소, 넣으면 그 주소
주문일은 오늘손으로 - place() (테스트용 Clock)2026-10-03
접수되면 알린다@event(Placed(total)) + 손으로 raisePlaced()커밋 때 PurchaseOrderPlacedEvent

      

      

생성: 저장 직전에 채워지는 값

손으로: 가격 정책

결제 한 건이 처리되는 순서

  1. 요청 형식 생성DTO 검증: 줄 1개 이상, 수량 ≥ 1, 카드 필수 → 실패면 400
  2. 누가 생성create=user: 로그인 안 했으면 401, 주인 = 로그인한 고객
  3. DTO → 엔티티 생성품목·수량·주소·카드만 옮김 (가격·상태·합계는 읽기 전용)
  4. 파생 값 생성requireReferences(): 없는 품목 404, derive(): 줄 단가 스냅샷 → 소계 → 합계
  5. 가격 정책 손으로place(): 주문일, 기본 배송지, VAT, 할인, 반올림, raisePlaced()
  6. 불변식 생성저장 직전 다시 확인: 줄이 없으면 "the cart is empty"
  7. 저장·이벤트 생성상태 PLACED 로 저장, 커밋 때 접수 이벤트
DSL 로 못 하는 것:
• 가격 정책 - 설정 값(VAT 5.5%, 할인 2.25%)을 쓰는 계산, 계산 순서, 반올림. @compute 는 필드끼리의 곱·합만 됩니다. 이 샘플에 남은 유일한 도메인 코드입니다. 개선 후보: policy: (요율을 설정에서 읽는 계산).
• 기본값 규칙 - "배송지가 없으면 집 주소", "주문일은 오늘".
• 결제 승인·재고 - 모델에 없습니다 (원본 Java EE pet store 에도 없음). 카드사 연동은 DSL 의 port.yml(HTTP 포트)로 선언할 수 있지만 이 샘플은 쓰지 않습니다.

8. 주문 조회 DSL 로 전부

고객은 자기 주문 목록과 상세(줄·금액·배송지·카드 종류)를 봅니다. 관리자는 모든 주문을 봅니다.

업무 규칙DSL확인한 결과
자기 주문만@owner(customerId) @access(read=owner)검색은 본인 것만. {"customerId":1} 로 찾아도 0건. 남의 주문 404, 방문자 401
관리자는 전부(관리자는 모든 수준을 가짐)관리자 검색은 모든 고객의 주문
줄에 품목 이름dto.yml itemName: string @ref(item.name)줄마다 itemName ("Large")
최신순화면이 sortColumn: orderDate 를 보냄DSL 에 기본 정렬 없음


      

9. 주문 취소 DSL 로 전부

고객은 자기 주문을 배송 전(접수 상태) 에만 취소할 수 있습니다. 화면은 취소할 수 있을 때만 버튼을 보입니다.

업무 규칙DSL확인한 결과
자기 주문만cancel=owner남의 주문 → 404
접수 상태에서만states: status: cancel: PLACED -> CANCELLED두 번째 취소·배송 후 취소 → 409 "cannot cancel when status is SHIPPED; it must be [PLACED]"
취소되면 알린다@event(… Cancelled)PurchaseOrderCancelledEvent
버튼은 할 수 있을 때만(생성) TypeScript Orders.canCancel(order)서버의 canCancel() 과 같은 판단
모델에 없는 것: 환불. 결제 모델이 없어서 취소는 상태만 바꿉니다. 결제 상태가 생기면 같은 cancel 명령이 두 생명주기(주문·결제)를 함께 바꾸게 쓸 수 있습니다 - 주문 생명주기 참고.

10. 배송 (백오피스) DSL 로 전부

관리자는 백오피스에서 모든 주문을 보고, 접수된 주문을 배송 처리합니다. 주문은 고칠 수 없고, 지우는 것은 관리자뿐입니다.

업무 규칙DSL확인한 결과
관리자만@access 에 ship 을 적지 않음고객 → 403 "ship PurchaseOrder is for the administrator"
접수 상태에서만states: status: ship: PLACED -> SHIPPED취소된 주문 배송 → 409
배송되면 알린다@event(… Shipped)PurchaseOrderShippedEvent
버튼은 할 수 있을 때만(생성) TypeScript Orders.canShip(order)-
주문은 고칠 수 없다@editable 이 아닌 루트 → 수정 API 없음PUT → 405
삭제는 관리자만delete 를 적지 않음고객 403, 관리자 200

주문 생명주기 (states:)

PLACED →ship (관리자) SHIPPED PLACED →cancel (본인) CANCELLED


      

업무 지표와 업무 용어 (metrics:, @terms)

가게가 보는 숫자 - 매출(취소 제외, VAT 포함·할인 후), 주문 수, 객단가(주문 1건 평균), 배송·취소 건수, 고객 수 - 도 업무 정보입니다. 화면·SQL·BI 도구마다 "매출" 을 따로 정의하면 숫자가 달라지므로, DSL 에 한 번 적습니다.


      
      

      

확인: MetricsTest 가 주문을 접수·배송·취소한 뒤, 내보낸 지표 SQL 을 실제 DB 에서 실행한 값과 엔티티로 직접 계산한 값이 같은지 봅니다 (매출, 주문 수, 객단가, 배송·취소 건수, 고객 수).

아직 못 하는 것: 지표는 한 엔티티 안에서 셉니다. "취소되지 않은 주문의 팔린 수량" 처럼 주문 줄을 주문 상태로 거르는 지표는 쓸 수 없어서 넣지 않았습니다 - 억지로 쓰면 취소분까지 셉니다. 기간(월별)은 BI 도구가 orderDate 로 나눕니다.

오류 응답

모든 오류는 같은 모양(ProblemDetail: status, title, detail, 프로그램이 분기하는 code, 필드 오류는 errors 와 fieldErrors)입니다. 서비스가 거절한 것은 생성된 PetstoreApiErrors 가, 컨트롤러에 닿기 전에 Spring MVC 가 거절한 것(JSON 이 아닌 본문, 타입이 틀린 값, 없는 경로·메서드)은 그 파일이 가져오는 런타임의 RequestShapeErrors 가 만듭니다. 문구는 요청의 언어(Accept-Language, petstore-messages_ko.properties)로, 헤더가 없거나 번역이 없는 언어면 영어로 나갑니다 - 서버 머신의 언어로 나가지 않습니다(ShopFlowTest 가 한국어 JVM 에서 확인).

상태뜻예 (확인함)
400요청이 틀림 - DSL 의 검증, 잘못된 검색, 필수 값 누락, 타입이 받지 않는 값, JSON 이 아닌 본문"lines: the cart is empty", "No property 'nope'…", "A required value is missing.", 수량 -1 (lines[0].quantity, typeMismatch), 본문 {"lines": (unreadable)
401로그인 필요"Sign in to create PurchaseOrder"
403이 사람에게 허락되지 않음"ship PurchaseOrder is for the administrator"
404없음 - 또는 남의 것, 또는 요청이 가리킨 행이 없음, 또는 없는 경로남의 주문·고객 정보, "Item 99999 does not exist", 없는 상품 GET ("Item not found")
405그런 연산이 없음주문 PUT (method-not-allowed, Allow: POST)
409저장된 데이터나 지금 상태와 충돌"A unique value is already taken." (같은 로그인 ID), 품목이 남은 상품 삭제, "cannot cancel when status is SHIPPED…"
422도메인의 다른 규칙이 거절 (불변식)-

손으로 쓴 것

파일내용왜 DSL 이 아닌가
domain/model/PurchaseOrder.java가격 정책 place(): VAT·할인·반올림, 기본 배송지, 주문일설정 값을 쓰는 계산·기본값 규칙 문법 없음
application/PurchaseOrderService.java요율을 설정에서 읽고, 처음 저장될 때 place()위와 같음
adapter/in/web/shop/AccountController.java가입하고 바로 로그인가입과 로그인을 한 요청으로 묶는 것은 화면의 선택 (로그인·로그아웃·me 는 생성)
DemoAccounts.java데모 계정 admin/admin, user/user기준·데모 데이터 문법 없음 (data.sql 도 손으로)
OrderLine.java, CustomerService.java, PetstoreApplication.java빈 클래스, Clock 빈-
web/src/*.ts (540줄)화면화면은 DSL 의 대상이 아님. 생성된 TS 클라이언트(타입·API·검증·라벨·canX)를 씀

검토: 이 가게의 업무를 DSL 로 다 표현할 수 있나

위 업무별 표의 규칙 56개(표의 행) 중 DSL 로 쓴 것 50개, 손으로 쓴 것 4개, DSL 에 쓸 곳이 없어 DB 제약으로만 지켜지는 것 1개, 화면이 정하는 것 1개입니다. 표에 행으로 넣지 않은 빈 곳 - 비밀번호 최소 길이, 비밀번호 변경 때 현재 비밀번호 확인, 결제 승인·환불·재고(모델에 없음), 장바구니(브라우저에만, 의도) - 은 각 업무의 상자에 적었습니다. DSL 에 넣으려면 필요한 것:

#DSL 에 없는 것지금은제안
1업무 지표 - 매출, 주문 수, 객단가완료 - metrics:Ossie metrics 로 내보냄. 다른 엔티티 조건이 필요한 지표는 아직 안 됨
2업무 용어 - 주문 = 구매·order완료 - @termsOssie ai_context.synonyms
3가격 정책 - 설정 값으로 계산손으로 place()policy: - vat: total * config(vat-rate) / 100 같은 계산
4기본값 규칙 - 배송지 = 집 주소, 주문일 = 오늘손으로@default(customer.homeAddress), @default(today)
5로그인 API완료 - @login + @passwordsignin·signout·me 와 TS 클라이언트 생성. 가입 후 자동 로그인은 손으로
6비밀번호 최소 길이된다 - @length(min=8) 이 해시 전에 검사됨 (이 샘플은 데모 계정 때문에 넣지 않음)-
7삭제 조건 - 품목이 있는 상품은 못 지움DB 외래 키 409@deleteWhen(items empty) 같은 규칙 → 409 와 업무 문구
8기준·데모 데이터data.sql, DemoAccountsdata: 블록 (가입과 같은 규칙 - 해시 등 - 을 거쳐 넣음)
9기본 정렬화면이 지정@expose(20, sort=orderDate desc)

나머지 - 누가 무엇을 하는지, 주문의 생명주기, 가격 스냅샷, 합계, 필수·형식 검증, 비밀번호 저장, 민감 정보 숨김, 참조, 이벤트 - 는 DSL 에 있고, 서버·화면 코드와 Ossie 모델이 모두 거기서 생성됩니다.

Apache Ossie 시맨틱 모델

빌드할 때마다 같은 DSL 에서 blueprint/semantic_model.yml 이 만들어집니다 (Apache Ossie core spec 0.2.0.dev0). 분석·BI·AI 도구가 이 파일로 테이블의 뜻을 압니다: 컬럼의 타입, 조인, 상태 코드의 뜻(PLC = Placed), 그리고 비밀번호·카드 번호 컬럼은 내보내지 않습니다. 내보낸 모든 테이블·컬럼·키·조인이 실제 DB 에 있는지 빌드할 때마다 테스트(SemanticModelTest)가 확인합니다. Ossie 공식 스키마(ossie-schema.json)로 직접 검증한 결과 오류는 0 이었습니다(2026-10-03).



      

전체 DSL

env.yml
model/enum.yml
model/entity.yml
model/dto.yml
model/metrics.yml

← 쇼핑몰로 돌아가기