본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
11장 : 관계 소유권과 식별

복합 식별 키와 계층 구조

댓글→답글→반응으로 복합 키가 전파되는 DDL을 만들고 넓은 FK·인덱스·변경 비용을 대리 키 모델과 비교합니다.

복합 키 한 단계는 읽기 쉽지만 자식이 다시 부모가 되면 모든 구성 열이 다음 FK로 전달됩니다.

게시글 댓글의 답글, 답글 반응까지 식별 관계로 연결하면 마지막 테이블이 업무 의미보다 조상 키 복사에 더 많은 열을 씁니다.

식별안에서 댓글 PK는 (post_id, sequence_no), 답글 PK는 여기에 reply_no, 반응 PK는 reaction_no까지 더합니다.

비식별안에서는 comment_id, reply_id, reaction_id가 각 대표 PK이고 부모 FK는 한 열입니다.


누적되는 복합 키

DDL은 가능하지만 반응 조회에 post_id·sequence_no·reply_no·reaction_no를 모두 알아야 합니다.

댓글 순서 변경이 모든 하위 PK/FK 변경으로 번지고 보조 인덱스마다 긴 PK 접미 키가 붙습니다.

아래 예제는 ch11-1에서 만든 두 댓글 모델의 고정 예제 데이터를 부모로 사용합니다.

조상 키가 계속 자라는 식별 계층
DROP TABLE IF EXISTS reply_reaction_identifying;
DROP TABLE IF EXISTS comment_reply_identifying;

CREATE TABLE comment_reply_identifying (
  post_id BIGINT UNSIGNED NOT NULL,
  sequence_no SMALLINT UNSIGNED NOT NULL,
  reply_no INT UNSIGNED NOT NULL,
  body VARCHAR(500) NOT NULL,
  PRIMARY KEY (post_id, sequence_no, reply_no),
  FOREIGN KEY (post_id, sequence_no)
    REFERENCES post_comment_identifying (post_id, sequence_no)
);
CREATE TABLE reply_reaction_identifying (
  post_id BIGINT UNSIGNED NOT NULL,
  sequence_no SMALLINT UNSIGNED NOT NULL,
  reply_no INT UNSIGNED NOT NULL,
  reaction_no INT UNSIGNED NOT NULL,
  emoji VARCHAR(16) NOT NULL,
  PRIMARY KEY (post_id, sequence_no, reply_no, reaction_no),
  FOREIGN KEY (post_id, sequence_no, reply_no)
    REFERENCES comment_reply_identifying
      (post_id, sequence_no, reply_no)
);

SET @identifying_post_id := (
  SELECT MIN(post_id) FROM post_comment_identifying
);

INSERT INTO comment_reply_identifying
  (post_id, sequence_no, reply_no, body)
VALUES (@identifying_post_id, 1, 1, '복합 키 답글');

INSERT INTO reply_reaction_identifying
  (post_id, sequence_no, reply_no, reaction_no, emoji)
VALUES (@identifying_post_id, 1, 1, 1, '좋아요');

세 단계 뒤 반응의 대표 PK가 네 열입니다.

기본 RESTRICT에서는 부모 순번 변경이 거절되고, ON UPDATE CASCADE를 선택하면 모든 하위 PK와 FK 변경으로 번집니다.

하위 참조가 있는 부모 순번 변경 실패
UPDATE post_comment_identifying
SET sequence_no = 2
WHERE post_id = @identifying_post_id
  AND sequence_no = 1;
-- ERROR 1451: comment_reply_identifying이 기존 복합 키를 참조
키 구조 확인 결과
comment PK columns: 2
reply PK columns: 3
reaction PK columns: 4

move comment sequence 1 → 2 with RESTRICT: ERROR 1451
with ON UPDATE CASCADE: comment + all replies + all reactions change
simple reaction URL id: unavailable

관계가 종속적이라는 사실과 모든 계층의 업무 식별을 조상 키 전체로 표현해야 한다는 결론은 다릅니다.

하위 행이 API·감사·검토에서 독립 참조되면 짧고 안정적인 ID가 유리합니다.

넓은 클러스터형 PK는 InnoDB의 모든 보조 인덱스 리프에 포함되어 저장 공간에도 전파됩니다.

열 수만이 아니라 타입 바이트와 보조 인덱스 개수를 계산합니다.

세대마다 늘어난 키 열 세기

  1. 키 성장 — 단계마다 PK 열·바이트 수를 표로 만듭니다.
  2. 보조 인덱스 — 각 리프에 클러스터형 PK가 추가되는 비용을 계산합니다.
  3. 변경 전파 — 부모 순번·소유 변경이 하위 식별을 바꾸는지 봅니다.
  4. API 참조 — 외부 링크와 로그가 어떤 키를 전달해야 하는지 확인합니다.

식별 의미와 참조 폭

comment_id·reply_id를 대표 PK로 두어 하위 FK를 짧게 만들고, 각 부모 범위의 순서·reply_no는 UNIQUE로 보존할 수 있습니다.

종속 수명은 FK와 삭제 정책이 표현하며 반드시 복합 PK만이 소유를 표현하는 것은 아닙니다.

