분류테스트 플랫폼 아래의 글

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 서브모듈을 꼭 한번 시도해보세요!

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

프로젝트의 요구사항을 고려했을 때, 효율적인 테스트 도구 시스템을 구축하기 위한 데이터베이스 설계 및 주요 로직 단계를 Java, Spring, iBatis, MariaDB 환경에 맞춰 상세히 정리해 드릴게요.


1. 데이터베이스 설계 (MariaDB)

테스트 도구의 다양한 파라미터와 계층 구조를 효율적으로 관리하기 위해 다음 테이블들을 설계할 수 있습니다.

1.1 TEST_PLAN 테이블

  • 설명: 최상위 테스트 계획 정보를 저장합니다.
  • 컬럼:

    • PLAN_ID (BIGINT PK, AUTO\_INCREMENT): 테스트 계획 ID
    • PLAN_NAME (VARCHAR(255) NOT NULL): 테스트 계획 이름
    • DESCRIPTION (TEXT): 테스트 계획 설명
    • CREATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시
    • UPDATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시

1.2 TEST_SCENARIO 테이블

  • 설명: 각 테스트 계획에 속하는 시나리오 정보를 저장합니다.
  • 컬럼:

    • SCENARIO_ID (BIGINT PK, AUTO\_INCREMENT): 테스트 시나리오 ID
    • PLAN_ID (BIGINT FK): 소속 테스트 계획 ID (TEST_PLAN.PLAN_ID 참조)
    • SCENARIO_NAME (VARCHAR(255) NOT NULL): 테스트 시나리오 이름
    • DESCRIPTION (TEXT): 테스트 시나리오 설명
    • CREATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시
    • UPDATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시

1.3 TEST_CASE 테이블

  • 설명: 각 시나리오에 속하는 테스트 케이스 정보를 저장합니다.
  • 컬럼:

    • CASE_ID (BIGINT PK, AUTO\_INCREMENT): 테스트 케이스 ID
    • SCENARIO_ID (BIGINT FK): 소속 테스트 시나리오 ID (TEST_SCENARIO.SCENARIO_ID 참조)
    • CASE_NAME (VARCHAR(255) NOT NULL): 테스트 케이스 이름
    • DESCRIPTION (TEXT): 테스트 케이스 설명
    • CREATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시
    • UPDATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시

1.4 TEST_ACTIVITY 테이블

  • 설명: 각 테스트 케이스에 속하는 개별 테스트 활동(SQL 템플릿 포함) 정보를 저장합니다.
  • 컬럼:

    • ACTIVITY_ID (BIGINT PK, AUTO\_INCREMENT): 테스트 활동 ID
    • CASE_ID (BIGINT FK): 소속 테스트 케이스 ID (TEST_CASE.CASE_ID 참조)
    • ACTIVITY_NAME (VARCHAR(255) NOT NULL): 테스트 활동 이름
    • SQL_TEMPLATE (LONGTEXT NOT NULL): 실행될 SQL 템플릿 (파라미터 포함)
    • CREATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시
    • UPDATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시

1.5 PARAMETER_DEFINITION 테이블

  • 설명: 각 계층(Plan, Scenario, Case, Activity)에서 정의된 파라미터 정보를 저장합니다.
  • 컬럼:

    • PARAM_DEF_ID (BIGINT PK, AUTO\_INCREMENT): 파라미터 정의 ID
    • REFER_ID (BIGINT NOT NULL): 참조하는 엔티티 ID (Plan ID, Scenario ID, Case ID, Activity ID)
    • REFER_TYPE (VARCHAR(20) NOT NULL): 참조하는 엔티티 타입 (PLAN, SCENARIO, CASE, ACTIVITY)
    • PARAM_NAME (VARCHAR(100) NOT NULL): 파라미터 이름 (예: testId, ADD_CONDITION_1, userId)
    • PARAM_TYPE (VARCHAR(50) NOT NULL): 파라미터 타입 (SYSTEM, CONDITIONAL, STATIC, DYNAMIC, SQL_QUERY)
    • PARAM_VALUE (TEXT): 파라미터 값 (고정값, SQL 쿼리, 동적 SQL 등. 타입에 따라 의미 다름)
    • DESCRIPTION (TEXT): 파라미터 설명
    • CREATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시
    • UPDATED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시

