한지민 닮은, 파도 치는 바다 위 절벽, 한복을 입은 여성 무사가 사인검, 옷자랏이 바람에 날리는, 카메라를 보다, 얼굴 포커스

사인검 (四寅劍 / Sain-geom)
곡도 (曲刀 / Gok-do)
신칼 (神칼 / Shin-kal)

석장 (Seuk-jang): 전통적 지팡이 형태의 의식 도구
쌍검 (Ssang-geum): 역사적 의미를 지닌 쌍검 (쌍날 검)
각궁 (Gak-gung): 전통 한국식 각궁 (목각궁)

한지민 닮은, 파도 치는 바다 위 절벽, 한복을 입은 여성 무사가 석장을 들고 있다, 옷자랏이 바람에 날리는 , 카메라를 보다, 얼굴 포커스

휘두른다

TestRail의 핵심 엔터티(프로젝트, 테스트 케이스, 실행, 결과 등) 간의 관계를 정의하는 관계형 모델의 구조.

사용자가 유연하게 필드를 추가할 수 있는 커스텀 필드 기능이 MariaDB의 동적 컬럼(Dynamic Columns)과 같은 고급 기능을 통해 어떻게 구현되었는지에 대한 가설.

MariaDB 환경에서 최적의 성능과 데이터 무결성을 보장하기 위한 문자 집합, 콜레이션 및 스토리지 엔진에 대한 권장 사항.

성공적인 데이터베이스 관리 및 재해 복구를 위한 필수적인 백업 전략.

이 보고서는 시스템 관리자, DevOps 엔지니어, 또는 고급 QA 전문가와 같이 TestRail 데이터의 내부 구조를 이해하고 직접 관리해야 하는 기술 전문가를 대상으로 합니다.

주요 엔터티 및 관계 요약

엔터티 설명 기본 키 (PK) 핵심 관계
projects 최상위 컨테이너. 모든 작업의 범위 설정. id -
suites 테스트 케이스의 그룹. 프로젝트 내에서 관리. id projects.id (1:N)
sections 테스트 케이스를 구조화하기 위한 계층적 폴더. id projects.id, suites.id, sections.id (self-referencing)
cases 개별 테스트 케이스 정의. id projects.id, suites.id, sections.id
runs 특정 테스트 스위트에서 테스트를 실행하는 인스턴스. id projects.id, suites.id, plans.id (N:1)
results 테스트 케이스 실행의 결과 기록. id runs.id, cases.id, users.id
plans 여러 테스트 실행을 그룹화하는 상위 개념. id projects.id, milestones.id (N:1)
milestones 프로젝트의 주요 이정표 또는 릴리스. id projects.id
users TestRail 사용자 계정. id results.assignedto_id 등

TestRail의 모든 작업은 프로젝트(projects) 내에서 이루어집니다. 프로젝트는 고유한 ID, 이름 및 공지사항을 가집니다. 테스트 스위트(

suites)는 프로젝트에 속하며, 테스트 케이스의 컨테이너 역할을 합니다. 스위트는 이름, 설명, 그리고 마스터 스위트 또는 베이스라인 스위트인지 여부를 나타내는 플래그를 포함합니다.

projects 테이블

id (INT UNSIGNED): 프로젝트의 고유 식별자. 기본 키.

name (VARCHAR(255)): 프로젝트 이름.

announcement (TEXT): 프로젝트 공지사항.

suite_mode (TINYINT): 프로젝트의 스위트 모드(단일 스위트, 여러 스위트 등).

is_completed (TINYINT(1)): 프로젝트 완료 여부.

suites 테이블

id (INT UNSIGNED): 스위트의 고유 식별자. 기본 키.

project_id (INT UNSIGNED): 스위트가 속한 프로젝트의 ID. projects 테이블의 id에 대한 외래 키.

name (VARCHAR(255)): 스위트 이름.

description (TEXT): 스위트 설명.

is_master (TINYINT(1)): 마스터 스위트 여부.

is_baseline (TINYINT(1)): 베이스라인 스위트 여부.

