Files
baron_qa_write/docs/관리페이지 md 파일/uuidv7-first-distributed-design-and-task-list.md
T
root 3c10478482
Deploy EG-BIM QA Gateway / deploy (push) Successful in 2m4s
관리페이지 데이터 전송 구현
2026-09-21 14:46:36 +09:00

34 KiB

UUID v7 기반 선제적 분산 설계 및 작업 목록

1. 목적

이 문서는 다음 방향으로 시스템을 처음부터 설계하기 위한 기준을 정의한다.

  • 핵심 도메인 테이블의 PK를 UUID v7 BINARY(16)으로 사용
  • 프로젝트별 작성 서버에서 중앙 DB 저장 전 ID 선발급
  • MySQL과 Redis는 Docker 내부 네트워크에서만 접근
  • 외부 API 재시도와 중복 등록을 Unique Key로 차단
  • 향후 API 수평 확장, Read Replica, 메시지 큐, DB 샤딩에 대응

현재 코드에는 공통 INT AUTO_INCREMENT PK, ParseIntPipe, id: number 타입, 프로젝트·채널·피드백 숫자 ID가 널리 사용되고 있다. 따라서 이 설계는 단순 컬럼 변경이 아니라 DB·API·프론트엔드·Docker·테스트를 함께 전환하는 신규 기준으로 적용한다.

현재 전환 상태

UUID v7 생성·검증·BINARY(16) 변환 유틸리티, TypeORM Transformer/Pipe, 공통 Entity PK 전환을 적용했다. API 식별자와 웹 내부 타입은 UUID 문자열 계약을 유지한다. 관리 화면의 피드백 URL은 가독성을 위해 프로젝트명·채널명을 경로 alias로 사용하고 피드백 자체는 UUID를 사용한다.

Node.js 24.14.1 환경에서 API 전체 타입체크, API 단위 테스트 66개 suite·993건, API 배포용 SWC 빌드 762개 파일, 웹 전체 타입체크와 프로덕션 빌드 79개 페이지를 통과했다. 로컬 MySQL을 초기화한 뒤 migration을 순서대로 실행했고, 최종 migration 이력 59개와 모든 도메인 PK BINARY(16)을 확인했다. TypeORM의 migrations.id는 메타데이터 테이블이므로 변환 대상에서 제외한다.

기존 단위·통합·E2E 테스트 코드에 남아 있던 숫자형 내부 ID fixture와 레거시 API 경로는 UUID v7·현행 REST 계약으로 전환했다. 실제 MySQL을 사용하는 OpenSearch 비활성 E2E는 12개 suite·129건, OpenSearch 활성 E2E는 2개 suite·27건, 웹 단위·계약 테스트는 3개 suite·5건, Secretary UUID/ABC 계약 테스트는 4건을 통과했다.

최종 로컬 기동 점검에서는 시스템 Node 18만 보이는 셸에서도 start-local.sh.nvmrc의 Node 24.14.1을 자동 선택했다. API DB health와 Secretary API health는 HTTP 200, Web 루트는 로그인 경로로 HTTP 308, Redis는 PONG으로 응답했다. API 응답의 UUID v7 x-correlation-id 헤더도 실제 Fastify 요청으로 검증했다.


2. 최종 설계 원칙

2.1 식별자 원칙

모든 핵심 도메인 PK       UUID v7, DB는 BINARY(16)
외부 API 식별자           PK와 동일한 UUID v7
관리 화면 피드백 URL      projectName + channelName + feedback UUID v7
프로젝트·채널 관계        FK로 검증
원본 시스템 중복 방지     source_namespace + source_record_id
요청 재시도 중복 방지      consumer + idempotency_key
Redis                    보조 저장소, Unique 제약의 대체재가 아님

UUID v7은 애플리케이션에서 생성한다. MySQL의 AUTO_INCREMENT나 Redis의 INCR를 전역 ID 생성기로 사용하지 않는다.

별도의 public_id 컬럼은 두지 않는다. DB PK인 UUID v7을 API·웹훅·로그의 공통 리소스 식별자로 사용한다. 관리 화면 URL의 프로젝트명·채널명은 사람이 읽기 위한 경로 alias이며, 서버 요청과 권한 검증에는 alias로 해석한 실제 UUID PK를 사용한다.

2.2 UUID 저장 방식

id BINARY(16) NOT NULL PRIMARY KEY

API와 애플리케이션에서는 표준 UUID 문자열을 사용하고, TypeORM Transformer에서 문자열과 BINARY(16)을 변환한다.

API: 0198f4c0-7a54-7abc-8b1e-9d2a4e5b6c7d
DB:  16-byte binary value