1.6 TEST_EXECUTION_RESULT 테이블

  • 설명: 테스트 실행 결과 및 실행된 SQL, 최종 파라미터 값을 저장합니다.
  • 컬럼:

    • EXEC_ID (BIGINT PK, AUTO\_INCREMENT): 실행 결과 ID
    • ACTIVITY_ID (BIGINT FK): 실행된 테스트 활동 ID (TEST_ACTIVITY.ACTIVITY_ID 참조)
    • EXECUTED_SQL (LONGTEXT NOT NULL): 최종 실행된 SQL 문장
    • PARAMETER_VALUES (JSON): 실행 시 사용된 최종 파라미터 값들 (JSON 형태로 저장하여 유연성 확보)
    • EXECUTION_STATUS (VARCHAR(50) NOT NULL): 실행 상태 (SUCCESS, FAILED 등)
    • RESULT_DATA (LONGTEXT): SQL 실행 결과 데이터 (필요시 저장)
    • ERROR_MESSAGE (TEXT): 에러 발생 시 메시지
    • EXECUTED_AT (TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 실행일시

2. 주요 로직 단계별 정리 (Java, Spring, iBatis)

Spring Boot와 MyBatis(iBatis의 후속 프레임워크)를 기반으로 주요 로직 단계를 설명합니다.

2.1. 테스트 계획 수립 및 파라미터 정의 (SQL 템플릿 및 파라미터 정의)

개요:
사용자가 웹 UI를 통해 테스트 계획, 시나리오, 케이스, 액티비티를 정의하고, 각 단계에서 필요한 파라미터들을 설정합니다. 이 정보들은 위에서 설계한 DB 테이블에 저장됩니다.

주요 컴포넌트:

  • Spring REST Controller: 클라이언트의 요청을 받아 테스트 계획, 시나리오, 케이스, 액티비티 및 파라미터 정의를 처리합니다.
  • Service Layer: 비즈니스 로직을 담당하며, DTO를 통해 데이터를 주고받습니다.
  • MyBatis Mapper: DB와 상호작용하여 데이터를 저장하고 조회합니다.

저장 예시 (MyBatis Mapper XML):

<insert id="insertTestPlan" useGeneratedKeys="true" keyProperty="planId">
    INSERT INTO TEST_PLAN (PLAN_NAME, DESCRIPTION)
    VALUES (#{planName}, #{description})
</insert>

<insert id="insertParameterDefinition">
    INSERT INTO PARAMETER_DEFINITION (REFER_ID, REFER_TYPE, PARAM_NAME, PARAM_TYPE, PARAM_VALUE, DESCRIPTION)
    VALUES (#{referId}, #{referType}, #{paramName}, #{paramType}, #{paramValue}, #{description})
</insert>

로직 흐름:

  1. 사용자가 UI를 통해 테스트 계획 생성 및 이름, 설명 입력.
  2. TestPlanController가 요청을 받아 TestPlanService 호출.
  3. TestPlanServiceTestPlanMapper를 통해 TEST_PLAN 테이블에 데이터 삽입.
  4. 유사한 방식으로 TEST_SCENARIO, TEST_CASE, TEST_ACTIVITY 및 각 단계의 PARAMETER_DEFINITION 데이터를 저장. 특히 SQL_TEMPLATETEST_ACTIVITY 테이블에 그대로 저장됩니다.

2.2. 테스트 실행 데이터 생성: 계획 기반 실행 가능한 테스트 세트 생성

개요:
사용자가 특정 테스트 계획을 선택하여 실행을 요청하면, 해당 계획에 속한 모든 시나리오, 케이스, 활동들을 조회하여 실행 가능한 테스트 세트를 구성합니다. 이 단계에서는 실제 SQL 실행은 하지 않고, 실행 준비를 위한 데이터를 로드합니다.

주요 컴포넌트:

  • Spring REST Controller: 테스트 실행 요청을 받습니다.
  • Service Layer: 실행할 테스트 세트를 구성하는 핵심 로직을 포함합니다.
  • MyBatis Mapper: 계층별 정보와 파라미터 정의를 조회합니다.

조회 예시 (MyBatis Mapper XML):

<resultMap id="activityWithParams" type="com.example.testtool.model.TestActivity">
    <id property="activityId" column="ACTIVITY_ID"/>
    <result property="activityName" column="ACTIVITY_NAME"/>
    <result property="sqlTemplate" column="SQL_TEMPLATE"/>
    <collection property="parameters" ofType="com.example.testtool.model.ParameterDefinition">
        <id property="paramDefId" column="PARAM_DEF_ID"/>
        <result property="paramName" column="PARAM_NAME"/>
        <result property="paramType" column="PARAM_TYPE"/>
        <result property="paramValue" column="PARAM_VALUE"/>
        </collection>
</resultMap>

<select id="getActivitiesWithParametersByPlanId" resultMap="activityWithParams">
    SELECT
        ta.ACTIVITY_ID, ta.ACTIVITY_NAME, ta.SQL_TEMPLATE,
        pd.PARAM_DEF_ID, pd.PARAM_NAME, pd.PARAM_TYPE, pd.PARAM_VALUE
    FROM TEST_ACTIVITY ta
    JOIN TEST_CASE tc ON ta.CASE_ID = tc.CASE_ID
    JOIN TEST_SCENARIO ts ON tc.SCENARIO_ID = ts.SCENARIO_ID
    JOIN TEST_PLAN tp ON ts.PLAN_ID = tp.PLAN_ID
    LEFT JOIN PARAMETER_DEFINITION pd ON (pd.REFER_ID = ta.ACTIVITY_ID AND pd.REFER_TYPE = 'ACTIVITY')
                                     OR (pd.REFER_ID = tc.CASE_ID AND pd.REFER_TYPE = 'CASE')
                                     OR (pd.REFER_ID = ts.SCENARIO_ID AND pd.REFER_TYPE = 'SCENARIO')
                                     OR (pd.REFER_ID = tp.PLAN_ID AND pd.REFER_TYPE = 'PLAN')
    WHERE tp.PLAN_ID = #{planId}
    ORDER BY ts.SCENARIO_ID, tc.CASE_ID, ta.ACTIVITY_ID, pd.REFER_TYPE DESC, pd.PARAM_NAME
</select>

로직 흐름:

  1. 사용자가 특정 PLAN_ID로 테스트 실행 요청.
  2. TestExecutionServicegetActivitiesWithParametersByPlanId 같은 매퍼를 호출하여 해당 계획에 속한 모든 Activity와 각 계층(Plan, Scenario, Case, Activity)에 정의된 모든 파라미터를 조회합니다.
  3. 조회된 데이터를 바탕으로 ExecutionJob 또는 TestExecution 객체 리스트를 생성합니다. 각 ExecutionJob은 하나의 TestActivity와 해당 Activity에 적용될 가능성이 있는 모든 상위/자신 계층의 파라미터 정보를 포함합니다.

2.3. 파라미터 바인딩: 계층적 파라미터 우선순위 적용

개요:
이 단계는 가장 중요하며, 조회된 SQL 템플릿에 최종적으로 적용될 파라미터 값을 결정합니다. 우선순위는 Test Activity (Level 1) \> Test Case (Level 2) \> Test Scenario (Level 3) \> Test Plan (Level 4) 순으로 적용됩니다.

주요 컴포넌트:

  • ParameterResolver Service/Utility: 파라미터 우선순위를 처리하고 최종 파라미터 맵을 생성합니다.
  • JdbcTemplate (또는 MyBatis의 동적 SQL 기능): SQL 템플릿에 파라미터를 바인딩합니다.

로직 흐름 (ParameterResolver의 역할):

public Map<String, Object> resolveParameters(Long planId, Long scenarioId, Long caseId, Long activityId) {
    Map<String, Object> finalParameters = new HashMap<>();

    // 1. Level 4: Test Plan 파라미터 로드 및 적용
    loadAndApplyParameters(finalParameters, planId, "PLAN");

    // 2. Level 3: Test Scenario 파라미터 로드 및 적용 (동일 이름 시 덮어쓰기)
    loadAndApplyParameters(finalParameters, scenarioId, "SCENARIO");

    // 3. Level 2: Test Case 파라미터 로드 및 적용 (동일 이름 시 덮어쓰기)
    loadAndApplyParameters(finalParameters, caseId, "CASE");

    // 4. Level 1: Test Activity 파라미터 로드 및 적용 (동일 이름 시 덮어쓰기)
    loadAndApplyParameters(finalParameters, activityId, "ACTIVITY");

    // 5. 시스템 파라미터 추가 (항상 최신값으로 적용)
    finalParameters.put("testId", "TEST_" + System.currentTimeMillis());
    finalParameters.put("scenarioId", "SCENARIO_" + System.currentTimeMillis());

    // 6. 런타임 계산 파라미터 (Dynamic Parameters) 처리
    //    PARAM_TYPE이 'DYNAMIC'인 파라미터는 PARAM_VALUE에 정의된 SQL을 실행하여 결과값을 finalParameters에 추가
    processDynamicParameters(finalParameters);

    // 7. SQL 쿼리 결과값 파라미터 (SQL Query Parameters) 처리
    //    PARAM_TYPE이 'SQL_QUERY'인 파라미터는 PARAM_VALUE에 정의된 SQL을 실행하여 결과값을 finalParameters에 추가
    //    이는 SQL 템플릿 치환 이전에 처리되어야 함.
    processSqlQueryParameters(finalParameters);

    return finalParameters;
}

private void loadAndApplyParameters(Map<String, Object> params, Long referId, String referType) {
    if (referId == null) return;
    List<ParameterDefinition> definitions = parameterDefinitionMapper.findByReferIdAndType(referId, referType);
    for (ParameterDefinition def : definitions) {
        // SYSTEM, CONDITIONAL, STATIC 파라미터는 여기서 직접 적용
        if (def.getParamType().equals("SYSTEM") || def.getParamType().equals("STATIC") || def.getParamType().equals("CONDITIONAL")) {
            params.put(def.getParamName(), def.getParamValue());
        }
        // DYNAMIC, SQL_QUERY 타입은 나중에 별도 처리
    }
}

private void processDynamicParameters(Map<String, Object> params) {
    // PARAMETER_DEFINITION 테이블에서 'DYNAMIC' 타입의 파라미터를 찾아
    // PARAM_VALUE에 있는 SQL을 실행하여 결과값을 params에 추가
    // 이 과정은 DataSource 및 JdbcTemplate 필요
    // 예: select sysdate from dual
}

private void processSqlQueryParameters(Map<String, Object> params) {
    // PARAMETER_DEFINITION 테이블에서 'SQL_QUERY' 타입의 파라미터를 찾아
    // PARAM_VALUE에 있는 SQL을 실행하여 결과값을 params에 추가
    // 이 결과는 해당 파라미터가 사용될 SQL 템플릿에 직접 치환될 예정
}

파라미터 처리 순서 (최종 SQL 생성 전):

  1. 시스템 파라미터 치환 ($paramName): $testId, $scenarioId 등 시스템에서 사전에 정의된 파라미터들을 ParameterResolver에서 생성된 최종 파라미터 맵의 값으로 치환합니다.
  2. SQL 쿼리 결과값 파라미터 (SQL Query Parameters) 처리: SQL Query Parameters 타입의 파라미터(PARAM_TYPE = 'SQL_QUERY')는 PARAM_VALUE에 정의된 SQL을 직접 실행하여 그 결과값으로 SQL 템플릿 내의 해당 파라미터 이름을 대체합니다. 이는 SQL 문맥에 따라 다르게 처리될 수 있으므로, SQL 템플릿 치환 전에 미리 처리되어야 합니다.
  3. 조건부 SQL 블록 처리 (#{conditionalSql}): #{ADD_CONDITION_1}과 같이 조건부로 추가되는 SQL 블록을 처리합니다. ParameterResolver에서 결정된 최종 파라미터 맵에 ADD_CONDITION_1이라는 키가 존재하고 값이 비어있지 않다면 해당 값을 SQL에 삽입합니다. 값이 없거나 비어있으면 해당 블록을 제거합니다.
  4. Add-On 파라미터 처리 (WHERE 절 추가): 이 부분은 CONDITIONAL 파라미터와 유사하게 처리될 수 있습니다. PARAM_TYPE = 'CONDITIONAL'로 정의된 파라미터들이 finalParameters 맵에 존재하고 그 값이 유효하다면, SQL 템플릿의 WHERE 1=1 뒤에 해당 조건절을 추가합니다.

2.4. SQL 실행: 동적 SQL 생성 및 데이터베이스 실행

개요:
파라미터 바인딩이 완료된 최종 SQL을 MariaDB에 실행하고 그 결과를 저장합니다.

주요 컴포넌트:

  • Spring JdbcTemplate: 동적으로 생성된 SQL을 실행하기에 적합합니다.
  • MyBatis Dynamic SQL: XML 또는 어노테이션 기반으로 동적 SQL을 생성하는 데 사용할 수 있습니다. 여기서는 JdbcTemplate 사용이 더 유연합니다.
  • Transaction Management: SQL 실행 시 트랜잭션 관리가 필요합니다.

로직 흐름:

@Service
public class SqlExecutorService {

    private final JdbcTemplate jdbcTemplate;
    private final TestExecutionResultMapper resultMapper;

    public SqlExecutorService(JdbcTemplate jdbcTemplate, TestExecutionResultMapper resultMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.resultMapper = resultMapper;
    }

    @Transactional
    public void executeSqlAndSaveResult(Long activityId, String processedSql, Map<String, Object> finalParameters) {
        String executionStatus = "FAILED";
        String errorMessage = null;
        String resultData = null;

        try {
            // SELECT 문인 경우 쿼리 실행 및 결과 저장
            if (processedSql.trim().toUpperCase().startsWith("SELECT")) {
                List<Map<String, Object>> rows = jdbcTemplate.queryForList(processedSql);
                resultData = new ObjectMapper().writeValueAsString(rows); // JSON 형태로 저장
            }
            // INSERT, UPDATE, DELETE 등 DML 문인 경우 업데이트 갯수 저장
            else {
                int affectedRows = jdbcTemplate.update(processedSql);
                resultData = "Affected Rows: " + affectedRows;
            }
            executionStatus = "SUCCESS";
        } catch (Exception e) {
            errorMessage = e.getMessage();
            // 에러 로깅
        } finally {
            // 실행 결과 저장
            TestExecutionResult result = new TestExecutionResult();
            result.setActivityId(activityId);
            result.setExecutedSql(processedSql);
            result.setParameterValues(new ObjectMapper().writeValueAsString(finalParameters)); // JSON 문자열로 변환
            result.setExecutionStatus(executionStatus);
            result.setResultData(resultData);
            result.setErrorMessage(errorMessage);
            resultMapper.insertTestExecutionResult(result);
        }
    }
}

2.5. 결과 검증: SQL 수행 결과 저장

개요:
SQL 실행 후 결과를 TEST_EXECUTION_RESULT 테이블에 저장합니다. 이 결과는 나중에 테스트 리포트 생성 등에 활용될 수 있습니다.

주요 컴포넌트:

  • MyBatis Mapper: TEST_EXECUTION_RESULT 테이블에 데이터를 삽입합니다.

저장 예시 (MyBatis Mapper XML):

<insert id="insertTestExecutionResult" useGeneratedKeys="true" keyProperty="execId">
    INSERT INTO TEST_EXECUTION_RESULT (ACTIVITY_ID, EXECUTED_SQL, PARAMETER_VALUES, EXECUTION_STATUS, RESULT_DATA, ERROR_MESSAGE)
    VALUES (#{activityId}, #{executedSql}, #{parameterValues}, #{executionStatus}, #{resultData}, #{errorMessage})
</insert>

3. 구현 시 고려사항 및 추가 제안

  • 보안: 동적 SQL 실행은 SQL Injection에 매우 취약합니다. 사용자 입력 값은 절대로 직접 SQL에 삽입하지 않도록 철저히 검증하고, 가능하다면 PreparedStatement를 통해 바인딩해야 합니다. SQL_QUERY 타입의 파라미터나 동적 SQL을 사용할 경우, 최대한 제한적인 권한의 DB 계정을 사용하고, 입력값에 대한 화이트리스트 방식의 검증을 강력히 적용해야 합니다.
  • 런타임 계산 파라미터 (Dynamic Parameters) 구현: PARAM_VALUE에 저장된 SQL을 별도의 JdbcTemplate으로 실행하여 결과값을 받아와야 합니다. 이때, 해당 SQL 자체도 또 다른 파라미터를 포함할 수 있는지 등 복잡도를 고려해야 합니다.
  • SQL 쿼리 결과값 파라미터 (SQL Query Parameters) 구현: PARAM_VALUE에 정의된 SQL을 실행하고, 그 결과값(단일 값으로 가정)으로 SQL 템플릿 내의 특정 ${paramName}을 대체해야 합니다. 이는 문자열 치환 방식으로 구현될 수 있습니다.
  • 트랜잭션 관리: 각 테스트 활동의 SQL 실행은 독립적인 트랜잭션으로 관리하거나, 전체 테스트 계획 실행을 하나의 큰 트랜잭션으로 묶을지 전략을 수립해야 합니다.
  • 비동기 처리: 대량의 테스트 케이스를 실행할 경우, 각 TestActivity의 SQL 실행을 비동기로 처리하여 시스템 응답성을 높일 수 있습니다 (예: Spring @Async, CompletableFuture).
  • 에러 핸들링: SQL 실행 중 발생하는 다양한 예외 상황에 대한 견고한 에러 핸들링 로직이 필요합니다.
  • 결과 리포팅: 저장된 TEST_EXECUTION_RESULT 데이터를 기반으로 테스트 실행 보고서를 생성하는 기능이 추가되어야 합니다.
  • UI/UX: 사용자가 파라미터를 쉽게 정의하고, SQL 템플릿을 작성하며, 실행 결과를 직관적으로 확인할 수 있는 웹 UI 개발이 중요합니다.

이 설계와 로직 흐름은 제시된 요구사항을 충족하며, 안정적이고 확장 가능한 테스트 도구 시스템을 구축하는 데 도움이 될 것입니다.

React, Typescript의 클래스 컴퍼넌트 환경에서 monitoringType 변수가 T 일 경우 'Activity Name'를 리턴하고 S 일 경우
'Monitoring Name'를 리턴하는 예제 만들어 주세요.

import React, { Component } from 'react';

type MonitoringType = 'T' | 'S';

interface Props {
monitoringType: MonitoringType;
}

class MonitoringLabel extends Component {
getLabel = (): string => {

const { monitoringType } = this.props;

switch (monitoringType) {
  case 'T':
    return 'Activity Name';
  case 'S':
    return 'Monitoring Name';
  default:
    return 'Unknown';
}

};

render() {

return (
  <div>
    <label>{this.getLabel()}</label>
  </div>
);

}
}

export default MonitoringLabel;

switch 문장 말고 간단하게 T이면 Activity Name 나머지는 Monitoring Name 으로 간략하게 수정해주세요

import React, { Component } from 'react';

type MonitoringType = 'T' | 'S';

interface Props {
monitoringType: MonitoringType;
}

class MonitoringLabel extends Component {
getLabel = (): string => {

return this.props.monitoringType === 'T' ? 'Activity Name' : 'Monitoring Name';

};

render() {

return (
  <div>
    <label>{this.getLabel()}</label>
  </div>
);

}
}

export default MonitoringLabel;

요구 사항 기반 테스트 관리 소프트웨어 Visure의 기능 및 관련 자료 정리 해주세요

요구 사항 기반 테스트 관리 소프트웨어인 Visure Requirements ALM은 복잡한 시스템과 소프트웨어 개발 프로젝트를 위한 요구사항 관리(Requirements Management), 테스트 관리(Test Management), 리스크 관리(Risk Management) 및 추적성(Traceability)을 제공하는 통합 Application Lifecycle Management(ALM) 플랫폼입니다.