2.2 테스트 섹션 및 케이스
섹션(sections)은 테스트 케이스를 기능적 영역별로 계층적으로 그룹화하는 데 사용됩니다. 섹션은 부모-자식 관계를 가질 수 있어, 복잡한 트리 구조를 형성할 수 있습니다. 각 테스트 케이스(

cases)는 특정 섹션과 스위트에 속하며, 고유한 제목, 생성 및 수정 날짜 등의 메타데이터를 포함합니다.

sections 테이블

id (INT UNSIGNED): 섹션의 고유 식별자. 기본 키.

project_id (INT UNSIGNED): 섹션이 속한 프로젝트의 ID.

suite_id (INT UNSIGNED): 섹션이 속한 스위트의 ID.

parent_id (INT UNSIGNED): 상위 섹션의 ID. 계층 구조를 위해 자기 참조 외래 키로 사용됩니다.

name (VARCHAR(255)): 섹션 이름.

description (TEXT): 섹션 설명.

cases 테이블

id (INT UNSIGNED): 테스트 케이스의 고유 식별자. 기본 키.

project_id (INT UNSIGNED): 케이스가 속한 프로젝트의 ID.

suite_id (INT UNSIGNED): 케이스가 속한 스위트의 ID.

section_id (INT UNSIGNED): 케이스가 속한 섹션의 ID. sections 테이블의 id에 대한 외래 키.

title (VARCHAR(255)): 테스트 케이스의 제목.

created_by (INT UNSIGNED): 케이스를 생성한 사용자의 ID.

created_on (TIMESTAMP): 생성 날짜.

updated_by (INT UNSIGNED): 마지막으로 수정한 사용자의 ID.

updated_on (TIMESTAMP): 마지막 수정 날짜.

refs (VARCHAR(255)): 외부 참조(예: 요구사항 ID) 목록.

2.3 테스트 실행 및 결과
테스트 실행(runs)은 특정 스위트의 테스트 케이스 집합을 실행하는 개념입니다. 실행은 마일스톤, 담당자, 그리고 이름과 설명을 가집니다. 테스트 결과(

results)는 개별 테스트 케이스 실행의 결과를 기록합니다. API는 테스트 실행에 대해

passed_count, failed_count와 같은 필드를 반환하지만, 이는 데이터베이스에 직접 저장된 컬럼이 아니라 애플리케이션 계층에서 집계 쿼리를 통해 동적으로 계산된 값입니다. 이러한 설계는 데이터 중복을 방지하고, 단일 테이블인

results에서 모든 결과를 관리함으로써 데이터 무결성을 보장하는 데 매우 효과적입니다. 만약 이러한 계산된 값이 별도의 컬럼에 저장된다면, 새로운 결과가 추가되거나 기존 결과가 변경될 때마다 관련 실행 및 계획 테이블에 대한 복잡한 연쇄 업데이트가 필요하여 성능 저하 및 오류 발생 가능성이 높아질 수 있습니다.

runs 테이블

id (INT UNSIGNED): 테스트 실행의 고유 식별자. 기본 키.

project_id (INT UNSIGNED): 실행이 속한 프로젝트의 ID.

suite_id (INT UNSIGNED): 실행이 기반한 스위트의 ID.

plan_id (INT UNSIGNED): 실행이 속한 계획의 ID.

milestone_id (INT UNSIGNED): 실행이 속한 마일스톤의 ID.

assignedto_id (INT UNSIGNED): 담당자 ID.

name (VARCHAR(255)): 실행 이름.

description (TEXT): 실행 설명.

is_completed (TINYINT(1)): 완료 여부.

created_on (TIMESTAMP): 생성 날짜.

start_on (TIMESTAMP): 시작 날짜.

due_on (TIMESTAMP): 완료 예정일.

results 테이블

id (INT UNSIGNED): 결과의 고유 식별자. 기본 키.

run_id (INT UNSIGNED): 결과가 속한 테스트 실행의 ID. runs 테이블의 id에 대한 외래 키.

case_id (INT UNSIGNED): 결과가 기록된 테스트 케이스의 ID.

status_id (TINYINT): 결과 상태 (예: 1=Passed, 5=Failed).

assignedto_id (INT UNSIGNED): 결과에 대한 담당자 ID.

comment (TEXT): 결과에 대한 코멘트.

version (VARCHAR(255)): 테스트된 버전.

created_on (TIMESTAMP): 생성 날짜.