CHAR(36) 또는 VARCHAR(36)을 PK로 사용하지 않는다. UUID v7 기본 정렬 순서를 보존할 수 있도록 네트워크 바이트 순서로 저장한다.

2.3 PK와 업무 식별자의 분리

UUID PK는 전역 객체 식별자다. 사람이 보는 업무 번호와 외부 원본 식별자는 별도로 관리한다.

feedback.id             UUID v7 PK
feedback.display_code   ABC-2026-000123  -- 선택적 업무 표시 번호
feedback.source_namespace
feedback.source_record_id

project_id + channel_id + feedback_id를 조합해 새로운 PK를 만들지 않는다. UUID v7 자체가 전역적으로 유일하므로 피드백 PK는 feedback_id 하나로 충분하다.

2.4 확정된 분산 경계

연간 피드백·댓글 합계 5만 건을 기준으로 다음을 확정한다.

  • 현재 feedbacks에는 project_id를 중복 저장하지 않는다. 프로젝트는 feedbacks.channel_id → channels.project_id로 해석한다.
  • API는 요청의 projectId, channelId, feedbackId가 같은 계층인지 조회 시 반드시 검증한다.
  • 향후 DB 샤딩 키는 project_id로 한다. 프로젝트가 테넌트보다 부하 분산 단위가 작고 API 권한 경계와 일치하기 때문이다.
  • source_namespace는 영문 대문자·숫자·밑줄로 구성한 2~64자 코드로 발급한다. 예: EGBIM_QA, TOVA_PROD.
  • 모든 RP는 source_namespace 안에서 source_record_id를 전역 유일하게 제공한다. 기존 시스템이 채널 범위 ID만 제공하면 namespace 자체를 EGBIM_QA_CHANNEL_A처럼 분리한다.
  • Redis는 멱등성 빠른 차단, 분산 락, 레이트리밋에만 우선 사용한다. 업무 원본과 Unique 제약의 최종 권위는 MySQL이다.
  • 연 5만 건 단계에서는 피드백 JSON Generated Column을 선제 생성하지 않는다. 반복 쿼리와 slow query 측정으로 대상 키가 확정될 때만 추가한다.

namespace 등록 정보는 다음 논리 모델로 관리한다. 초기에는 배포 설정으로 등록하고, RP가 늘어날 때 동일 구조의 관리 테이블로 승격한다.

source_namespace
  code              varchar(64) unique
  display_name      varchar(255)
  owner             varchar(255)
  environment       enum(QA, STAGING, PROD)
  is_active         boolean

3. Project·Channel·Feedback 관계 설계

3.1 권장 관계

Tenant
  └─ Project
       └─ Channel
            └─ Feedback
                 └─ Comment

권장 FK:

projects.tenant_id       → tenants.id
channels.project_id      → projects.id
feedbacks.channel_id     → channels.id
comments.feedback_id     → feedbacks.id

정규화만 고려하면 feedbacks에는 channel_id만 저장하고 프로젝트는 채널을 통해 조회할 수 있다.

3.2 분산 라우팅을 위한 project_id 보관

향후 프로젝트 단위 샤딩 또는 권한 필터링을 빠르게 적용하려면 feedbacks.project_id를 중복 보관할 수 있다.

현재 연 5만 건 설계에서는 이 중복 컬럼을 적용하지 않는다. 아래 복합 FK는 단일 DB의 실제 부하 측정에서 프로젝트 직접 필터가 병목으로 확인되거나 샤딩 전환이 시작될 때 적용하는 선택 설계다.

이 경우 단순히 애플리케이션에서만 일치 여부를 확인하지 않고, 다음 복합 FK로 채널과 프로젝트의 일치성을 DB에서도 검증한다.

ALTER TABLE channels
    ADD UNIQUE KEY uk_channels_project_id_id (project_id, id);

ALTER TABLE feedbacks
    ADD CONSTRAINT fk_feedback_project_channel
    FOREIGN KEY (project_id, channel_id)
    REFERENCES channels (project_id, id);

이 설계의 의미는 다음과 같다.

  • feedbacks.project_id: 샤드 라우팅·권한·프로젝트 목록 조회용
  • feedbacks.channel_id: 실제 접수 채널 FK
  • feedbacks.id: 전역 객체 PK

project_idchannel_id는 PK를 구성하지 않는다. 두 컬럼은 소속 검증과 라우팅을 위한 관계 컬럼이다.

3.3 API 경로의 의미

다음 경로는 리소스 식별자라기보다 소속 검증과 권한 범위를 표현한다.

/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}

조회 조건은 다음처럼 구성한다.

