- options JSON 컬럼 패턴을 엑셀 비유로 쉽게 설명 - 멀티테넌시(tenant_id) 개념 해설 - 실제 SAM 테이블 예시 (주문, 입고, 공정) - FAQ 5개, 판단 흐름도, 한 장 요약 포함 - INDEX.md에 문서 등록
22 KiB
SAM 테이블 설계 가이드 — 비전문가용
작성일: 2026-03-02 대상 독자: 개발자, 기획자, 관리자 — 데이터베이스를 잘 모르는 분도 읽을 수 있습니다 관련 정책:
standards/options-column-policy.md(개발자 전용 상세 규칙)
1. 이 문서는 왜 필요한가?
SAM은 여러 회사가 하나의 시스템을 공유하는 구조입니다. A회사, B회사, C회사가 모두 같은 프로그램을 쓰지만, 각 회사가 필요한 정보는 다릅니다.
이 문서는 SAM에서 데이터를 어떻게 저장하는지, 그 설계 철학을 누구나 이해할 수 있도록 설명합니다.
2. 기본 개념: 데이터베이스 테이블이란?
데이터베이스 테이블은 엑셀 시트와 같습니다.
"주문" 테이블 (= 엑셀 시트)
열(컬럼) → 주문번호 │ 고객명 │ 금액 │ 상태
─────────────────────────────────────────────────────────
행(레코드) 1 → ORD-001 │ 김철수 │ 500,000 │ 완료
행(레코드) 2 → ORD-002 │ 이영희 │ 300,000 │ 진행중
행(레코드) 3 → ORD-003 │ 박민수 │ 800,000 │ 대기
- 열(컬럼) = 정보의 종류 (주문번호, 고객명, 금액...)
- 행(레코드) = 실제 데이터 한 건 (주문 1건)
3. 문제: 회사마다 필요한 정보가 다르다
SAM은 여러 회사가 같은 테이블을 공유합니다.
같은 "주문" 테이블을 쓰는데...
🏭 A회사 (블라인드 제조)
→ "절곡 각도", "날개 수" 정보가 필요해요
🏭 B회사 (스크린 제조)
→ "메시 밀도", "소재 종류" 정보가 필요해요
🏭 C회사 (셔터 제조)
→ "날개 간격", "색상 코드" 정보가 필요해요
3.1 전통적인 해결 방법 (SAM은 이렇게 안 합니다)
필요할 때마다 엑셀에 열을 추가하는 것처럼, 테이블에 컬럼을 추가합니다.
"주문" 테이블 — 전통적 방식
주문번호 │ 고객명 │ 금액 │ 절곡각도 │ 날개수 │ 메시밀도 │ 소재 │ 날개간격 │ 색상코드
─────────────────────────────────────────────────────────────────────────────────
ORD-001 │ 김철수 │ 50만 │ 45도 │ 12개 │ (빈칸) │ (빈칸) │ (빈칸) │ (빈칸) ← A회사
ORD-002 │ 이영희 │ 30만 │ (빈칸) │ (빈칸)│ 18 │ 폴리 │ (빈칸) │ (빈칸) ← B회사
ORD-003 │ 박민수 │ 80만 │ (빈칸) │ (빈칸)│ (빈칸) │ (빈칸) │ 25mm │ #FF0000 ← C회사
문제점:
- 회사가 100개면? 열이 수백 개로 늘어남
- 각 회사는 자기 것 빼고 전부 빈칸
- 새 회사가 들어올 때마다 시스템 전체를 수정해야 함
- 열 추가 = 시스템 중단 위험이 있는 작업
3.2 SAM의 해결 방법: "메모칸(options)" 하나로 통합
핵심 열만 남기고, 나머지는 메모칸 하나에 자유롭게 적습니다.
"주문" 테이블 — SAM 방식
주문번호 │ 고객명 │ 금액 │ 상태 │ options (메모칸)
────────────────────────────────────────────────────────────────────────
ORD-001 │ 김철수 │ 50만 │ 완료 │ {"절곡각도": 45, "날개수": 12} ← A회사
ORD-002 │ 이영희 │ 30만 │ 진행 │ {"메시밀도": 18, "소재": "폴리에스터"} ← B회사
ORD-003 │ 박민수 │ 80만 │ 대기 │ {"날개간격": 25, "색상코드": "#FF0000"} ← C회사
ORD-004 │ 최지은 │ 40만 │ 대기 │ null ← 메모 없음
options = JSON이라는 형식의 메모칸. { } 안에 자유롭게 정보를 넣을 수 있습니다.
4. 어떤 정보를 열(컬럼)로 만들고, 어떤 정보를 메모칸(options)에 넣나?
이것이 SAM 테이블 설계의 가장 중요한 판단 기준입니다.
4.1 판단 흐름 (5가지 질문)
새로운 정보를 저장해야 할 때, 아래 질문에 답합니다.
질문 1. 이 정보로 다른 테이블의 데이터를 연결(참조)하나?
예: 고객ID로 고객 테이블을 찾는다
→ YES: 일반 컬럼
질문 2. 이 정보로 자주 검색(필터)하나?
예: "완료" 상태인 주문만 보여줘
→ YES: 일반 컬럼
질문 3. 이 정보로 정렬하나?
예: 최신 주문부터 보여줘
→ YES: 일반 컬럼
질문 4. 이 정보가 절대 중복되면 안 되나?
예: 주문번호는 세상에 하나뿐이어야 한다
→ YES: 일반 컬럼
질문 5. 이 정보로 합계/평균을 계산하나?
예: 이번 달 매출 합계
→ YES: 일반 컬럼
질문 1~5 전부 NO → options 메모칸에 저장
4.2 실생활 예시로 비교
예시 1: "주문" 테이블
| 정보 | 어디에 저장? | 이유 |
|---|---|---|
| 주문번호 | 일반 컬럼 | 중복 불가 + 검색 필수 |
| 고객 ID | 일반 컬럼 | 고객 테이블과 연결 |
| 금액 | 일반 컬럼 | 합계 계산 필요 |
| 상태 (진행/완료) | 일반 컬럼 | 필터(검색) 필수 |
| 생성일 | 일반 컬럼 | 정렬 필요 |
| 배송지 주소 | options | 부가 정보, 검색 안 함 |
| 수신자 이름 | options | 부가 정보 |
| 수신자 연락처 | options | 부가 정보 |
| 특이사항 메모 | options | 있어도 되고 없어도 됨 |
실제 SAM 코드에서 주문(Order) 테이블:
일반 컬럼: id, tenant_id, order_number, client_id, total_amount, status, created_at
options: {"shipping_cost_code":"착불", "receiver":"홍길동",
"receiver_contact":"010-1234-5678",
"shipping_address":"서울 강남구 역삼동 123"}
예시 2: "입고검사" 테이블
| 정보 | 어디에 저장? | 이유 |
|---|---|---|
| 품목 ID | 일반 컬럼 | 품목 테이블과 연결 |
| 수량 | 일반 컬럼 | 합계 계산 |
| 입고일 | 일반 컬럼 | 정렬 + 검색 |
| 제조사 | options | 모든 입고에 있지는 않음 |
| 검사 결과 (합격/불합격) | options | 검사를 안 하는 회사도 있음 |
| 검사일 | options | 선택적 정보 |
실제 SAM 코드에서 입고(Receiving) 테이블:
일반 컬럼: id, tenant_id, item_id, quantity, received_at, status
options: {"manufacturer":"삼성전자",
"inspection_status":"합격",
"inspection_date":"2026-03-01"}
검사 결과가 options에 있는 이유: 모든 회사가 입고검사를 하는 것은 아닙니다. A회사는 검사를 하고, B회사는 안 합니다. 이걸 일반 컬럼으로 만들면 B회사에겐 항상 빈칸입니다.
예시 3: "공정" 테이블
| 정보 | 어디에 저장? | 이유 |
|---|---|---|
| 공정 코드 | 일반 컬럼 | 중복 불가 + 검색 |
| 공정명 | 일반 컬럼 | 검색 + 표시 |
| 담당 부서 | 일반 컬럼 | 필터 |
| 작업일지 필요 여부 | options | 회사별로 다름 |
| 검사 필요 여부 | options | 회사별로 다름 |
일반 컬럼: id, tenant_id, process_code, process_name, department
options: {"needs_work_log": true, "needs_inspection": false}
5. 메모칸(options)의 실제 모습: JSON이란?
options에 저장되는 데이터 형식은 JSON입니다.
JSON은 프로그래밍 세계의 "구조화된 메모장"이라고 생각하면 됩니다.
5.1 JSON 기본 문법
{ ← 시작
"키": "값", ← 문자(텍스트)
"이름": "홍길동",
"나이": 30, ← 숫자 (따옴표 없음)
"합격": true, ← 참/거짓 (따옴표 없음)
"메모": null ← 값 없음
} ← 끝
5.2 중첩(nested) — 메모 안의 메모
{
"배송": { ← 배송 관련 정보를 묶음
"주소": "서울 강남구 역삼동",
"수신자": "홍길동",
"연락처": "010-1234-5678"
},
"검사": { ← 검사 관련 정보를 묶음
"결과": "합격",
"검사일": "2026-03-01",
"검사자ID": 5
}
}
5.3 목록(배열) — 여러 개를 나열
{
"선택지": [ ← 대괄호 [ ] = 목록
{"label": "블라인드", "value": "blind"},
{"label": "스크린", "value": "screen"},
{"label": "셔터", "value": "shutter"}
]
}
이 형태는 드롭다운 메뉴의 선택지 목록을 저장할 때 사용합니다.
6. 멀티테넌시란? — 여러 회사가 하나의 시스템을 쓰는 구조
6.1 개념
┌──────────────────────────────────────────────┐
│ SAM 시스템 (하나의 프로그램) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ A회사 │ │ B회사 │ │ C회사 │ │
│ │ tenant=1 │ │ tenant=2 │ │ tenant=3 │ │
│ │ │ │ │ │ │ │
│ │ 블라인드 │ │ 스크린 │ │ 셔터 │ │
│ │ 제조 │ │ 제조 │ │ 제조 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 같은 테이블을 쓰지만, │
│ tenant_id로 데이터가 완전히 분리됨 │
└──────────────────────────────────────────────┘
6.2 tenant_id = 회사 식별 번호
모든 테이블의 모든 행에 tenant_id(회사 번호)가 붙어 있습니다.
"주문" 테이블
id │ tenant_id │ 주문번호 │ 금액 │ options
───────────────────────────────────────────────────────────
1 │ 1 │ ORD-001 │ 50만 │ {"절곡각도": 45} ← A회사 데이터
2 │ 1 │ ORD-002 │ 30만 │ {"절곡각도": 90} ← A회사 데이터
3 │ 2 │ ORD-001 │ 80만 │ {"메시밀도": 18} ← B회사 데이터
4 │ 3 │ ORD-001 │ 40만 │ {"날개간격": 25} ← C회사 데이터
A회사가 로그인하면 → 시스템이 자동으로 tenant_id = 1인 데이터만 보여줌
B회사가 로그인하면 → 시스템이 자동으로 tenant_id = 2인 데이터만 보여줌
A회사는 B회사의 데이터를 절대 볼 수 없습니다. 시스템이 자동으로 차단합니다.
6.3 options + tenant_id = 강력한 조합
이 두 가지가 합쳐지면:
같은 테이블, 같은 컬럼 구조인데
✅ 회사마다 다른 데이터 (tenant_id로 분리)
✅ 회사마다 다른 속성 (options로 유연하게)
✅ 시스템 수정 없이 확장 가능
7. SAM 테이블의 표준 구조
SAM에서 새 테이블을 만들면 항상 이 구조를 따릅니다.
┌─────────────────────────────────────────────────────────────────┐
│ SAM 표준 테이블 구조 │
│ │
│ ① 식별자 │
│ id — 자동 생성 번호 (1, 2, 3...) │
│ tenant_id — 어느 회사의 데이터인지 │
│ │
│ ② 핵심 정보 (검색/정렬/연결에 쓰는 것만) │
│ code — 코드 (중복 불가) │
│ status — 상태 (검색용) │
│ is_active — 사용 여부 │
│ sort_order — 표시 순서 │
│ (+ FK 컬럼들) — 다른 테이블 연결 │
│ │
│ ③ 메모칸 │
│ options — 나머지 전부 (JSON) │
│ │
│ ④ 감사 기록 (자동) │
│ created_by — 누가 만들었나 │
│ updated_by — 누가 수정했나 │
│ deleted_by — 누가 삭제했나 │
│ created_at — 언제 만들었나 │
│ updated_at — 언제 수정했나 │
│ deleted_at — 언제 삭제했나 (휴지통 개념) │
└─────────────────────────────────────────────────────────────────┘
영역별 설명
| 영역 | 역할 | 비유 |
|---|---|---|
| ① 식별자 | "이 데이터가 누구 것인지" 구분 | 우편물의 받는 사람 + 주소 |
| ② 핵심 정보 | 검색, 정렬, 집계에 꼭 필요한 정보 | 엑셀의 고정 열 |
| ③ options | 회사마다 다른 부가 정보 | 엑셀의 "비고" 칸 (자유 서식) |
| ④ 감사 기록 | 언제 누가 뭘 했는지 자동 추적 | CCTV 기록 |
8. 실제 SAM에서 options를 쓰는 테이블들 (22개)
현재 SAM에서 options 메모칸을 사용하는 주요 테이블입니다.
| 테이블 | 한글명 | options에 저장하는 정보 예시 |
|---|---|---|
orders |
주문 | 배송지, 수신자, 연락처, 담당자 |
quotes |
견적 | 견적 요약, 비용 항목, 가격 조정 |
receivings |
입고 | 제조사, 검사 결과, 검사일 |
work_orders |
작업지시 | 절곡 정보 (bending_info) |
work_order_items |
작업지시 항목 | 작업 결과, 양품/불량 수량, LOT번호 |
processes |
공정 | 작업일지 필요 여부, 검사 필요 여부 |
order_nodes |
주문 노드 | 위치, 구역, 층, 실 (트리 구조) |
products |
제품 | 동적 옵션 (라벨, 값, 단위) |
items |
품목 | 품목별 동적 속성 |
materials |
자재 | 자재 추가 속성 |
menus |
메뉴 | 섹션, 메뉴 타입, 필요 권한 |
users |
사용자 | 개인 설정/환경설정 |
tenants |
회사(테넌트) | 회사 규모, 업종 |
document_template_section_fields |
문서 양식 필드 | 선택지 목록, API 경로 |
item_fields |
품목 필드 정의 | 필드별 세부 설정 |
9. 자주 묻는 질문 (FAQ)
Q1. options에 넣으면 검색이 안 되나요?
아닙니다. MySQL 8.0은 JSON 내부도 검색할 수 있습니다.
일반 컬럼 검색: "상태가 '완료'인 주문 찾아줘" → 매우 빠름
options 검색: "제조사가 '삼성'인 입고 찾아줘" → 가능하지만 조금 느림
다만, 매일 수천 번 검색하는 정보라면 일반 컬럼으로 승격하는 것이 맞습니다. 가끔 검색하는 정보라면 options로 충분합니다.
Q2. options에 아무 정보나 마음대로 넣을 수 있나요?
기술적으로는 가능하지만, 개발팀 내부에서 어떤 키를 쓸지 미리 약속합니다.
✅ 약속된 키: {"manufacturer": "삼성", "inspection_status": "합격"}
❌ 멋대로: {"asdf": 123, "temp_data": "???"}
코드에서 상수로 정의하여 일관성을 유지합니다.
Q3. 전통적 방식보다 뭐가 좋은 건가요?
| 비교 항목 | 전통적 방식 (열 추가) | SAM 방식 (options JSON) |
|---|---|---|
| 새 정보 추가 시 | 시스템 수정 필요 | 코드만 변경 |
| 다른 회사에 영향 | 있음 (전체 구조 변경) | 없음 |
| 빈칸(null) 낭비 | 많음 | 없음 |
| 검색 속도 | 빠름 | 조금 느림 (충분히 실용적) |
| 유연성 | 낮음 | 높음 |
| 시스템 중단 위험 | 있음 (대형 테이블 수정 시) | 없음 |
Q4. 그럼 모든 정보를 options에 넣으면 되지 않나요?
아닙니다. 핵심 정보는 반드시 일반 컬럼으로 만들어야 합니다.
❌ 나쁜 예: 모든 것을 options에
id │ tenant_id │ options
──────────────────────────────────────────────────────────────
1 │ 1 │ {"주문번호":"ORD-001", "금액":500000, "상태":"완료", ...}
→ 주문번호 검색 느림, 금액 합계 계산 불가, 중복 방지 불가
✅ 좋은 예: 핵심은 컬럼, 부가는 options
id │ tenant_id │ order_number │ amount │ status │ options
──────────────────────────────────────────────────────────────
1 │ 1 │ ORD-001 │ 500000 │ 완료 │ {"배송지":"서울..."}
→ 검색 빠름, 합계 가능, 중복 방지 가능, 부가 정보도 유연
Q5. options 데이터는 화면에서 어떻게 보이나요?
사용자 화면에서는 options 안에 있는지, 일반 컬럼인지 구분할 수 없습니다. 프로그램이 자동으로 꺼내서 보여줍니다.
화면에 보이는 모습:
┌─────────────────────────────────┐
│ 입고 상세 정보 │
│ │
│ 품목: SUS304 스틸 │ ← 일반 컬럼
│ 수량: 100개 │ ← 일반 컬럼
│ 입고일: 2026-03-01 │ ← 일반 컬럼
│ 제조사: 삼성전자 │ ← options에서 꺼냄
│ 검사결과: 합격 │ ← options에서 꺼냄
│ 검사일: 2026-03-01 │ ← options에서 꺼냄
└─────────────────────────────────┘
사용자는 어디에 저장되어 있는지 알 필요 없음!
10. 한 장 요약
┌─────────────────────────────────────────────────────────────────┐
│ │
│ SAM 테이블 설계 = "핵심만 컬럼, 나머진 메모칸(options)" │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ tenant_id → 어느 회사 것인지 (자동 격리) │ │
│ │ 핵심 컬럼들 → 검색/정렬/연결/집계에 쓰는 필수 정보 │ │
│ │ options → 나머지 전부 (회사마다 다른 부가 정보) │ │
│ │ 감사 컬럼들 → 누가/언제 만들고/수정하고/삭제했는지 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 이렇게 하면: │
│ ✅ 회사 추가해도 테이블 구조 안 바꿈 │
│ ✅ 새 정보 추가해도 시스템 수정 최소화 │
│ ✅ 회사마다 다른 정보를 유연하게 저장 │
│ ✅ 데이터 보안 (회사 간 완전 분리) │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 문서
| 문서 | 설명 | 대상 |
|---|---|---|
| options-column-policy.md | 개발자용 상세 정책 (코드 규칙, 마이그레이션 패턴) | 개발자 |
| database/README.md | DB 스키마 전체 현황 (220개 모델) | 개발자 |
| PROJECT_DEVELOPMENT_POLICY.md | 개발 공통 정책 (테이블 생성 절차) | 개발자 |
| system/overview.md | SAM 시스템 전체 아키텍처 | 전체 |
최종 업데이트: 2026-03-02