2.4 계획 및 마일스톤
테스트 계획(plans)은 여러 테스트 실행을 그룹화하고 관리하는 데 사용됩니다. 마일스톤(

milestones)은 프로젝트의 중요한 이정표를 나타냅니다. 실행 및 계획은 마일스톤과 연결될 수 있습니다.

plans 테이블

id (INT UNSIGNED): 계획의 고유 식별자. 기본 키.

project_id (INT UNSIGNED): 계획이 속한 프로젝트의 ID.

milestone_id (INT UNSIGNED): 연결된 마일스톤의 ID.

name (VARCHAR(255)): 계획 이름.

description (TEXT): 계획 설명.

is_completed (TINYINT(1)): 완료 여부.

milestones 테이블

id (INT UNSIGNED): 마일스톤의 고유 식별자. 기본 키.

project_id (INT UNSIGNED): 마일스톤이 속한 프로젝트의 ID.

name (VARCHAR(255)): 마일스톤 이름.

description (TEXT): 마일스톤 설명.

start_on (TIMESTAMP): 시작 날짜.

due_on (TIMESTAMP): 완료 예정일.

2.5 사용자 및 역할
사용자(users) 테이블은 애플리케이션 내의 모든 사용자를 관리합니다. created_by, updated_by, assignedto_id와 같은 필드는 다른 테이블에서 사용자 엔터티를 참조하는 외래 키 역할을 합니다.

users 테이블

id (INT UNSIGNED): 사용자의 고유 식별자. 기본 키.

name (VARCHAR(255)): 사용자 이름.

email (VARCHAR(255)): 이메일 주소.

is_active (TINYINT(1)): 계정 활성화 여부.

  1. 유연한 데이터 모델: 커스텀 필드 및 동적 데이터
    3.1 유연성의 필요성
    TestRail은 1.3 버전부터 커스텀 필드 기능을 도입하여 사용자가 테스트 케이스 및 테스트 결과에 자신만의 필드를 추가할 수 있도록 했습니다. 이러한 유연성은 고정된 스키마로는 구현하기 어렵습니다. 커스텀 필드를 추가할 때마다 데이터베이스 스키마를 변경하는 것은 비효율적이며, 대규모 애플리케이션에서는 실현 불가능한 방법입니다.

따라서, 커스텀 필드를 관리하기 위해 별도의 메타데이터 테이블이 필요합니다.

custom_fields 테이블

id (INT UNSIGNED): 커스텀 필드 ID. 기본 키.

name (VARCHAR(255)): 필드의 내부 이름.

label (VARCHAR(255)): UI에 표시될 라벨.

type_id (INT): 필드 유형(예: 드롭다운, 텍스트, 날짜 등).

context_id (INT): 필드가 적용되는 컨텍스트(예: 케이스, 결과).

custom_field_options 테이블

id (INT UNSIGNED): 옵션의 고유 ID. 기본 키.

field_id (INT UNSIGNED): 연결된 커스텀 필드의 ID. custom_fields.id에 대한 외래 키.

project_id (INT UNSIGNED): 프로젝트 ID. projects.id에 대한 외래 키. 특정 프로젝트에 국한된 옵션을 관리합니다.

value (VARCHAR(255)): 옵션 값.

3.2 MariaDB 동적 컬럼의 역할
커스텀 필드 자체의 정의를 저장하는 것 외에, 가장 중요한 문제는 실제 데이터를 어디에 저장하느냐입니다. 전통적인 접근 방식은 EAV(Entity-Attribute-Value) 모델을 사용하는 것이지만, 이는 조인 쿼리를 복잡하게 만들고 성능 문제를 야기할 수 있습니다.

TestRail이 MariaDB를 지원하고 Docker 환경에서 최신 MariaDB 이미지를 사용하는 점을 고려할 때, MariaDB의 고유한 기능인 동적 컬럼(Dynamic Columns)을 활용했을 가능성이 높습니다. 이 기능은 하나의

BLOB 컬럼 내에 키-값 쌍을 유연하게 저장할 수 있게 해줍니다.