SELECT *
FROM feedbacks
WHERE id = :feedbackId
  AND project_id = :projectId
  AND channel_id = :channelId;

여기서 (project_id, channel_id, feedback_id)는 Unique Key로 만들지 않는다. feedback_id가 이미 PK이기 때문이다.


4. Unique Key 설계

4.1 기본 원칙

Unique Key는 다음 세 가지를 구분해서 설계한다.

  1. 객체의 전역 식별: UUID v7 PK
  2. 업무상 중복 방지: 프로젝트·채널·외부 원본 키 조합
  3. HTTP 재시도 중복 방지: 소비자·멱등 키 조합

서로 다른 목적의 값을 하나의 거대한 복합 Unique Key로 합치지 않는다.

4.2 권장 Unique Key 목록

대상 권장 Unique Key 목적
Tenant tenant_id 또는 외부 SSO 테넌트 키 전역 테넌트 식별
Project (tenant_id, project_code) 테넌트 내 프로젝트 식별
Channel (project_id, channel_code) 프로젝트 내 채널 식별
Feedback (source_namespace, source_record_id) 외부 원본 중복 방지
Comment (feedback_id, source_namespace, source_record_id) 외부 댓글 중복 방지
Issue (project_id, issue_key) 프로젝트 내 이슈 식별
Feedback-Issue (feedback_id, issue_id) 다대다 연결 중복 방지
Field (channel_id, field_key) 채널 내 필드 키 중복 방지
Option (field_id, option_key) 필드 내 옵션 키 중복 방지
Member (project_id, user_id, role_id) 프로젝트 권한 중복 방지
Idempotency (consumer, idempotency_key) 동일 요청 재처리 방지
External Issue (provider, project_id, external_issue_id) 외부 이슈 중복 방지
Object Storage (storage_bucket, storage_key) 파일 객체 중복 방지

4.3 Feedback 원본 키의 최종 권장안

가장 좋은 방식은 RP마다 source_namespace를 발급하고, 그 네임스페이스 안에서 원본 ID를 유일하게 관리하도록 계약하는 것이다.

source_namespace = RP_EGBIM
source_record_id = 502

Unique Key:

UNIQUE KEY uk_feedback_source
    (source_namespace, source_record_id)

이렇게 하면 project_idchannel_id를 Unique Key에 불필요하게 추가하지 않아도 된다.

4.4 원본 ID의 범위가 좁은 경우

외부 RP가 원본 ID를 프로젝트 또는 채널별로만 유일하게 보장한다면 범위를 명시해야 한다.

프로젝트 범위: (source_namespace, project_id, source_record_id)
채널 범위:    (source_namespace, channel_id, source_record_id)

둘 중 하나만 선택한다. 프로젝트와 채널이 모두 포함된 다음 구조는 원본 계약상 정말 필요한 경우에만 사용한다.

(source_namespace, project_id, channel_id, source_record_id)

기본 정책은 source_namespace + source_record_id로 하고, 레거시 RP에 한해서만 프로젝트 또는 채널 범위를 추가한다.

4.5 Idempotency Key와 Redis

Redis의 SETNX는 빠른 중복 요청 차단에 사용할 수 있지만 최종 정합성은 MySQL Unique Key가 보장해야 한다.

1. Redis SETNX idempotency:{consumer}:{key}
2. MySQL INSERT
3. MySQL Unique 충돌 시 기존 결과 조회
4. 처리 결과를 Redis에 짧은 TTL로 캐시

Redis 장애나 만료가 발생해도 MySQL Unique Key로 중복 저장이 발생하지 않아야 한다.


5. 핵심 테이블 예시

CREATE TABLE projects (
    id BINARY(16) NOT NULL,
    tenant_id BINARY(16) NOT NULL,
    project_code VARCHAR(64) NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at DATETIME(6) NOT NULL,
    updated_at DATETIME(6) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_projects_tenant_code (tenant_id, project_code),
    KEY idx_projects_tenant (tenant_id)
);

CREATE TABLE channels (
    id BINARY(16) NOT NULL,
    project_id BINARY(16) NOT NULL,
    channel_code VARCHAR(64) NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at DATETIME(6) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_channels_project_code (project_id, channel_code),
    UNIQUE KEY uk_channels_project_id_id (project_id, id),
    KEY idx_channels_project_created (project_id, created_at, id)
);