대리 키는 모델 의미를 약화시키지 않지만 DDL만 보면 부모 범위 후보 키가 덜 눈에 띌 수 있습니다.

명명된 UNIQUE와 ERD 카디널리티, 용어집을 함께 유지합니다.

식별 의미와 참조 폭을 분리하기

  1. 외부 주소 — 반응을 단독 ID로 API·감사에서 찾는지 봅니다.
  2. 재정렬 — 순서 변경이 식별자 변경이어야 하는지 묻습니다.
  3. 인덱스 폭 — 보조 인덱스 수×클러스터형 PK 바이트를 비교합니다.
  4. 삭제 — 대리 키 전환 뒤에도 FK RESTRICT/CASCADE 정책을 명시합니다.

대표 ID와 부모 범위 키

reply_idreaction_id로 참조를 고정하고 (comment_id, reply_no) 같은 업무 후보 키를 유지합니다.

짧은 FK를 가진 비식별 계층
DROP TABLE IF EXISTS reply_reaction;
DROP TABLE IF EXISTS comment_reply;

CREATE TABLE comment_reply (
  reply_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  comment_id BIGINT UNSIGNED NOT NULL,
  reply_no INT UNSIGNED NOT NULL,
  body VARCHAR(500) NOT NULL,
  PRIMARY KEY (reply_id),
  UNIQUE (comment_id, reply_no),
  FOREIGN KEY (comment_id)
    REFERENCES post_comment_surrogate (comment_id)
);
CREATE TABLE reply_reaction (
  reaction_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  reply_id BIGINT UNSIGNED NOT NULL,
  member_id BIGINT UNSIGNED NOT NULL,
  emoji VARCHAR(16) NOT NULL,
  PRIMARY KEY (reaction_id),
  UNIQUE (reply_id, member_id, emoji),
  FOREIGN KEY (reply_id) REFERENCES comment_reply (reply_id),
  FOREIGN KEY (member_id) REFERENCES members (id)
);

SET @surrogate_parent_comment_id := (
  SELECT MIN(comment_id) FROM post_comment_surrogate
);
SET @reaction_member_id := (SELECT MIN(id) FROM members);

INSERT INTO comment_reply (comment_id, reply_no, body)
VALUES (@surrogate_parent_comment_id, 1, '대리 키 답글');

SET @reply_id := LAST_INSERT_ID();

INSERT INTO reply_reaction (reply_id, member_id, emoji)
VALUES (@reply_id, @reaction_member_id, '좋아요');

반응은 한 ID로 참조하고 댓글·답글의 부모 범위 중복은 각 UNIQUE가 막습니다.

답글 순번과 회원 반응 중복 실패
INSERT INTO comment_reply (comment_id, reply_no, body)
VALUES (@surrogate_parent_comment_id, 1, '중복 답글');
-- ERROR 1062: (comment_id, reply_no) UNIQUE 중복
INSERT INTO reply_reaction (reply_id, member_id, emoji)
VALUES (@reply_id, @reaction_member_id, '좋아요');
-- ERROR 1062: (reply_id, member_id, emoji) UNIQUE 중복
개선 실행 결과
comment reference: comment_id
reply reference: reply_id
reaction reference: reaction_id
duplicate reply_no in comment: ERROR 1062
duplicate same member emoji on reply: ERROR 1062

대리 키만으로는 다른 댓글의 답글을 섞는 복합 업무 조건을 자동 확인하지 않습니다.

각 FK가 직접 부모를 가리키고 필요한 테넌트·소유자 일치 규칙은 복합 UNIQUE/FK 또는 서비스 검증으로 보강합니다.

순번은 표시 순서일 뿐 정체성이 아니므로 변경해도 reply_id가 고정됩니다.

이 의미가 요구와 일치하지 않으면 복합 식별안을 유지합니다.

조회·재정렬·중복을 두 안으로 비교하기

  1. DDL 폭 — 두 안의 PK·FK 열 수와 index_length를 비교합니다.
  2. 조회reaction_id 단독과 네 열 조회 계획을 비교합니다.
  3. 재정렬 — 댓글 순서 UPDATE 뒤 답글 URL이 유지되는지 봅니다.
  4. 중복 — 업무 UNIQUE 위반 예제 데이터가 모두 거절되는지 확인합니다.

키 전파 폭 측정

키 열 수와 인덱스 크기를 감으로 말하지 않고 메타데이터에서 추출해 모델 결정 기록에 붙입니다.

PK·FK column 수 비교
SELECT table_name, constraint_name, COUNT(*) AS column_count
FROM information_schema.key_column_usage
WHERE table_schema = DATABASE()
  AND table_name IN (
    'comment_reply_identifying', 'reply_reaction_identifying',
    'comment_reply', 'reply_reaction'
  )
GROUP BY table_name, constraint_name
ORDER BY table_name, constraint_name;

SELECT table_name, index_length
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name LIKE '%reaction%';

식별 반응 PK/FK는 4/3 열, 비식별 반응 PK/FK는 1/1 열입니다.

index_length는 샘플 행 수를 맞춘 뒤 비교해야 합니다.

