Files
sam-docs/dev/standards/options-column-policy.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

11 KiB

JSON options 컬럼 표준 정책

작성일: 2026-02-27 상태: 설계 확정 적용 범위: 모든 비즈니스 테이블 신규 생성 및 확장


1. 개요

1.1 목적

멀티테넌시 환경에서 테넌트별 스키마 변경(ALTER TABLE) 없이 유연하게 속성을 확장하기 위한 options JSON 컬럼의 표준 사용 정책을 정의한다.

1.2 핵심 원칙

FK/조인키만 테이블 컬럼으로 추가한다. 나머지 속성은 options JSON에 저장한다.


2. 컬럼 분류 기준

2.1 전용 컬럼으로 만들어야 하는 경우

✅ FK/조인키 (다른 테이블 참조)
✅ WHERE 조건에 자주 사용되는 필터 (status, is_active)
✅ ORDER BY 정렬 대상 (sort_order, created_at)
✅ UNIQUE 제약이 필요한 필드 (code, number)
✅ INDEX가 필요한 고빈도 조회 필드
✅ 집계/통계 대상 (SUM, AVG 등)

2.2 options JSON에 넣어야 하는 경우

✅ 테넌트별로 다를 수 있는 확장 속성
✅ 선택적(nullable) 부가 정보
✅ 구조가 유동적인 데이터 (키-값 쌍이 변할 수 있음)
✅ 드롭다운 선택지 목록
✅ 중첩 구조 (배열, 객체)
✅ 이력/스냅샷성 데이터 (취소 사유, 변환 전 원본 등)

2.3 판단 흐름도

새 필드 추가 필요?
  ├── FK/조인? ──────────────────→ 전용 컬럼
  ├── WHERE/ORDER BY 필수? ──────→ 전용 컬럼
  ├── UNIQUE 제약 필요? ─────────→ 전용 컬럼
  ├── 집계(SUM/AVG) 대상? ──────→ 전용 컬럼
  └── 그 외 ─────────────────────→ options JSON

참고: MySQL 8.0은 options->key 경로로 JSON 내부 필드를 쿼리할 수 있다. 저빈도 필터링은 JSON 경로 쿼리로 충분하다.


3. 마이그레이션 표준

3.1 신규 테이블 생성 시

모든 비즈니스 테이블에 options 컬럼을 기본 포함한다.

Schema::create('example_table', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');

    // --- 전용 컬럼 (FK, 필터, 정렬, 유니크) ---
    $table->string('code', 50)->comment('코드');
    $table->string('status', 20)->default('draft')->comment('상태');
    $table->boolean('is_active')->default(true)->comment('활성 여부');
    $table->integer('sort_order')->default(0)->comment('정렬 순서');

    // --- options JSON (확장 속성) ---
    $table->json('options')->nullable()->comment('추가 옵션');

    // --- 감사 컬럼 ---
    $table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
    $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
    $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
    $table->timestamps();
    $table->softDeletes();
});

3.2 기존 테이블에 options 추가 시

Schema::table('existing_table', function (Blueprint $table) {
    $table->json('options')->nullable()->after('remark')->comment('추가 옵션');
});

3.3 선언 규칙

항목 표준
컬럼명 options (고정)
타입 json
nullable nullable() (필수)
comment 용도 설명 포함
위치 비즈니스 컬럼 이후, 감사 컬럼 이전

4. 모델 표준

4.1 기본 선언 (필수)

모든 모델에 공통 적용한다.

class ExampleModel extends Model
{
    protected $fillable = [
        // ... 전용 컬럼들
        'options',
        'created_by',
        'updated_by',
        'deleted_by',
    ];

    protected $casts = [
        'options' => 'array',  // ✅ 'array' 통일 (❌ 'json' 사용 금지)
    ];
}

주의: 'array''json' cast의 차이

  • 'array' → PHP 배열로 변환 (표준)
  • 'json' → JSON 문자열로 유지
  • 프로젝트 전체에서 'array'로 통일한다.

4.2 헬퍼 메서드 (필수)

options를 사용하는 모든 모델에 아래 2개 메서드를 추가한다.

/**
 * options에서 값 조회 (점표기 지원)
 */
public function getOption(string $key, mixed $default = null): mixed
{
    return data_get($this->options, $key, $default);
}

/**
 * options에 값 설정 (점표기 지원)
 */
public function setOption(string $key, mixed $value): static
{
    $options = $this->options ?? [];
    data_set($options, $key, $value);
    $this->options = $options;

    return $this;
}

data_get/data_set을 사용하는 이유: 중첩 키를 점표기(meta.color)로 접근할 수 있다.

4.3 키 상수 정의 (권장)

options에 3개 이상의 키를 사용하면 상수로 정의한다.

// Options 키 상수
public const OPTION_MANUFACTURER      = 'manufacturer';
public const OPTION_INSPECTION_STATUS = 'inspection_status';
public const OPTION_INSPECTION_DATE   = 'inspection_date';

4.4 Accessor + $appends (API 노출 시)

options 내부 값을 API 응답에 일급 필드로 노출해야 할 때 적용한다.

protected $appends = [
    'manufacturer',
    'inspection_status',
];

public function getManufacturerAttribute(): ?string
{
    return $this->getOption(self::OPTION_MANUFACTURER);
}

public function getInspectionStatusAttribute(): ?string
{
    return $this->getOption(self::OPTION_INSPECTION_STATUS);
}

4.5 구현 수준 가이드

상황에 따라 적절한 수준을 선택한다.