CREATE TABLE feedbacks (
    id BINARY(16) NOT NULL,
    channel_id BINARY(16) NOT NULL,
    source_namespace VARCHAR(64) NULL,
    source_record_id VARCHAR(255) NULL,
    data JSON NOT NULL,
    admin_first_read_at DATETIME(6) NULL,
    created_at DATETIME(6) NOT NULL,
    updated_at DATETIME(6) NOT NULL,
    deleted_at DATETIME(6) NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_feedback_source (source_namespace, source_record_id),
    KEY idx_feedback_channel_list
        (channel_id, deleted_at, created_at DESC, id DESC),
    CONSTRAINT fk_feedback_channel
        FOREIGN KEY (channel_id) REFERENCES channels (id)
);

CREATE TABLE feedback_comments (
    id BINARY(16) NOT NULL,
    feedback_id BINARY(16) NOT NULL,
    source_namespace VARCHAR(64) NULL,
    source_record_id VARCHAR(255) NULL,
    content TEXT NOT NULL,
    created_at DATETIME(6) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_comment_source
        (feedback_id, source_namespace, source_record_id),
    KEY idx_comments_feedback_created (feedback_id, created_at, id),
    CONSTRAINT fk_comment_feedback
        FOREIGN KEY (feedback_id) REFERENCES feedbacks (id)
);

외부 원본이 없는 내부 생성 데이터에서는 source_namespacesource_record_id를 NULL로 둘 수 있다. MySQL의 Unique Key는 NULL을 여러 건 허용하므로 내부 댓글의 정상적인 다건 등록을 막지 않는다.

5.1 FK 마이그레이션 순서

UUID PK/FK 타입을 변경하거나 되돌릴 때는 참조 그래프를 기준으로 다음 순서를 지킨다.

  1. 쓰기를 중단하고 백업을 확인한다.
  2. 자식 테이블의 FK를 가장 하위부터 삭제한다. 연결 테이블 → 댓글·첨부 → 피드백·이슈 → 채널 → 프로젝트 → 테넌트 순이다.
  3. PK와 FK 컬럼을 모두 같은 BINARY(16) 정의로 변경한다.
  4. 부모 테이블부터 PK/Unique Key를 생성한다. 테넌트 → 프로젝트 → 채널 → 피드백·이슈 → 댓글·첨부 → 연결 테이블 순이다.
  5. 자식 방향으로 FK를 다시 생성한다.
  6. orphan 행, PK/FK 타입 불일치, 인덱스 누락이 0건인지 검사한 뒤 쓰기를 재개한다.

TypeORM migration의 up은 위 생성 순서를, down은 반대 순서를 사용한다. FK 이름은 migration에 명시하여 환경마다 자동 생성 이름이 달라지지 않게 한다.


6. REST 리소스와 화면 URL 설계

현재 상세창 URL은 다음처럼 리소스 ID가 Query String에 들어간다.

/main/project/2/feedback?channelId=2&feedbackId=106&detailWindow=1

UUID v7과 REST 규칙을 적용하면 리소스 식별자 또는 화면용 경로 alias를 경로에 배치하고, Query String은 필터·정렬·페이지네이션 같은 선택 조건에만 사용한다.

5.1 화면 URL

피드백 목록
/main/projects/{projectName}/channels/{channelName}/feedbacks

피드백 상세
/main/projects/{projectName}/channels/{channelName}/feedbacks/{feedbackId}

예시:

/main/projects/EGBIM/channels/Q&A/feedbacks/0198f4c2-7a54-7abc-8b1e-9d2a4e5b6c7f

프로젝트·채널 이름을 경로 alias로 사용하면 다음 효과가 있다.

  • 프로젝트·채널을 URL에서 바로 식별할 수 있음
  • 피드백은 변경되지 않는 UUID로 정확히 식별됨
  • detailWindow=1 같은 화면 제어용 플래그가 필요 없음
  • 새로고침·공유·뒤로 가기가 동일한 리소스를 가리킴
  • URL만 보고 프로젝트·채널·피드백 계층을 이해할 수 있음

feedbackId가 전역 식별자이고 화면 경로의 프로젝트명·채널명은 내부 projectId, channelId로 해석하여 소속과 권한을 검증한다. 프로젝트명은 전역 Unique, 채널명은 프로젝트 내 Unique 제약을 사용한다. 기존 UUID 화면 URL도 진입 호환용으로 유지하고 현재 이름 URL로 정규화한다. 이름 변경 후에는 새 이름이 canonical URL이 된다.

5.2 Query String 사용 범위

Query String은 리소스 ID가 아닌 조회 조건에만 사용한다.

/main/projects/{projectName}/channels/{channelName}/feedbacks
  ?status=OPEN
  &sort=createdAt.desc
  &cursor=...
  &limit=20

다음 항목은 Query String에 두지 않는다.

channelId=...
feedbackId=...
detailWindow=1

