Pet Store - 업무를 DSL 로
YAPS Pet Store 는 애완동물을 파는 온라인 가게입니다 (Java EE 7 pet store 를 ElastiCORE 3.0 으로 다시 만든 것). 손님은 분류(물고기·개·파충류·고양이·새) → 상품(불독·푸들…) → 품목(수컷 강아지·암컷 강아지처럼 실제로 사는 단위) 순으로 둘러보고 장바구니에 담습니다. 결제하려면 회원 가입·로그인하고, 배송지와 카드를 넣으면 주문이 접수됩니다. 금액은 품목 가격 합계에 VAT 5.5% 를 더하고, VAT 포함 금액에서 2.25% 할인합니다. 손님은 자기 주문을 보고 취소할 수 있고(배송 전까지), 가게 관리자는 백오피스에서 주문을 배송 처리하고 상품을 관리합니다.
이 문서는 그 업무를 업무 단위로 나눠, 각 규칙이 DSL 의 어디에 적혔는지, 무엇이 생성되고 무엇을 손으로 썼는지, 그리고 DSL 로 아직 표현할 수 없는 것까지 적습니다. "확인한 결과" 열은 실행 중인 애플리케이션에 HTTP 로 요청해서 본 응답입니다. DSL 과 생성 코드 발췌는 빌드 결과물에서 그대로 읽어 왔습니다.
사람과 권한
방문자
로그인 전. 상품을 보고 검색하고, 가입합니다.고객 (USER)
가입하면 이 역할. 자기 정보와 자기 주문만 보고 고칩니다.관리자 (ADMIN)
모든 것을 봅니다. 배송, 상품 관리, 고객 역할 변경, 삭제.DSL 은 엔티티마다 @access(연산=수준) 으로 말합니다. 수준은 public(방문자도) · user(로그인) ·
owner(본인 것만, @owner(필드) 로 주인을 정함) · admin. 적지 않은 연산은 관리자만 할 수 있습니다.
| 할 일 | 방문자 | 고객 (본인 것) | 고객 (남의 것) | 관리자 | DSL |
|---|---|---|---|---|---|
| 상품 보기·검색 | 200 | 200 | - | 200 | read=public |
| 상품 만들기·고치기·지우기 | 401 | 403 | - | 200 | (적지 않음 → 관리자) |
| 회원 가입 | 201 | - | - | - | create=public |
| 고객 정보 보기 | 401 | 200 | 404 | 200 | read=owner |
| 고객 정보 고치기 | 401 | 200 | 404 | 200 | update=owner |
| 고객 역할 바꾸기 | - | 무시됨 | - | 200 | role … @access(admin) |
| 탈퇴(고객 삭제) | 401 | 403 | 403 | 200 | (적지 않음 → 관리자) |
| 주문하기 | 401 | 200 | - | 200 | create=user |
| 주문 보기 | 401 | 200 | 404 | 200 | read=owner |
| 주문 취소 | 401 | 200 | 404 | 200 | cancel=owner |
| 배송 | 401 | 403 | 403 | 200 | (적지 않음 → 관리자) |
| 주문 삭제 | 401 | 403 | 403 | 200 | (적지 않음 → 관리자) |
남의 것은 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 @ManyToOneproduct: 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…" |
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, @past | 400 "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/signout | 204, 이후 /customer/me 401 |
| 지금 누구인가 | 생성 GET /customer/me | 방문자 401, 고객 200 |
| 로그인 안 했으면 방문자 | 런타임 SessionCurrentUser (요청 밖에서는 아무도 아님) | - |
생성: 비밀번호 확인
생성: 로그인 API (CustomerController)
@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 |
commands: changePassword: current, next 로 명령을 선언하고 확인 로직은 행위 클래스에 씁니다.
고객이 스스로 탈퇴하게 하려면 @access(…, delete=owner) 한 단어면 됩니다.6. 장바구니 DSL 밖
손님은 품목을 담고, 수량을 바꾸고, 비웁니다. 로그인하지 않아도 담을 수 있고, 결제할 때 로그인합니다.
장바구니는 브라우저(localStorage) 에만 있습니다 (web/src/cart.ts, 손으로). 서버에는 결제 순간에 품목 ID 와 수량만 갑니다.
화면에 보이는 가격은 안내용이고, 실제 가격은 서버가 결제 때 다시 매깁니다(7번 @snapshot).
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: PLACED | PLC |
| VAT 5.5% 더하고 할인 2.25% | 손으로 - PurchaseOrder.place(), 요율은 application.yml | VAT 1.65, 할인 0.71, 합계 30.94 |
| 배송지 없으면 집 주소 | 손으로 - place() | 배송지 비우면 집 주소, 넣으면 그 주소 |
| 주문일은 오늘 | 손으로 - place() (테스트용 Clock) | 2026-10-03 |
| 접수되면 알린다 | @event(Placed(total)) + 손으로 raisePlaced() | 커밋 때 PurchaseOrderPlacedEvent |
생성: 저장 직전에 채워지는 값
손으로: 가격 정책
결제 한 건이 처리되는 순서
- 요청 형식 생성DTO 검증: 줄 1개 이상, 수량 ≥ 1, 카드 필수 → 실패면 400
- 누가 생성
create=user: 로그인 안 했으면 401, 주인 = 로그인한 고객 - DTO → 엔티티 생성품목·수량·주소·카드만 옮김 (가격·상태·합계는 읽기 전용)
- 파생 값 생성
requireReferences(): 없는 품목 404,derive(): 줄 단가 스냅샷 → 소계 → 합계 - 가격 정책 손으로
place(): 주문일, 기본 배송지, VAT, 할인, 반올림,raisePlaced() - 불변식 생성저장 직전 다시 확인: 줄이 없으면 "the cart is empty"
- 저장·이벤트 생성상태 PLACED 로 저장, 커밋 때 접수 이벤트
• 가격 정책 - 설정 값(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:)
states:바로 아래 키status는 필드 이름입니다 -fields:의status: OrderStatus.- 상태 이름(
PLACED…)은 그 필드 타입인OrderStatusenum 의 상수 이름입니다 (DB·JSON 코드PLC가 아님). enum 에 없는 이름이면 빌드가 경고하고 그 생명주기를 만들지 않습니다. initial은 시작 상태, 나머지 키는 명령이고 값은시작 상태 -> 도착 상태입니다. 명령마다 API(POST …/{id}/ship)가 생깁니다.- SHIPPED 다음은? 나가는 화살표가 없는 상태는 끝입니다. 다음 단계가 필요하면 enum 에
DELIVERED를 더하고deliver: SHIPPED -> DELIVERED한 줄. - 상태 필드가 둘 이상(주문 상태·결제 상태)이면 필드마다 생명주기를 쓰고, 같은 명령 이름을 두 곳에 두면 그 명령은 둘을 함께 바꿉니다 - 모든 생명주기를 먼저 확인하고, 하나라도 아니면 아무것도 바꾸지 않습니다.
업무 지표와 업무 용어 (metrics:, @terms)
가게가 보는 숫자 - 매출(취소 제외, VAT 포함·할인 후), 주문 수, 객단가(주문 1건 평균), 배송·취소 건수, 고객 수 - 도 업무 정보입니다. 화면·SQL·BI 도구마다 "매출" 을 따로 정의하면 숫자가 달라지므로, DSL 에 한 번 적습니다.
- 한 줄이 지표 하나:
sum|avg|min|max|count(엔티티.필드), 조건은where 필드 != 값 and …, 지표끼리는revenue / orderCount. - enum 은 상수 이름으로 씁니다(
CANCELLED) - 내보낼 때 저장 코드('CAN')로 바뀝니다. @terms(…)는 업무에서 부르는 이름·동의어입니다. 엔티티(주문 = order, purchase, 구매)·필드(가격)·지표에 붙입니다.- 애플리케이션 코드는 만들지 않습니다. Apache Ossie 시맨틱 모델의
metrics와ai_context.synonyms로 나가서, BI·AI 도구가 "매출" 을 물으면 이 정의를 씁니다.
확인: MetricsTest 가 주문을 접수·배송·취소한 뒤, 내보낸 지표 SQL 을 실제 DB 에서 실행한 값과 엔티티로 직접 계산한 값이
같은지 봅니다 (매출, 주문 수, 객단가, 배송·취소 건수, 고객 수).
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 | 완료 - @terms | Ossie ai_context.synonyms |
| 3 | 가격 정책 - 설정 값으로 계산 | 손으로 place() | policy: - vat: total * config(vat-rate) / 100 같은 계산 |
| 4 | 기본값 규칙 - 배송지 = 집 주소, 주문일 = 오늘 | 손으로 | @default(customer.homeAddress), @default(today) |
| 5 | 로그인 API | 완료 - @login + @password | signin·signout·me 와 TS 클라이언트 생성. 가입 후 자동 로그인은 손으로 |
| 6 | 비밀번호 최소 길이 | 된다 - @length(min=8) 이 해시 전에 검사됨 (이 샘플은 데모 계정 때문에 넣지 않음) | - |
| 7 | 삭제 조건 - 품목이 있는 상품은 못 지움 | DB 외래 키 409 | @deleteWhen(items empty) 같은 규칙 → 409 와 업무 문구 |
| 8 | 기준·데모 데이터 | data.sql, DemoAccounts | data: 블록 (가입과 같은 규칙 - 해시 등 - 을 거쳐 넣음) |
| 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).