따라서, cases 및 results 테이블에는 모든 커스텀 필드 데이터를 저장하기 위한 custom_data라는 추가적인 BLOB 컬럼이 존재할 것으로 추정됩니다. 이 모델은 스키마 변경 없이 무제한의 커스텀 필드를 지원하며, VIRTUAL 컬럼을 사용하여 특정 커스텀 필드에 인덱스를 생성함으로써 효율적인 검색 성능을 유지할 수 있습니다. 이러한 설계는 TestRail이 단순히 MySQL의 대안으로서 MariaDB를 선택한 것이 아니라, MariaDB가 제공하는 독점적인 기능을 전략적으로 활용하여 애플리케이션의 핵심 기능을 구현했음을 의미합니다.

  1. MariaDB 스키마 구현 및 최적화
    4.1 문자 집합, 콜레이션 및 국제화
    MariaDB에서 문자 데이터의 저장 및 정렬을 올바르게 처리하기 위해 적절한 문자 집합(character set)과 콜레이션(collation)을 설정하는 것이 중요합니다. 기본값인

latin1은 한글, 이모지 등 다중 바이트 문자를 올바르게 처리하지 못할 수 있습니다. 따라서, 데이터베이스 생성 시

utf8mb4 문자 집합과 utf8mb4_unicode_ci 콜레이션을 명시적으로 설정하여 국제화 및 특수 문자 지원을 확보해야 합니다. 이는 데이터 손상 없이 모든 언어의 데이터를 완벽하게 저장하고 비교할 수 있게 합니다.

4.2 스토리지 엔진 및 성능
MariaDB는 다양한 스토리지 엔진을 지원하지만, TestRail과 같은 트랜잭션 중심의 애플리케이션에는 InnoDB 엔진이 최적의 선택입니다.

InnoDB는 ACID(원자성, 일관성, 고립성, 내구성) 원칙을 준수하며, 행 수준 잠금, 외래 키 지원 및 트랜잭션 처리를 제공하여 데이터 무결성과 신뢰성을 보장합니다. 보고서의 모든 테이블은 ENGINE=InnoDB를 사용하여 생성됩니다.

4.3 트랜잭션 무결성 및 데이터 일관성
테이블 간의 논리적 관계를 유지하기 위해 FOREIGN KEY 제약 조건을 명시적으로 정의하는 것이 필수적입니다. 이는 참조되는 레코드가 삭제될 때 발생하는 고아(orphaned) 레코드의 생성을 방지하고, 데이터베이스 수준에서 관계형 무결성을 강제합니다. 예를 들어, runs 테이블의 project_id 컬럼은 projects 테이블의 id 컬럼을 참조하는 외래 키로 정의되어야 합니다.

  1. 고급 데이터베이스 관리 및 통합
    5.1 3단계 백업 전략의 중요성
    TestRail의 공식 문서에 따르면, 완전한 백업은 세 가지 핵심 요소로 구성됩니다: 데이터베이스, 설치 파일, 그리고 첨부 파일 및 보고서. Docker 구성에서도 이 세 가지 요소는 각각

testrail_mysql, testrail_root, testrail_opt라는 별도의 볼륨으로 관리됩니다.

따라서 데이터베이스 백업을 위해 mysqldump로 SQL 파일을 생성하는 것만으로는 불충분합니다. 설치 디렉토리의 파일들, 특히 데이터베이스 연결 정보가 담긴 config.php 파일과 첨부 파일, 보고서가 저장된 디렉토리를 함께 백업해야 완전한 복원이 가능합니다.

5.2 직접 SQL 쿼리를 위한 모범 사례
TestRail 데이터베이스에 대한 직접적인 데이터 조작(예: UPDATE, DELETE, INSERT)은 애플리케이션의 내부 로직과 데이터 무결성을 훼손할 위험이 있으므로 강력히 권장되지 않습니다. 그러나, 데이터 분석 및 사용자 정의 보고서 생성을 위해

SELECT 쿼리를 사용하는 것은 매우 유용합니다. 재구축된 스키마를 기반으로, 사용자는 다음과 같은 보고서를 직접 생성할 수 있습니다.

프로젝트별 테스트 케이스 상태 분석: cases 테이블과 results 테이블을 조인하여 특정 프로젝트의 모든 테스트 케이스에 대한 최신 상태를 집계합니다.

사용자별 작업 부하: users 테이블과 runs 또는 results 테이블을 조인하여 특정 사용자가 담당하는 테스트 실행 및 결과 수를 계산합니다.