5.3 REST API 경로

컬렉션은 복수 명사로 표현하고, 동작은 HTTP method로 표현한다.

Method Path 의미
GET /api/projects/{projectId}/channels/{channelId}/feedbacks 피드백 목록 조회
POST /api/projects/{projectId}/channels/{channelId}/feedbacks 피드백 생성
GET /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId} 피드백 상세 조회
PATCH /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId} 피드백 부분 수정
DELETE /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId} 피드백 삭제 또는 소프트 삭제
GET /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/comments 댓글 목록 조회
POST /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/comments 댓글 생성
GET /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issues 연결 이슈 목록 조회
PUT /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issues/{issueId} 이슈 연결
DELETE /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issues/{issueId} 이슈 연결 해제

UUID가 전역적으로 유일하고 API가 소속 검증을 내부에서 처리한다면 다음 짧은 URI를 canonical detail URI로 추가할 수도 있다.

GET   /api/v1/feedbacks/{feedbackId}
PATCH /api/v1/feedbacks/{feedbackId}

초기에는 권한 경계를 명확히 유지하기 위해 프로젝트·채널이 포함된 scoped URI를 기본으로 사용한다.

5.4 기존 API 전환 규칙

기존 형태 권장 형태
POST /feedbacks/search GET /feedbacks?query=... 또는 복합 검색 시 POST /feedback-searches
POST /feedbacks/{id}/issue/{issueId} PUT /feedbacks/{id}/issues/{issueId}
DELETE /feedbacks/{id}/issue/{issueId} DELETE /feedbacks/{id}/issues/{issueId}
POST /feedbacks-with-images POST /feedbacks multipart/form-data
PUT /feedbacks/{id} 부분 수정은 PATCH /feedbacks/{id}
DELETE /feedbacks 대량 삭제 개별 DELETE 반복 또는 별도 bulk command

5.5 외부 계약 버전과 호환 기간

  • 현재 외부 계약 버전은 OpenAPI 1.0.0이며 실제 URL은 기존 호환을 위해 /api prefix를 유지한다.
  • canonical 요청은 POST /feedbacks(JSON 또는 multipart)와 PATCH /feedbacks/{id}이다.
  • POST /feedbacks-with-images와 피드백·댓글 수정 PUT alias는 2027-03-31까지 유지한다.
  • 구형 alias 응답에는 Deprecation: trueSunset: Wed, 31 Mar 2027 00:00:00 GMT를 제공한다.
  • 다음 호환 불가능 변경부터 URL 또는 media type에 명시적인 v2를 도입하고 OpenAPI major 버전을 함께 올린다.

5.6 상세창 라우팅

상세창이 Sheet/Modal이어도 URL은 상세 리소스를 가리켜야 한다.

목록에서 피드백 클릭
  ↓
URL을 /feedbacks/{feedbackId}로 변경
  ↓
상세 Sheet 표시
  ↓
뒤로 가기 시 목록 복귀

따라서 detailWindow=1은 제거하고, 현재 라우트에 feedbackId가 있는지로 상세창 표시 여부를 판단한다.

5.7 REST URL의 권한 검증

서버는 경로의 세 ID가 실제로 연결되어 있는지 확인해야 한다.

SELECT f.id
FROM feedbacks f
JOIN channels c ON c.id = f.channel_id
WHERE f.id = :feedbackId
  AND f.channel_id = :channelId
  AND c.project_id = :projectId;

UUID가 추측하기 어렵다는 이유로 프로젝트 권한과 채널 접근 권한 검사를 생략하지 않는다.


7. Docker 네트워크 및 포트 설계

6.1 포트 정책

Docker Compose에서 ports는 호스트로 포트를 publish하고, expose는 컨테이너 간 사용 포트를 문서화한다. 실제 외부 차단 경계는 내부 Docker network와 ports 미설정이다.

서비스 운영 외부 공개 Docker 내부 접근
Web/Nginx 공개 web:3000
API 직접 공개하지 않음 api:4000
Secretary API 필요 경로만 공개 secretary-api:8010
MySQL 공개하지 않음 mysql:3306
Redis 공개하지 않음 redis:6379
OpenSearch 공개하지 않음 opensearch:9200

API의 DB 연결 문자열은 호스트 포트가 아니라 서비스 DNS를 사용한다.

MYSQL_PRIMARY_URL=mysql://userfeedback:***@mysql:3306/userfeedback
REDIS_URL=redis://:***@redis:6379/0

6.2 Compose 기본 구조

