사용자 스토리, 엔티티, 워크플로에서 실무에 맞는 데이터베이스 스키마로 전환하는 실용적 방법과 AI로 누락/규칙을 점검하는 법을 배웁니다.

데이터베이스 스키마는 앱이 정보를 어떻게 기억할지에 대한 설계도입니다. 실무 관점에서 보면:
스키마가 실제 업무에 맞으면 사람들의 실제 행위—생성, 검토, 승인, 예약, 할당, 취소—를 반영합니다. 화이트보드상으로 깔끔해 보이는 구조보다 실제로 필요한 데이터를 저장하는 게 중요합니다.
사용자 스토리와 수용 기준은 누가 무엇을 하고, ‘완료’가 무엇인지 평이한 언어로 설명합니다. 이를 출발점으로 삼으면 중요한 세부사항(예: “환불을 누가 승인했는지 추적해야 한다” 또는 “예약은 여러 번 재조정될 수 있다”)을 놓칠 가능성이 줄어듭니다.
스토리에서 시작하면 범위(scope)에 대해 정직해집니다. 스토리(또는 워크플로)에 없으면 ‘옵션’으로 처리하고, 불필요하게 복잡한 모델을 미리 구축하지 마세요.
AI는 다음을 빠르게 도와줄 수 있습니다:
AI가 신뢰할 수 없거나 할 수 없는 것:
AI는 강력한 도우미로 쓰되, 결정권자는 사람이어야 합니다.
만약 AI 도우미를 실제 개발로 빠르게 연결하고 싶다면 Koder.ai 같은 vibe-coding 플랫폼이 스키마 결정에서 React + Go + PostgreSQL 앱으로 빠르게 전환하도록 도와줄 수 있습니다—단, 모델, 제약, 마이그레이션은 여전히 여러분이 통제합니다.
스키마 설계는 루프입니다: 초안 → 스토리에 대입해 테스트 → 누락된 데이터 발견 → 개선. 목표는 완벽한 초안이 아니라, 각 사용자 스토리에 대해 “이 워크플로가 필요한 모든 것을 저장할 수 있다”라고 설명할 수 있는 모델입니다.
요구사항을 테이블로 바꾸기 전에 무엇을 모델링할지 명확히 하세요. 좋은 스키마는 빈 페이지에서 시작하지 않습니다—사람들이 실제로 하는 일과 나중에 필요할 증거(화면, 출력물, 엣지 케이스)에서 시작합니다.
사용자 스토리는 핵심이지만 그 자체만으로는 부족합니다. 다음을 모으세요:
AI를 쓴다면 이런 입력들이 모델을 현실에 맞게 고정시켜 줍니다. AI는 엔티티와 필드를 빠르게 제안할 수 있지만, 제품에 맞지 않는 구조를 발명하지 않도록 실제 아티팩트가 필요합니다.
수용 기준에는 데이터 규칙이 가장 많이 숨어 있는 경우가 많습니다. 데이터로 명시하지 않아도 다음과 같은 문장을 찾으세요:
모호한 스토리(“사용자는 프로젝트를 관리할 수 있다”)는 여러 엔티티와 워크플로를 숨깁니다. 또 흔한 누락은 취소, 재시도, 부분 환불, 재할당 같은 엣지 케이스입니다.
테이블이나 다이어그램을 생각하기 전에 사용자 스토리를 읽고 명사를 강조하세요. 요구사항 글쓰기에서 명사는 시스템이 기억해야 할 ‘사물’을 가리키며, 종종 스키마의 엔티티가 됩니다.
간단한 정신 모델: 명사는 엔티티가 되고, 동사는 액션이나 워크플로가 된다. 예: “매니저가 기술자에게 작업을 할당한다”면 엔티티 후보는 manager, technician, job이고, “할당”은 나중에 모델링할 관계를 암시합니다.
모든 명사가 테이블이 될 필요는 없습니다. 명사가 엔티티 후보일 때:
한 번만 등장하거나 다른 것을 설명하는 용어(예: “빨간 버튼”, “금요일”)이면 엔티티가 아닐 수 있습니다.
모든 세부사항을 테이블로 만드는 것은 흔한 실수입니다. 간단한 규칙:
두 가지 예시:
Address는 별도 엔티티. 단일 우편 주소만 필요하고 재사용하지 않는다면 속성으로 둬도 됨.AI는 스토리를 스캔해 후보 명사를 테마별로 묶어 초안 목록을 빠르게 만들 수 있습니다. 유용한 프롬프트 예: “저장해야 할 명사들을 추출하고 중복/동의어를 그룹핑해줘.”
AI 출력은 시작점으로 보고 다음 같은 후속 질문을 하세요:
1단계의 목표는 각 엔티티를 실제 스토리에 근거해 방어할 수 있는 짧고 깔끔한 목록을 만드는 것입니다.
엔티티(예: Order, Customer, Ticket)를 정했다면 다음은 나중에 필요할 세부사항을 캡처하는 것입니다. 데이터베이스에서 이 세부사항은 필드(혹은 속성)입니다—시스템이 잊으면 안 될 알림들입니다.
사용자 스토리로 시작하고 수용 기준을 체크리스트처럼 읽으세요.
요구사항에 “사용자가 배송일로 주문을 필터링할 수 있다”고 쓰여 있으면 delivery_date는 필수입니다(다른 저장 데이터에서 신뢰성 있게 유도되지 않는 한). “누가 요청을 승인했는지와 언제인지 보여줘”라면 보통 approved_by, approved_at이 필요합니다.
실용적 테스트: 이 값이 화면에 표시되거나, 검색/정렬/감사/계산에 필요할까? 그렇다면 필드일 가능성이 큽니다.
많은 스토리에 “상태”, “타입”, “우선순위” 같은 단어가 나옵니다. 이를 통제된 어휘로 처리하세요—허용값이 제한된 세트입니다.
세트가 작고 안정적이면 enum 스타일 필드로 충분합니다. 늘어나거나 라벨/권한 관리가 필요하면 별도 조회 테이블(예: status_codes)을 만들어 참조를 저장하세요.
이렇게 스토리가 신뢰할 수 있는 필드로 바뀝니다—검색 가능하고 리포트에 쓸 수 있으며 입력 오류를 줄입니다.
엔티티(예: User, Order, Invoice, Comment)와 필드를 나열했으면, 이제 이를 연결하세요. 관계는 스토리가 암시하는 “사물들이 어떻게 상호작용하는지” 레이어입니다.
일대일(1:1): “하나의 것이 정확히 하나의 다른 것을 가진다.”
User ↔ Profile (특별한 이유가 없다면 병합 가능)일대다(1:N): “하나가 여러 개를 가질 수 있다.” (가장 흔함)
User → Order (Order에 user_id 저장)다대다(M:N): “많은 것들이 많은 것들과 연결된다.” 추가 테이블이 필요함.
데이터베이스에 Order 안에 “제품 ID 목록”을 넣으면 나중에 검색/업데이트/리포팅에서 문제가 됩니다. 대신 관계 자체를 나타내는 조인 테이블을 만드세요.
예:
OrderProductOrderItem(조인 테이블)OrderItem은 보통 다음을 포함합니다:
order_idproduct_idquantity, unit_price, discount)스토리의 세부사항(예: 수량)은 개체 어느 쪽에도 속하지 않고 관계 위에 놓이는 경우가 많다는 점을 주목하세요.
스토리는 연결이 반드시 있어야 하는지 아니면 없을 수도 있는지 알려줍니다.
Order에 user_id가 필요(비어있으면 안 됨)phone은 널 허용shipping_address_id는 비어있을 수 있음간단 체크: 스토리가 레코드를 생성할 때 그 링크 없이는 만들 수 없다고 암시하면 필수로 처리하세요. “할 수 있다(may)”나 예외가 있으면 선택으로 처리하세요.
스토리를 읽을 때 간단한 페어링으로 다시 작성하세요:
User 1:N CommentComment N:1 User스토리에 있는 모든 상호작용에 대해 이렇게 하세요. 끝날 즈음에는 ER 다이어그램 도구를 열기 전에 실제 업무 흐름에 맞는 연결된 모델이 완성되어 있을 것입니다.
사용자 스토리는 사람들이 무엇을 원하는지 알려주고, 워크플로는 일이 어떻게 진행되는지 단계별로 보여줍니다. 워크플로를 데이터로 옮기면 “저장하는 것을 잊었다”는 문제를 빌드 전에 빨리 발견할 수 있습니다.
워크플로를 액션과 상태 변화의 순서로 적으세요. 예:
굵은 단어들은 보통 status 필드(또는 작은 상태 테이블)가 되며, 허용값이 명확히 정의됩니다.
각 단계를 걸어가며 “나중에 무엇을 알아야 할까?”를 묻습니다. 워크플로는 보통 다음과 같은 필드를 요구합니다:
submitted_at, approved_at, completed_atcreated_by, assigned_to, approved_byrejection_reason, approval_notesequence대기, 에스컬레이션, 전달이 포함되면 보통 최소 하나의 타임스탬프와 ‘현재 누가 담당인지’ 필드가 필요합니다.
일부 워크플로 단계는 필드 이상의 데이터 구조를 요구합니다:
AI에 (1) 사용자 스토리와 수용 기준, (2) 워크플로 단계를 모두 주고 각 단계에 필요한 데이터를 나열하게 하세요(상태, 행위자, 타임스탬프, 출력물). 그런 다음 현재 필드/테이블로 지원할 수 없는 요구사항을 하이라이트하게 하세요.
Koder.ai 같은 플랫폼에서는 이 ‘갭 체크’가 특히 실용적인데, 스키마 가정을 조정하고 스캐폴딩을 재생성하며 장황한 수동 작업 없이 빠르게 반복할 수 있기 때문입니다.
사용자 스토리를 테이블로 바꿀 때 단순히 필드를 나열하는 것이 아니라 시간이 지나도 데이터를 식별하고 일관성을 유지하는 방법을 결정하는 것입니다.
**기본키(primary key)**는 하나의 레코드를 고유하게 식별합니다—행의 영구 신분증이라 생각하세요.
이유: 스토리는 업데이트, 참조, 이력을 암시합니다. 예: “지원 담당자가 주문을 보고 환불을 발행할 수 있다”면 주문을 가리킬 수 있는 안정적 방법이 필요합니다.
보통 내부 id(숫자 또는 UUID)를 사용해 절대 바뀌지 않게 합니다.
**외래키(foreign key)**는 한 테이블이 다른 테이블을 안전하게 가리키는 방법입니다. 예: orders.customer_id가 customers.id를 참조하면 데이터베이스가 모든 주문이 실제 고객에 속하도록 강제할 수 있습니다.
스토리 예: “사용자는 자신의 송장을 볼 수 있다” → 송장은 떠다니는 것이 아니라 고객에 붙어 있어야 합니다(종종 주문이나 구독에도 붙음).
사용자 스토리에는 종종 숨겨진 고유성 요구가 있습니다:
이 규칙들은 혼란스러운 중복을 예방합니다.
인덱스는 “이메일로 고객 찾기”나 “고객별 주문 나열” 같은 검색을 빠르게 합니다. 먼저 가장 흔한 쿼리와 고유성 규칙에 맞춘 인덱스를 추가하세요.
나중에 최적화할 것: 드물게 쓰는 리포트나 추측성 필터에 대한 과도한 인덱싱은 미루세요. 시연 환경의 느린 쿼리 증거를 기반으로 최적화하세요.
정규화의 목표는 상충하는 중복을 방지하는 것입니다. 같은 사실이 두 곳에 저장되면 언젠가 불일치가 발생합니다(이름의 두 철자, 두 가격, 두 개의 ‘현재’ 주소). 정규화된 스키마는 각 사실을 한 번만 저장하고 참조합니다.
1) 반복 그룹 찾기
Phone1, Phone2, Phone3 또는 ItemA, ItemB, ItemC 같은 패턴이 보이면 별도 테이블(e.g., CustomerPhones, OrderItems)로 분리하세요. 반복 그룹은 검색·검증·확장이 어렵게 만듭니다.
2) 동일한 이름/세부사항을 여러 테이블에 복사하지 마세요
CustomerName이 Orders, Invoices, Shipments에 복사되어 있다면 진실의 원천이 여러 개입니다. 고객 세부는 Customers에 두고 다른 곳에는 customer_id만 저장하세요.
3) 동일한 것을 위한 여러 열 피하기
billing_address, shipping_address, home_address 같은 여러 컬럼은 진짜로 다른 개념이면 괜찮습니다. 그러나 여러 주소를 모델링하려면 Addresses 테이블과 type 필드를 사용하세요.
4) 조회값과 자유 텍스트 분리하기
사용자가 알려진 목록에서 선택하면(상태, 카테고리, 역할) 일관되게 enum이나 조회 테이블로 모델링하세요. “Pending” vs “pending” vs “PENDING” 같은 문제를 방지합니다.
5) 모든 비-ID 필드가 올바른 것에 의존하는지 확인
테이블에서 어떤 컬럼이 테이블의 주된 엔티티를 설명하지 않는다면 다른 곳으로 옮겨야 합니다. 예: Orders에 product_price가 있으면 “주문 시점의 가격”이라는 역사적 스냅샷인지 확인하세요.
의도적으로 복제할 때가 있습니다:
핵심은 의도적으로 하는 것임을 문서화하고, 어떤 필드가 출처인지와 복사 업데이트 방식을 기록하는 것입니다.
AI는 의심스러운 중복(반복 컬럼, 유사 필드명, 일관성 없는 상태 필드)을 찾아 분할을 제안할 수 있습니다. 하지만 단순성·유연성·성능 간의 트레이드오프는 사람이 제품 사용 방식을 기반으로 결정해야 합니다.
유용한 규칙: 나중에 재생성할 수 없는 사실은 저장하고, 나머지는 계산하세요.
저장 데이터는 진실의 출처입니다: 개별 라인 아이템, 타임스탬프, 상태 변경, 누가 무엇을 했는지. 계산(파생) 데이터는 그 사실들에서 만들어지는 값들: 합계, 카운터, is overdue 같은 플래그, 현재 재고 같은 롤업입니다.
같은 입력에서 두 값을 계산할 수 있다면 사실(facts)을 저장하고 나머지를 계산하는 것이 안전합니다. 그렇지 않으면 모순이 생깁니다.
파생값은 입력이 바뀔 때마다 달라집니다. 입력과 결과를 둘 다 저장하면 워크플로와 엣지 케이스(수정, 환불, 부분 배송, 소급 변경) 전반에서 동기화를 유지해야 합니다. 하나라도 누락되면 데이터가 서로 다른 이야기를 하게 됩니다.
예: order_items와 함께 order_total을 저장하면 수량을 바꾸거나 할인을 적용했을 때 총액이 완벽히 업데이트되지 않을 위험이 있습니다.
워크플로는 언제 역사적 진실을 저장해야 하는지 알려줍니다. 사용자가 ‘그 당시의 값’을 알아야 한다면 스냅샷을 저장하세요.
주문 예시:
order_total(스냅샷)—세금, 할인, 가격 규칙이 나중에 바뀔 수 있기 때문재고는 보통 입고·판매·조정 같은 이동으로부터 계산합니다. 감사가 필요하면 이동을 저장하고 리포팅 성능을 위해 주기적 스냅샷을 선택적으로 저장합니다.
로그인 트래킹의 경우 last_login_at을 이벤트 타임스탬프로 저장하세요. “지난 30일간 활성인가?” 같은 것은 계산으로 유지하세요.
지원 티켓 앱을 예로 들어 5개의 사용자 스토리에서 간단한 ER 모델(엔티티 + 필드 + 관계)로 가고, 하나의 워크플로로 검증해보겠습니다.
이 명사들에서 핵심 엔티티를 도출합니다:
전(흔한 누락): Ticket에 assignee_id는 있지만, 오직 상담원만 할당될 수 있다는 규칙을 보장하지 않음.
후(수정): AI가 이를 지적하고 실제 규칙을 추가합니다: assignee는 role = “agent”인 User여야 한다 (애플리케이션 검증이나 DB 제약/정책으로 구현). 이렇게 하면 나중에 “고객에게 할당됨” 같은 잘못된 데이터로 리포트가 깨지는 것을 막습니다.
스키마는 모든 사용자 스토리를 데이터로 신뢰성 있게 답할 수 있을 때만 ‘완료’입니다. 가장 단순한 검증은 각 스토리를 집어들고 묻는 것입니다: “이 데이터베이스로 이 질문에 항상 답할 수 있는가?” 가능성이 “아마도”라면 모델에 누락이 있는 것입니다.
모든 사용자 스토리를 리포트나 API가 묻는 질문으로 바꿔보세요. 예:
스토리를 명확한 질문으로 표현할 수 없으면 그 스토리가 불명확한 것입니다. 표현은 할 수 있는데 스키마로 답할 수 없다면 필드, 관계, 상태/이벤트, 제약 중 하나가 부족한 것입니다.
핵심 테이블마다 5–20행 정도의 작은 데이터셋을 만들고 정상 케이스와 까다로운 케이스(중복, 누락값, 취소)를 포함하세요. 그 데이터로 스토리를 시뮬레이션하면 “어떤 주소가 사용되었는지 알 수 없다”거나 “누가 변경을 승인했는지 저장할 곳이 없다” 같은 문제를 빨리 발견할 수 있습니다.
AI에 각 스토리별 검증 질문(엣지 케이스, 삭제 시나리오 포함)을 생성하게 하고, 그 질문에 답하려면 어떤 데이터가 필요한지 나열하게 하세요. 그 목록을 스키마와 비교하면 불일치가 구체적 작업 항목으로 드러납니다.
AI는 데이터 모델링을 빠르게 해주지만, 민감 정보 유출이나 잘못된 가정 하드코딩 위험을 키울 수 있습니다. 매우 빠른 어시스턴트로 취급하되 가드레일을 두세요.
모델링에 충분히 현실적이되 안전하게 익명화된 입력을 공유하세요:
invoice_total: 129.50, status: "paid")피해야 할 것:
현실감 있는 데이터가 필요하면 형식을 맞춘 합성 샘플을 생성하세요—프로덕션 행을 복사하지 마세요.
스키마는 보통 “모두가 다르게 가정했다”는 문제로 실패합니다. ER 모델 옆(또는 같은 저장소)에 짧은 결정 로그를 남기세요:
이렇게 하면 AI 출력이 일회성 산출물이 아니라 팀 지식이 됩니다.
스키마는 스토리와 함께 진화합니다. 다음을 지키세요:
Koder.ai 같은 플랫폼을 쓰면 스키마 변경 시 스냅샷과 롤백 같은 가드레일을 활용하고, 깊은 커스터마이징이 필요할 때 소스 코드를 내보내어 전통적 검토 과정을 밟을 수 있습니다.
스토리를 읽고 시스템이 기억해야 할 ‘사물’을 나타내는 명사를 강조하세요(예: Ticket, User, Category).
명사를 엔티티로 올릴 기준은:
각 엔티티는 특정 스토리 문장을 들어 정당화할 수 있는 짧은 목록으로 유지하세요.
“속성 vs 엔티티” 테스트를 사용하세요:
customer.phone_number).빠른 단서: 언젠가 “여러 개”가 필요하다면 별도 테이블이 필요할 가능성이 큽니다.
수용 기준을 저장 체크리스트로 취급하세요. 필터/정렬/표시/감사에 필요하다고 명시되면, 그것은 저장되어야 하거나(또는 신뢰할 수 있게 파생되어야) 합니다.
예:
approved_by, approved_atdelivery_dateemail에 고유 제약/인덱스스토리 문장을 관계 문장으로 다시 써보세요:
customer_id를 orders에 둠)order_items 추가)관계 자체에 데이터(수량, 가격, 역할 등)가 있다면 그 데이터는 조인 테이블에 둡니다.
M:N은 두 외래키와 관계 고유의 필드를 저장하는 조인 테이블로 모델링하세요.
전형적 패턴:
ordersproducts워크플로를 단계별로 따라가며 “이 일이 나중에 발생했음을 증명하려면 무엇이 필요할까?”를 물어보세요.
일반적으로 추가되는 항목:
submitted_at, closed_atcreated_by, , 우선 다음을 추가하세요:
id)orders.customer_id → customers.id)그다음 자주 사용하는 조회를 위한 인덱스(예: , , )를 추가하세요. 추측성 인덱싱은 실제 쿼리 패턴을 보고 미루는 것이 안전합니다.
간단한 일관성 검사:
Phone1/Phone2 같은 반복 그룹이 보이면 자식 테이블로 분리하세요.성능·리포팅·스냅샷 같은 명확한 이유가 있을 때만 의도적으로 비정규화하고, 어떤 필드가 진실의 근거인지 문서화하세요.
나중에 재생성할 수 없는 사실은 저장하고, 그렇지 않으면 계산하세요.
저장에 적합:
계산에 적합:
is overdue 같은 플래그같은 파생값을 저장한다면 어느 시점에 어떻게 동기화할지 명확히 정하고 엣지 케이스를 테스트하세요.
초안 생성용으로 AI를 활용하고, 결과는 항상 실물 아티팩트와 대조하세요.
실무 프롬프트 예시:
가드레일:
order_itemsorder_idproduct_idquantityunit_price하나의 컬럼에 ID 목록을 넣지 마세요—조회, 업데이트, 무결성 검증이 어려워집니다.
assigned_toclosed_byrejection_reason“누가 언제 상태를 바꿨나”를 추적해야 하면 단일 필드를 덮어쓰지 말고 이벤트/감사 테이블을 추가하세요.
emailcustomer_idstatus + created_atorder_total