마일스톤 진행률: milestones 테이블을 runs 테이블과 조인하여 각 마일스톤에 속한 테스트 실행의 완료 상태를 파악합니다.

5.3 데이터베이스 스키마와 API의 관계
TestRail은 복잡한 데이터 조작을 API 계층에 캡슐화합니다. 예를 들어,

add_result_for_case API 호출은 테스트 케이스 ID와 실행 ID, 그리고 결과를 인수로 받아, 데이터베이스의 results 테이블에 새 레코드를 안전하게 추가하는 애플리케이션 로직을 실행합니다. 또한, API는

passed_count, failed_count와 같은 집계된 데이터를 즉시 계산하여 반환함으로써 데이터베이스에 직접 접근하지 않고도 최신 상태를 파악할 수 있게 합니다. 이는 TestRail의 데이터 모델이 애플리케이션 로직과 긴밀하게 통합되어, 외부에서 직접적인 조작을 최소화하는 안전하고 안정적인 구조를 갖추고 있음을 의미합니다.

부록 A: 완전한 MariaDB 스키마 생성 스크립트
SQL

-- TestRail MariaDB 데이터베이스 생성 스크립트

-- 이 스크립트는 TestRail의 공식 문서, API 응답, 그리고 Docker 구성 파일을
-- 분석하여 재구축한 시스템 테이블 스키마를 기반으로 합니다.
-- 모든 테이블은 InnoDB 엔진과 utf8mb4 문자 집합을 사용합니다.

-- 이는 트랜잭션 무결성과 국제화된 문자 지원을 보장합니다.

-- 참고: 이 스크립트는 백업 및 데이터 분석 목적으로 사용되어야 하며,
-- 데이터베이스의 직접적인 조작은 데이터 무결성을 훼손할 수 있으므로 권장되지 않습니다.
-- 특히 custom_data 컬럼은 MariaDB의 동적 컬럼 기능을 활용하여 구현되었을 가능성이 높습니다.

-- 데이터베이스 생성 및 문자 집합/콜레이션 설정
CREATE DATABASE IF NOT EXISTS testrail

DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_unicode_ci;

USE testrail;