services:
  api:
    ports:
      - '4000:4000' # 로컬 개발에서만 필요
    expose:
      - '4000'
    networks: [frontend, backend]

  mysql:
    expose:
      - '3306'
    networks: [backend]

  redis:
    image: redis:7
    expose:
      - '6379'
    networks: [backend]

networks:
  frontend:
  backend:
    internal: true

운영 Compose에서는 MySQL과 Redis에 ports를 설정하지 않는다. 로컬에서 DB 클라이언트로 접속해야 하면 별도의 local override에서만 다음처럼 loopback에 바인딩한다.

ports:
  - '127.0.0.1:13306:3306'

Redis는 외부에 공개하지 않는다. 캐시·락·멱등성 보조 용도로 사용하고 원본 데이터와 유일성 보장은 MySQL이 담당한다.

6.3 Redis 사용 범위

Redis에 저장할 수 있는 데이터:

  • 목록 캐시
  • 짧은 TTL의 Idempotency 결과
  • Rate Limit 카운터
  • 분산 Lock
  • 비동기 작업 큐의 임시 상태

Redis에 저장하면 안 되는 단일 원본:

  • 피드백 본문
  • 댓글 원문
  • 사용자 권한의 최종 상태
  • Unique Key를 대신하는 유일성 데이터

8. 구현 작업 목록

Phase 0. 설계 확정

  • UUID v7의 canonical string과 BINARY(16) 변환 규칙 확정
  • UUID v7 생성 라이브러리(uuid v13)와 기본 생성 방식 확정
  • 모든 핵심 도메인 테이블의 UUID PK 적용 범위 확정
  • source_namespace 발급 규칙과 등록 모델 설계 (2~64자 대문자·숫자·밑줄, 환경별 namespace)
  • source_record_id의 유일 범위를 namespace 전역으로 통일 (채널 범위 RP는 namespace 분리)
  • 현재 규모에서는 project_id를 feedbacks에 denormalize하지 않기로 확정
  • 향후 샤드 라우팅 기준을 project로 확정
  • 외부 API는 별도 publicId 없이 UUID PK를 id로 사용하고, 관리 화면 URL은 프로젝트명·채널명 alias와 피드백 UUID를 조합
  • Redis는 멱등성 빠른 차단·분산 락·레이트리밋에 사용하고 MySQL을 최종 권위로 확정

Phase 1. DB 스키마 및 마이그레이션

  • 대상 DB의 실제 PK·FK·인덱스·row 수 인벤토리 수집 (scripts/uuid-migration-inventory.sql 및 migration introspection)
  • 기존 CommonEntity의 increment PK 제거
  • UUID v7 공통 PK 컬럼/Transformer 구현 (apps/api/src/common/uuid-binary.transformer.ts)
  • 프로젝트·채널·피드백·댓글·이슈 PK를 BINARY(16)으로 설계
  • 모든 FK와 다대다 조인 테이블의 타입 통일
  • project_id + channel_id 복합 FK는 현재 미적용으로 검증 (feedbacks에 project_id를 저장하지 않음)
  • source_namespace + source_record_id Unique Key 추가
  • 댓글·외부 이슈·첨부파일의 원본 Unique Key 추가
  • 목록 조회용 (channel_id, deleted_at, created_at, id) 인덱스 추가
  • feedback_issue 연결 테이블에 (feedback_id, issue_id) PK 적용
  • JSON Generated Column은 현 규모에서 보류하고 slow query로 반복 키가 확인될 때 추가하도록 설계
  • DB 초기화용 schema dump와 TypeORM migration을 새 PK 기준으로 재생성
  • FK 삭제 순서와 생성 순서를 문서화

Phase 2. 애플리케이션 ID 계층

  • 점진적 전환이 가능한 Uuid branded 공통 ID 타입 도입 (런타임 경계는 UUID v7 검증 유지)
  • UUID v7 생성기 구현 (apps/api/src/common/uuid-v7.ts)
  • 문자열↔Buffer 변환 Transformer 구현
  • UUID 형식 검증 Pipe/Validator 구현 (apps/api/src/common/pipes/uuid-v7.pipe.ts)
  • ParseIntPipe를 UUID 검증으로 교체
  • id: number, projectId: number, channelId: number 타입 전환
  • Entity factory의 숫자 ID 할당 코드 전환
  • Map·Set·캐시 키 생성 규칙 전환
  • UUID를 일반 JavaScript number로 변환하지 않도록 금지 규칙 추가