수준 적용 조건 구현 항목
L1 기본 options 키 1~2개, 내부 사용 cast + getOption/setOption
L2 상수 options 키 3개 이상 L1 + OPTION_* 상수
L3 노출 API 응답에 옵션값 포함 L2 + accessor + $appends
L4 쿼리 options 키로 필터/정렬 필요 L3 + JSON 스코프

5. 서비스/컨트롤러 사용 패턴

5.1 저장 시 (Create/Update)

// 서비스에서 options 구성
$options = [];
if ($data['manufacturer'] ?? null) {
    $options[Receiving::OPTION_MANUFACTURER] = $data['manufacturer'];
}
if ($data['inspection_status'] ?? null) {
    $options[Receiving::OPTION_INSPECTION_STATUS] = $data['inspection_status'];
}

Receiving::create([
    'tenant_id' => $tenantId,
    // ... 전용 컬럼
    'options' => $options ?: null,  // 빈 배열이면 null 저장
]);

5.2 수정 시 (기존 options 유지)

// ✅ 올바른 방법: 기존 options를 유지하면서 특정 키만 변경
$model->setOption('cancelled_at', now()->toIso8601String());
$model->setOption('cancelled_by', $userId);
$model->save();

// ❌ 잘못된 방법: options 전체를 덮어씀 (기존 키 소실)
$model->options = ['cancelled_at' => now()];
$model->save();

5.3 조회 시 (읽기)

// 헬퍼 사용 (권장)
$manufacturer = $receiving->getOption('manufacturer');

// 중첩 키 (점표기)
$lotNo = $workOrderItem->getOption('result.lot_no');

// 기본값 지정
$status = $receiving->getOption('inspection_status', '-');

5.4 중첩 키 구조 (도메인 분리)

하나의 options에 여러 도메인 데이터를 저장할 때 최상위 키로 그룹핑한다.

// 작업 결과 + 검사 데이터 + 동적 BOM을 하나의 options에
$item->setOption('result', [
    'good_qty'     => 100,
    'defect_qty'   => 2,
    'lot_no'       => 'LOT-2026-001',
    'completed_at' => now()->toIso8601String(),
]);

$item->setOption('inspection_data', [
    'process_type' => 'bending',
    'inspector_id' => 5,
]);

$item->save();

6. MySQL JSON 경로 쿼리

6.1 기본 필터링

// 단순 키 비교
->where('options->manufacturer', '삼성전자')

// 중첩 키 비교
->where('options->result->worker_id', $workerId)

// NULL 체크
->whereNotNull('options->result')
->whereNull('options->cancelled_at')

6.2 LIKE 검색

->where('options->result->lot_no', 'like', "LOT-2026-%")

6.3 정렬

->orderByDesc('options->result->completed_at')

6.4 JSON 스코프 (L4)

자주 사용하는 JSON 필터는 스코프로 정의한다.

// 모델에 스코프 정의
public function scopeHasResult($query)
{
    return $query->whereNotNull('options->result');
}

public function scopeByProcessType($query, string $type)
{
    return $query->where('options->inspection_data->process_type', $type);
}

// 사용
WorkOrderItem::hasResult()->byProcessType('bending')->get();

6.5 성능 주의사항

⚠️ JSON 경로 쿼리는 인덱스를 사용하지 않는다.
⚠️ 대용량 테이블에서 고빈도 필터링이 필요하면 전용 컬럼으로 승격한다.
⚠️ Generated Column + INDEX로 JSON 필드에 인덱스를 걸 수 있지만, 필요시에만 적용한다.

7. 유사 JSON 컬럼과의 구분

options 외에 용도별로 분화된 JSON 컬럼을 사용한다.

컬럼명 용도 사용 조건
options 범용 확장 속성 기본 (모든 테이블)
attributes EAV 대체 동적 필드값 Item, Product 등 카탈로그성 엔티티
metadata 감사/관계 메타 정보 감사 로그, 이벤트 로그
settings 설정값 알림, 환경 설정
bom BOM 구성품 배열 제품/품목

원칙: 하나의 테이블에 JSON 컬럼이 2개 이상이면, 각 컬럼의 역할을 comment에 명확히 구분한다.


8. 드롭다운 선택지 패턴

필드 정의 테이블에서 선택지 목록을 options에 저장하는 패턴이다.

// 마이그레이션
$table->json('options')->nullable()->comment('드롭다운 옵션 [{label, value}]');

// 저장 구조
[
    {"label": "블라인드", "value": "blind"},
    {"label": "스크린",   "value": "screen"},
    {"label": "셔터",    "value": "shutter"}
]

적용 테이블: item_master_fields, item_fields, category_fields, document_template_section_fields


9. 체크리스트

신규 테이블 생성 시

  • $table->json('options')->nullable()->comment('...') 포함
  • 비즈니스 컬럼 이후, 감사 컬럼 이전 위치
  • 모델에 'options' => 'array' cast 선언
  • 모델에 getOption() / setOption() 헬퍼 추가
  • options 키 3개 이상 시 OPTION_* 상수 정의

options 키 추가 시

  • 기존 options 구조와 키 충돌 없는지 확인
  • 서비스에서 setOption() 사용 (직접 배열 덮어쓰기 금지)
  • API 응답 노출 필요 시 accessor + $appends 추가
  • 고빈도 필터링 필요 시 전용 컬럼 승격 검토

관련 문서

문서 설명
system/database/README.md DB 스키마 현황 및 공통 패턴
guides/PROJECT_DEVELOPMENT_POLICY.md 개발 공통 정책
standards/api-rules.md API 개발 규칙

최종 업데이트: 2026-02-27