같은 행 수로 인덱스 폭 측정하기

  1. 3단계 DDL — 식별안과 비식별안을 모두 만들고 SHOW 인덱스를 저장합니다.
  2. 행 수 적재 — 동일 10만 반응을 넣어 index_length·삽입 시간을 비교합니다.
  3. 순번 변경 — 하위 FK ON UPDATE와 대리 ID 고정의 잠금 범위를 비교합니다.
  4. 오류 예제 데이터 — 고아·중복·다른 테넌트 참조를 각각 시도합니다.

클러스터형 PK가 보조 리프에 남기는 값

넓은 자연 키가 클러스터형 PK이면 보조 리프에도 복제됩니다.

반면 대리 PK를 추가하면 조합 UNIQUE라는 별 B+Tree가 생기므로 무조건 공간이 작아지는 것은 아닙니다.

실제 열 폭과 인덱스 수를 계산합니다.

식별 계층이 파티션·테넌트 경계를 자연스럽게 포함하는 장점도 있습니다.

tenant_id를 모든 키에 포함해 교차-테넌트 참조를 구조적으로 막는다면 전파 비용과 보안 이득을 같이 평가합니다.

공개 ID·UUID·순번 재사용 예외

  • AUTO_INCREMENT ID를 외부에 노출할 때 추측 가능성이 권한 문제를 대신 해결하지는 않습니다.
  • UUID는 분산 생성 장점과 클러스터형 지역성 비용을 비교합니다.
  • 순번 재사용이 허용되면 삭제된 reply_no와 감사 이력 의미를 정의합니다.
  • 이모지 정렬 규칙이 서로 다른 표현을 같은 값으로 보는지 UNIQUE 의미를 테스트합니다.

깊은 계층의 키 전략 비교

선택얻는 효과감수할 비용적합한 조건
깊은 식별 계층조상 문맥이 키에 명시폭·변경 전파얕고 외부 참조 없을 때
단계별 대리 ID짧은 FK·안정 URL업무 UQ 추가참조와 재정렬이 많을 때
복합 테넌트 키경계 구조적 강제모든 인덱스 폭보안 격리가 최우선일 때
서비스 소유 검증DDL 단순화경쟁 조건·우회 경로DB 복합 FK가 과도할 때

PK 교체 마이그레이션을 단계화하기

키 마이그레이션은 새 ID 백필 후 이중 키 UNIQUE와 FK를 검증하고 애플리케이션 읽기/쓰기를 순차 전환합니다.

한 번에 PK를 교체하지 않습니다.

FK 열 유형·부호 없음 속성이 정확히 같아야 하며 대형 ALTER의 온라인 가능성과 복제본 지연을 확인합니다.

다음 문서는 일대일 공유 PK와 다대다 연결 테이블에서 식별 관계가 오히려 간결한 사례를 살펴봅니다.

모든 관계를 한 전략으로 통일하지 않습니다.


복합 키 전파 허용 기준

판단 축확인할 질문
깊이복합 키가 몇 세대 하위 FK까지 전달되는가?
PK 바이트가 보조 인덱스마다 얼마나 복제되는가?
재정렬표시 순서 변경이 정체성 변경이어야 하는가?
외부 참조로그·API·감사가 자식을 단독 식별하는가?
경계키 전파가 테넌트·소유권을 구조적으로 지키는 이득이 있는가?

복합 키의 의미적 명확성과 대리 키의 참조 안정성을 실제 깊이·인덱스·변경 워크로드로 비교합니다.

‘현대적’이라는 말만으로 모든 복합 키를 제거하지 않습니다.


연습 문제

plan_item의 점검 목록과 checklist_reply 두 단계를 설계하세요.

항목 순서는 변경되고 답글은 외부 알림에서 참조됩니다.

키 전략을 선택하고 후보 키를 보존하세요.

해설과 예시 답안

plan_item_id, checklist_id, reply_id 대리 PK를 쓰고 (plan_item_id, sequence_no)를 UNIQUE로 둡니다.

순서 변경이 답글 식별자를 바꾸지 않으며 알림은 reply_id를 참조합니다.

CREATE TABLE checklist_reply (
  reply_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  checklist_item_id BIGINT UNSIGNED NOT NULL,
  author_id BIGINT UNSIGNED NOT NULL,
  body VARCHAR(500) NOT NULL,
  created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  PRIMARY KEY (reply_id),
  FOREIGN KEY (checklist_item_id)
    REFERENCES concept_checklist_item (checklist_item_id)
);

체크리스트 순번 변경 뒤 reply_id가 유지되고, 고아 답글이 FK로 거절되는지 확인합니다.


핵심 정리

  • 식별 키는 하위 관계마다 열과 인덱스 폭을 전파합니다.
  • 대리 ID는 참조를 짧게 하지만 업무 UNIQUE를 대신하지 않습니다.
  • 재정렬·외부 URL·감사는 안정적인 자식 ID의 근거가 됩니다.
  • 키 전략은 메타데이터와 워크로드로 비용을 측정해 결정합니다.

다음 문서에서는 공유-PK 일대일과 다대다 연결 키처럼 식별 관계가 자연스러운 사례를 비교합니다.