Phase 3. API 및 외부 연동

  • API의 project/channel/feedback/comment/issue ID를 UUID로 전환
  • 화면 목록 URL을 /main/projects/{projectName}/channels/{channelName}/feedbacks로 전환
  • 화면 상세 URL을 /main/projects/{projectName}/channels/{channelName}/feedbacks/{feedbackId}로 전환
  • 프로젝트명·채널명 URL을 실제 UUID로 해석하고 기존 UUID 화면 URL을 이름 URL로 정규화
  • 이름 URL에서도 피드백 상세 권한 조회에는 실제 프로젝트 UUID를 전달해 중요도·삭제 등 수정 권한 유지
  • canonical URL에서 channelId, feedbackId, detailWindow Query String 제거 (기존 URL은 진입 호환용으로 유지)
  • open-detail-window를 경로 기반 라우팅으로 변경
  • 상세 Sheet/Modal 표시 여부를 feedbackId 라우트 존재 여부로 변경
  • 목록 Query String은 필터·정렬·페이지 상태에만 사용
  • API 경로의 issue 단수형을 issues 복수형으로 변경 (기존 단수형 경로는 호환 alias 유지)
  • feedbacks-with-imagesPOST /feedbacks multipart 요청으로 통합 (기존 경로는 sunset alias)
  • 피드백·댓글 부분 수정 API를 PATCH로 통일 (기존 PUT은 sunset alias)
  • API DTO의 ID 예시와 OpenAPI schema를 UUID 문자열로 갱신
  • 프로젝트·채널·피드백 소속 검증 쿼리 구현
  • source_namespace + source_record_id 기반 중복 등록 처리 구현
  • consumer + idempotency_key 기반 재시도 처리 구현 (Redis 선점 + MySQL Unique 최종 권위)
  • Webhook payload의 ID 형식과 상세 링크를 UUID canonical URL로 전환
  • OpenSearch 문서 ID와 채널 색인명을 UUID 문자열로 전환
  • Gitea/Jira/GitHub 외부 이슈 매핑 Unique Key와 provider 저장 적용
  • Secretary API의 abc_feedback_id 형식과 DB 모델 전환 (UUID v7 검증 및 Alembic 0020/0021)
  • Web 지원 BFF에서 UUID 피드백 ID를 Secretary 내부 숫자 티켓에 안전하게 연결하고 /tickets/NaN 요청 제거
  • Web의 로컬 workspace 매핑이 누락되어도 상태 목록은 Secretary의 DB 매핑으로 fallback하도록 보강
  • Web의 SUPPORT_*_PROJECT_ID·SUPPORT_*_CHANNEL_ID 고정 환경변수를 제거하고 ABC 내부 API 기반 동적 workspace 매핑(30초 캐시)으로 전환
  • 외부 클라이언트 계약 버전과 호환 기간 정의 (OpenAPI 1.0.0, legacy sunset 2027-03-31)
  • REST URL 공유·새로고침·상세창 복귀 계약 테스트 추가 (open-detail-window.spec.ts, canonical route 프로덕션 빌드 검증)

Phase 4. Redis 도입

  • Redis Compose 서비스 추가
  • 운영 Compose에서 Redis ports 제거, expose: 6379만 사용
  • Redis 전용 내부 네트워크 연결
  • Redis 비밀번호/ACL을 Secret 또는 환경변수로 관리
  • Redis healthcheck 추가
  • REDIS_URL 설정과 NestJS Redis 모듈 추가
  • 캐시 키 네이밍 규칙 정의 (abc:v1:{feature}:{kind}:{consumer}:{key})
  • Idempotency 키 TTL과 결과 보존 정책 정의 (lock 30초, 결과 24시간, MySQL 30일)
  • Redis 장애 시 MySQL만으로 처리 가능한 fallback 구현
  • Redis를 Unique Key의 최종 권위로 사용하지 않는 테스트 추가 (Redis 중지 상태 통합 테스트 통과)

Phase 5. Docker 및 배포

  • MySQL 운영 Compose의 host ports 제거
  • MySQL expose: 3306 추가
  • Redis expose: 6379 추가
  • API·Secretary API의 DB 주소를 mysql:3306으로 통일
  • API·Secretary API의 Redis 주소를 redis:6379로 통일
  • frontend·backend network 분리
  • backend network에 internal: true 적용
  • 로컬 전용 Compose override에만 데이터 저장소 127.0.0.1 포트 바인딩
  • MySQL·Redis healthcheck와 depends_on 조건 추가
  • 운영 비밀번호와 토큰을 Compose 파일에서 제거 (필수 환경변수 주입)
  • Gitea 배포 workflow에서 MySQL·Redis 비밀값을 검증하고 URL-encoding한 내부 DB/Redis URL을 동적으로 생성
  • 백업 볼륨과 복구 절차 문서화