-- users 테이블: 사용자 정보 저장
CREATE TABLE users (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL UNIQUE,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- projects 테이블: 프로젝트 정보 저장
CREATE TABLE projects (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`announcement` TEXT,
`suite_mode` TINYINT NOT NULL DEFAULT 1,
`is_completed` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- suites 테이블: 테스트 스위트 정보 저장
CREATE TABLE suites (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`project_id` INT UNSIGNED NOT NULL,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`is_master` TINYINT(1) NOT NULL DEFAULT 0,
`is_baseline` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- sections 테이블: 테스트 케이스 섹션(폴더) 정보 저장
CREATE TABLE sections (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`project_id` INT UNSIGNED NOT NULL,
`suite_id` INT UNSIGNED NOT NULL,
`parent_id` INT UNSIGNED,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
PRIMARY KEY (`id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`suite_id`) REFERENCES `suites`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `sections`(`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- milestones 테이블: 마일스톤 정보 저장
CREATE TABLE milestones (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`project_id` INT UNSIGNED NOT NULL,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`start_on` TIMESTAMP NULL,
`due_on` TIMESTAMP NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- plans 테이블: 테스트 계획 정보 저장
CREATE TABLE plans (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`project_id` INT UNSIGNED NOT NULL,
`milestone_id` INT UNSIGNED,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`is_completed` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON DELETE SET NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- runs 테이블: 테스트 실행 정보 저장
CREATE TABLE runs (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`project_id` INT UNSIGNED NOT NULL,
`suite_id` INT UNSIGNED NOT NULL,
`plan_id` INT UNSIGNED,
`milestone_id` INT UNSIGNED,
`assignedto_id` INT UNSIGNED,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`is_completed` TINYINT(1) NOT NULL DEFAULT 0,
`created_on` TIMESTAMP NOT NULL,
`start_on` TIMESTAMP NULL,
`due_on` TIMESTAMP NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`suite_id`) REFERENCES `suites`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`assignedto_id`) REFERENCES `users`(`id`) ON DELETE SET NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- cases 테이블: 테스트 케이스 정보 저장
CREATE TABLE cases (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`project_id` INT UNSIGNED NOT NULL,
`suite_id` INT UNSIGNED NOT NULL,
`section_id` INT UNSIGNED NOT NULL,
`title` VARCHAR(255) NOT NULL,
`created_by` INT UNSIGNED NOT NULL,
`created_on` TIMESTAMP NOT NULL,
`updated_by` INT UNSIGNED NOT NULL,
`updated_on` TIMESTAMP NOT NULL,
`refs` VARCHAR(255),
`estimate` VARCHAR(50),
`type_id` INT,
`priority_id` INT,
`milestone_id` INT UNSIGNED,
`custom_data` MEDIUMBLOB, -- MariaDB 동적 컬럼으로 추정되는 필드
PRIMARY KEY (`id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`suite_id`) REFERENCES `suites`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`section_id`) REFERENCES `sections`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`updated_by`) REFERENCES `users`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON DELETE SET NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- results 테이블: 테스트 결과 정보 저장
CREATE TABLE results (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`run_id` INT UNSIGNED NOT NULL,
`case_id` INT UNSIGNED NOT NULL,
`status_id` TINYINT NOT NULL,
`assignedto_id` INT UNSIGNED,
`comment` TEXT,
`version` VARCHAR(255),
`created_on` TIMESTAMP NOT NULL,
`defects` VARCHAR(255),
`custom_data` MEDIUMBLOB, -- MariaDB 동적 컬럼으로 추정되는 필드
PRIMARY KEY (`id`),
FOREIGN KEY (`run_id`) REFERENCES `runs`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`case_id`) REFERENCES `cases`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`assignedto_id`) REFERENCES `users`(`id`) ON DELETE SET NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- custom_fields 테이블: 커스텀 필드 메타데이터
CREATE TABLE custom_fields (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`label` VARCHAR(255) NOT NULL,
`type_id` INT NOT NULL,
`context_id` INT NOT NULL,
PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- custom_field_options 테이블: 커스텀 필드 옵션
CREATE TABLE custom_field_options (

`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`field_id` INT UNSIGNED NOT NULL,
`project_id` INT UNSIGNED,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`field_id`) REFERENCES `custom_fields`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

https://www.jannikbuschke.de/blog/git-submodules/

아래는 Jannik Buschke 블로그 글의 내용을 요약한 한국어 정리입니다.


여러 모노레포를 연결하는 Git 서브모듈 활용법

요약: Git 서브모듈은 여러 모노레포 간에 공유 모듈을 함께 개발할 수 있도록 해주는 강력하고 실용적인 방법입니다. "레포 안의 레포", 그것도 여러 겹으로 구성할 수 있어 매우 유용합니다!


1. 소개: 모노레포의 장점과 한계

모노레포(Monorepo)는 복잡한 앱 개발을 단순화해줍니다. 예를 들어, 다음과 같은 구조로 구성할 수 있습니다:

/app-repository
  /app1
  /app2
  /shared-module
  • app1app2는 비슷한 도메인을 다루며, shared-module은 공통 유틸리티를 담고 있습니다.
  • 이 구조에서는 shared-module소스 코드 수준에서 바로 연결할 수 있어, 개발 중인 기능을 즉시 사용하고 API 설계에 대한 빠른 피드백을 받을 수 있습니다.

한계점

  • shared-moduleapp1, app2 외에도 다른 앱(예: app3)에서도 유용할 수 있습니다.
  • 하지만 app3다른 저장소(repository)에 있고, 다른 조직이나 커뮤니티에서 관리한다면, shared-module을 공유하기 어렵습니다.
  • shared-module의 빌드 결과물(예: npm 패키지)을 배포하면 app3에서 사용할 수는 있지만, 함께 개발하거나 실시간으로 기여하기는 어렵습니다.

2. 해결책: Git 서브모듈(Submodules)

Git 서브모듈을 사용하면, 하나의 모듈이 여러 모노레포에 동시에 포함되어 개발될 수 있습니다.

구조 예시

  1. 먼저 shared-module을 독립된 저장소로 분리합니다:

    /module-repository
      /shared-module
  2. 이 저장소를 다른 모노레포에 서브모듈로 추가합니다:

    /mono-repository1
      /app1
      /app2
      /generic-module-as-a-git-submodule  ← 서브모듈
    
    /mono-repository2
      /app3
      /generic-module-as-a-git-submodule  ← 동일한 서브모듈
  • 서브모듈은 특정 커밋(commit)을 가리키는 링크일 뿐, 소스 코드 자체를 포함하지 않습니다.
  • 하지만 로컬에서 클론할 때, 서브모듈의 소스도 함께 체크아웃되므로, 마치 하나의 큰 저장소처럼 작업할 수 있습니다.

3. 서브모듈 작업 방법

  • 서브모듈 내부에서 명령어 실행: 서브모듈 저장소에 영향을 줍니다.
  • 부모 저장소에서 명령어 실행: 부모 저장소에 영향을 줍니다.
  • 두 저장소는 저장소 차원에서는 분리되어 있지만, 로컬 파일 시스템에서는 소스 코드가 함께 위치하므로 통합된 개발이 가능합니다.

서브모듈 추가하기

git submodule add <저장소-URL>
  • .gitmodules 파일이 생성되며, 서브모듈의 경로와 URL을 기록합니다.
  • 예:

    [submodule "my-module"]
      path = my-module
      url = https://github.com/jannikbuschke/my-module.git

서브모듈이 포함된 저장소 클론하기

git clone <저장소-URL> --recursive
  • --recursive 플래그를 꼭 사용해야 서브모듈도 함께 체크아웃됩니다.
  • 생략하면 서브모듈 폴더는 비어 있게 되며, 나중에 초기화하는 과정이 번거롭습니다.

서브모듈의 새로운 커밋 반영하기

  • 서브모듈에서 커밋을 하면, 부모 저장소는 자동으로 변경되지 않습니다.
  • 부모 저장소에서 다음과 같이 명시적으로 업데이트해야 합니다:

    git add <서브모듈-경로>
    git commit -m "Update submodule to latest commit"
  • git statusgit diff를 통해 어떤 커밋을 가리켜야 할지 확인할 수 있습니다.

4. 주의사항 및 팁

  • 서브모듈 커밋은 반드시 푸시해야 함:
    서브모듈에서 커밋했지만 푸시하지 않으면, 그 커밋은 로컬에만 존재합니다.
    이 상태에서 부모 저장소를 푸시하면, 다른 개발자나 CI/CD가 존재하지 않는 커밋을 체크아웃하려 해서 실패할 수 있습니다.
  • --recursive 플래그 필수:
    클론 시 --recursive를 빼먹지 마세요. 이후 수동 초기화는 다소 번거롭습니다.
  • 서브모듈 진입 시 브랜치 체크아웃:
    서브모듈은 기본적으로 detached HEAD 상태(커밋 해시 참조)로 시작합니다.
    작업하려면 git checkout main 또는 git switch - 등으로 브랜치를 명시적으로 체크아웃해야 합니다.
  • 서브모듈 경로 변경/삭제는 까다로움:
    이름 변경이나 삭제는 .gitmodules 파일을 직접 수정하거나, 새 위치에 클론하는 것이 가장 안전합니다.

5. 결론

  • Git 서브모듈은 종종 "사용하기 어려움", "문서 부족", "이상한 동작" 등으로 평가절하되지만, 실제로는 학습 곡선을 넘기면 매우 강력한 도구입니다.
  • 약간의 주의사항이 있지만, 그로 인한 이점이 훨씬 큽니다.
  • 여러 앱을 개발하면서 코드를 공유하거나, 오픈소스 프로젝트를 사용하면서 동시에 기여하고 싶다면, Git 서브모듈을 꼭 한번 시도해보세요!

핵심 메시지: 서브모듈은 "여러 모노레포에서 하나의 모듈을 함께 개발"하는 이상적인 솔루션입니다.

제목 : 배웅

미안한 사람에게 세상의 끝에서 나에게 향하는 약도가 쥐어 주었다.

한없이 사랑했고

한없이 그리워해도

당신은 나에게

천천히

기억이 잊어질 때 쯤

다시 만나길

다정하게

새겨진 시간 속에

한없이 기다릴 테니

천천히

행복한 시간들을

당신에게 천국이길

나는 기억해도

당신으 모두 잊기를

소박한 살이 였기에

가려진 인연이기에

이젠 문을 열고

고향으로

소박하게 살아온 삶이

행복한 삶이 였다.

사랑, 사랑, 사랑한다.