Phase 6. 테스트 및 검증

  • UUID v7 생성 충돌 테스트
  • 같은 밀리초 동시 생성 테스트
  • UUID binary round-trip 테스트
  • PK/FK 저장·조회 테스트 (로컬 migration 후 PK/FK 타입 불일치 0건)
  • project/channel/feedback 소속 불일치 차단 테스트
  • 동일 source record 재전송 테스트 (통합 DB에서 동일 UUID 반환 및 1건 유지 검증)
  • 동일 idempotency key 재전송 테스트 (동일 UUID·DB 1건, 다른 payload 409)
  • Redis 장애·재시작 시 중복 저장 방지 테스트 (실제 컨테이너 중지/재시작 검증)
  • API 전 엔드포인트 UUID 입력 테스트 (controller param 정적 계약 + OpenAPI path schema 검사)
  • OpenSearch·Webhook·Secretary 연동 테스트 (OpenSearch 활성 27건, Secretary 계약 5건)
  • 지원 상세 UUID 회귀 테스트 (상태·상세·담당자·내부 메모·댓글·읽음 처리 실제 HTTP 6건 모두 200)
  • 프로젝트명·채널명 피드백 URL과 기존 UUID URL의 HTTP 200 및 라우팅 단위 테스트 검증
  • workspace 코드↔ABC 프로젝트·채널 UUID 동적 매핑 단위 테스트 추가
  • 프로젝트명 URL 전환 후 중요도 선택이 read-only가 되지 않도록 권한 조회 회귀 수정
  • 중요도 수정 시 읽기 전용 요청자 메타데이터를 PATCH payload에서 제외하고 MEDIUM → HIGH → MEDIUM 실제 저장·원복 검증
  • UUID 피드백 상세 조회에 Secretary 담당자 정보를 병합하고 지정 해제 → 재지정 실제 저장·재조회·원복 검증
  • integration/E2E fixture·seed 데이터 UUID 전환 및 실제 실행 검증 (OpenSearch 비활성 12 suite·129건 통과)
  • 레거시 API E2E 경로를 현행 /admin/... 및 project-scoped API 계약으로 재작성
  • OpenSearch 활성·비활성 모드별 E2E provider와 검증 분리
  • 페이지네이션 cursor가 (created_at, id)를 안정적으로 처리하는지 검증 (동일 시각 UUID tie-break 포함)
  • 연간 5만~10만 건 데이터셋과 피크 동시 목록 쿼리 부하 테스트 (10만 행·동시 32 연결 결과 runbook 기록)

Phase 7. 운영 준비

  • UUID·source key·idempotency key 로깅 정책 확정
  • 개인정보와 원본 데이터가 로그에 노출되지 않도록 마스킹
  • DB Unique 충돌률 모니터링 (abc_db_unique_conflicts_total)
  • Redis 메모리·eviction·연결 수 모니터링 기준 정의
  • MySQL CPU·I/O·Buffer Pool·Lock wait 모니터링 기준 정의
  • API p95/p99와 재시도율 모니터링 (abc_http_request_duration_seconds, idempotency outcome)
  • UUID 기반 장애 추적용 correlation ID 적용 (Fastify 실제 HTTP 응답 헤더 및 adapter 호환 단위 테스트 검증)
  • MySQL 복구와 Redis 재구축 절차 검증 (33 tables·59 migrations·row mismatch 0)
  • 샤드 라우팅 기준과 확장 절차 문서화

9. 완료 기준

다음 조건을 만족하면 UUID v7 선제 전환 설계가 완료된 것으로 본다.

  • 핵심 도메인 테이블의 PK/FK가 모두 UUID v7 BINARY(16)이다.
  • project_id, channel_id는 관계·라우팅 컬럼이고 PK 조합에 중복 포함하지 않는다.
  • 외부 원본 중복은 source_namespace + source_record_id로 차단한다.
  • HTTP 재시도 중복은 MySQL Unique Key와 Redis 보조 처리로 차단한다.
  • MySQL과 Redis는 운영 환경에서 호스트에 직접 publish되지 않는다.
  • Redis가 없어도 원본 데이터의 정합성은 MySQL만으로 유지된다.
  • API·프론트엔드·웹훅·검색 색인이 UUID 형식을 일관되게 사용한다.
  • 피드백 상세 URL에 프로젝트명·채널명과 feedbackId가 경로로 표시되고 detailWindow Query String이 없다.
  • Query String은 필터·정렬·페이지네이션에만 사용된다.
  • API가 복수 명사와 HTTP method 중심의 REST 규칙을 따른다.
  • 연간 5만~10만 건 및 피크 부하 테스트를 통과한다.