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

안동민 개발노트

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

회원 게시판 논리 모델

모든 관계를 식별로 만드는 거대 복합 키 오류를 고치고 대리 키·업무 UNIQUE·공유 PK 예외를 함께 쓰는 논리 모델을 완성합니다.

현대적 서비스는 짧고 안정적인 대리 키와 비식별 FK를 기본으로 많이 사용합니다.

API·이벤트·분산 쓰기에서 참조가 단순하기 때문입니다.

그러나 이것은 모든 관계에 대리 ID를 추가하라는 규칙도, 복합 업무 키를 버리라는 뜻도 아닙니다.

완성 모델에는 회원, posts, 댓글, 답글, 프로필, 태그, post_tags가 있습니다.

댓글·답글은 외부 참조와 재정렬을 고려해 대리 키, 프로필은 공유 PK, post_tags는 단순 조합 PK를 선택합니다.

각 선택의 예외 근거를 설계 기록에 남깁니다.


자연 키 전파의 문제

회원 이메일, 게시글 created_at, 댓글 순서, reply_no를 한 복합 PK로 누적하면 이메일 변경과 순서 변경이 계층 전체 식별자를 바꿉니다.

문자열·시각·순번이 모든 보조 인덱스와 이벤트 본문에 복사됩니다.

업무 속성을 끝까지 전파한 거대 식별 키
DROP TABLE IF EXISTS reply_key_explosion_bad;

INSERT INTO members (email, password_hash, name, status)
VALUES ('old@example.com', '$2b$12$logical.key.fixture',
        '논리 키 실습 회원', 'ACTIVE');

SET @logical_member_id := LAST_INSERT_ID();

CREATE TABLE reply_key_explosion_bad (
  member_email VARCHAR(255) NOT NULL,
  post_created_at DATETIME(6) NOT NULL,
  comment_sequence SMALLINT UNSIGNED NOT NULL,
  reply_no INT UNSIGNED NOT NULL,
  body VARCHAR(500) NOT NULL,
  PRIMARY KEY (
    member_email, post_created_at,
    comment_sequence, reply_no
  )
);

INSERT INTO reply_key_explosion_bad
  (member_email, post_created_at, comment_sequence, reply_no, body)
VALUES
  ('old@example.com', '2026-07-14 09:00:00', 1, 1,
   '자연 키를 복사한 답글');

UPDATE members
SET email = 'new@example.com'
WHERE id = @logical_member_id;
-- 별도 reply 자연 키에는 이전 email이 남는다.

회원 원본 이메일은 바뀌었지만 독립 FK가 아닌 복사 문자열은 자동으로 연결되지 않아 같은 회원의 키가 두 값으로 갈라집니다.

오류 실행 결과
member current email: new@example.com
reply key email: old@example.com
declared FK between them: none
PK width: long string + datetime + 2 numbers
identity updates required: entire descendant tree

자연 값을 열 이름으로 복사하는 것은 식별 관계가 아니라 중복 저장입니다.

식별 관계를 쓰더라도 실제 FK가 부모 후보 키를 참조해야 합니다.

안정적인 member_id를 전달하면 이메일 변경은 일반 속성 UPDATE로 끝납니다.

대리 키 모델도 테넌트·업무 소유권을 자동 보장하지 않습니다.

필요한 복합 UNIQUE와 FK를 선택적으로 두어 교차-소유자 참조를 막습니다.

복사 자연 키가 끊어진 지점 찾기

  1. 실제 FK — 복사 열이 선언된 후보 키를 참조하는지 확인합니다.
  2. 변경성 — 이메일·시각·순번이 업무에서 바뀔 수 있는지 봅니다.
  3. — PK와 이벤트 본문의 바이트를 계산합니다.
  4. 예외 — 공유 PK·조합 PK가 더 간결한 관계를 별도로 찾습니다.

대리 키와 업무 제약

독립 엔티티와 외부 참조가 많은 관계에는 BIGINT 대리 PK를 두고 자연 후보 키를 UNIQUE로 보존합니다.

FK는 짧고 안정적인 ID를 참조합니다.

종속 1:1 프로필과 단순 M:N 연결처럼 조합 자체가 정체성인 곳은 공유·조합 PK를 예외로 사용합니다.

‘모든 PK는 BIGINT’ 같은 규칙보다 키 결정 기록이 중요합니다.

후보 키, 변경성, 참조 깊이, 삭제 수명, 테넌트 경계, 예상 쿼리를 문서마다 남기면 예외가 우연이 아니라 의식적인 설계가 됩니다.

대리 키 기본값과 식별 예외 함께 쓰기

  1. 엔티티 분류 — 독립 수명·외부 참조가 큰 엔티티부터 대리 PK 후보로 둡니다.
  2. 업무 키 — 이메일·게시글 조합·순서 조합 UNIQUE를 누락하지 않습니다.
  3. 예외 표시 — 공유 PK·조합 PK를 쓰는 이유와 전환 신호를 기록합니다.
  4. 검증 — 중복·고아·소유 불일치·삭제 정책 예제 데이터를 실행합니다.

혼합 키 전략

회원/게시글/댓글/답글/태그는 대리 PK, 프로필은 회원 공유 PK, post_tags는 조합 PK를 둡니다.

아래 핵심 DDL은 관계 선택을 한눈에 검증합니다.

대리 키와 식별 예외를 결합한 핵심 DDL
DROP TABLE IF EXISTS logical_reply;
DROP TABLE IF EXISTS logical_comment;

CREATE TABLE logical_comment (
  comment_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  post_id BIGINT UNSIGNED NOT NULL,
  sequence_no SMALLINT UNSIGNED NOT NULL,
  summary VARCHAR(240) NOT NULL,
  PRIMARY KEY (comment_id),
  UNIQUE (post_id, sequence_no),
  FOREIGN KEY (post_id) REFERENCES posts (id)
);
CREATE TABLE logical_reply (
  reply_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  comment_id BIGINT UNSIGNED NOT NULL,
  author_id BIGINT UNSIGNED NOT NULL,
  body VARCHAR(500) NOT NULL,
  PRIMARY KEY (reply_id),
  FOREIGN KEY (comment_id) REFERENCES logical_comment (comment_id),
  FOREIGN KEY (author_id) REFERENCES members (id)
);
-- member_profiles shared PK와 post_tags pair PK는 ch11-3 정의를 유지한다.

SET @logical_post_id := (SELECT MIN(id) FROM posts);

INSERT INTO logical_comment (post_id, sequence_no, summary)
VALUES (@logical_post_id, 1, '논리 모델 댓글');

SET @logical_comment_id := LAST_INSERT_ID();

INSERT INTO logical_reply (comment_id, author_id, body)
VALUES (@logical_comment_id, @logical_member_id, '안정적인 대리 키 답글');

UPDATE members
SET email = 'stable-id@example.com'
WHERE id = @logical_member_id;

UPDATE logical_comment
SET sequence_no = 2
WHERE comment_id = @logical_comment_id;

이메일·순서 변경이 reply_id를 바꾸지 않고, 같은 게시글 순서 중복은 UNIQUE가 계속 막습니다.

업무 후보 키와 부모 FK 위반 실패
INSERT INTO logical_comment (post_id, sequence_no, summary)
VALUES (@logical_post_id, 2, '중복 순번');
-- ERROR 1062: (post_id, sequence_no) UNIQUE 중복
INSERT INTO logical_reply (comment_id, author_id, body)
VALUES (999999, @logical_member_id, '고아 답글');
-- ERROR 1452: logical_reply.comment_id FK
개선 실행 결과
email UPDATE: member row 1 affected, descendant key updates 0
comment sequence UPDATE: comment row 1 affected, reply FK updates 0
duplicate post+sequence: ERROR 1062
orphan reply: ERROR 1452
profile duplicate / post-tag duplicate: ERROR 1062

논리 모델은 MySQL 타입 최적화 전에도 관계·키·NULL·삭제 규칙이 완전해야 합니다.

ch12 물리 모델에서 이름과 인덱스를 바꾸더라도 후보 키와 카디널리티는 유지합니다.

복합 소유권을 DB에서 강제하려면 UNIQUE(comment_id, post_id) 같은 중복 키와 복합 FK가 필요할 수 있습니다.

비용이 큰 경우 서비스 트랜잭션과 정기 일치 검사를 채택한 근거를 남깁니다.

변경·중복·고아 쿼리를 한 묶음으로 검증

  1. 키 목록 — 각 테이블의 PK·후보 키·FK를 표로 만듭니다.
  2. 관계 문장 — 최소·최대 수와 삭제 정책을 양방향으로 읽습니다.
  3. 변경 예제 데이터 — 이메일·순서·소유자 변경이 하위 식별자에 미치는 영향을 실행합니다.
  4. 예외 리뷰 — 공유·조합 PK의 독립 ID 전환 신호를 합의합니다.

논리 모델 검증 쿼리

고아, 업무 중복, 프로필 중복, 연결 중복은 모두 0행이어야 합니다.

모델 검토를 한 번의 그림 회의가 아니라 반복 가능한 진단으로 만듭니다.

핵심 관계 무결성 대사
SELECT c.reply_id
FROM logical_reply AS c
LEFT JOIN logical_comment AS p ON p.comment_id = c.comment_id
WHERE p.comment_id IS NULL;

SELECT post_id, sequence_no, COUNT(*)
FROM logical_comment
GROUP BY post_id, sequence_no HAVING COUNT(*) > 1;

SELECT member_id, COUNT(*)
FROM member_profiles
GROUP BY member_id HAVING COUNT(*) > 1;

SELECT post_id, tag_id, COUNT(*)
FROM post_tags
GROUP BY post_id, tag_id HAVING COUNT(*) > 1;

네 쿼리 모두 0행입니다.

FK·PK·UNIQUE가 이미 위반을 막지만 마이그레이션·제약 조건 비활성 이력을 탐지하는 운영 일치 검사로도 사용합니다.

논리 모델의 혼합 키 선택표

선택얻는 효과감수할 비용적합한 조건
대리 PK 기본짧고 안정적 참조업무 UQ 별도대부분 독립 엔티티일 때
공유 PK 예외종속 1:1 간결독립화 어려움확장 수명이 부모와 같을 때
조합 PK 예외단순 연결 정체성하위 참조 넓음연결 자체 참조 없을 때
복합 소유 FKDB의 경계 강제중복 열·인덱스보안·테넌트 규칙이 중요할 때

짧은 ID 뒤에 남는 소유권 책임

대리 키가 도메인 경계를 숨길 수 있어 API에서 ID만 받아 업데이트할 때 반드시 소유권 조건을 함께 둡니다.

WHERE reply_id=? AND author_id=? 또는 테넌트 조건식을 빼면 짧은 키가 보안 취약점을 만듭니다.

UUIDv7·분산 ID는 여러 쓰기 주체의 생성 이점이 있지만 저장 크기·페이지 분할·디버깅 비용이 있습니다.

단일 MySQL AUTO_INCREMENT 요구에서 유행만으로 바꾸지 않습니다.

소프트 삭제·병합·버전·연결 원본 예외

  • 소프트 삭제 행의 자연 키 재사용은 UNIQUE와 충돌하므로 활성 키 전략을 별도 설계합니다.
  • 병합된 회원 두 ID를 통합할 때 모든 FK를 한 트랜잭션/배치로 옮기고 후보 키 충돌을 해결합니다.
  • 공유 PK 프로필을 여러 버전으로 확장하면 (member_id, version) 또는 profile_version 엔티티가 필요합니다.
  • post_tags에 같은 태그를 여러 원본으로 중복 허용하면 조합 후보 키에 원본이 포함되는지 관계 의미를 다시 묻습니다.

스키마 불일치와 외부 ID 수명 관리

스키마 정적 검사로 FK 열 타입, 제약 조건 이름, 후보 UNIQUE 누락을 감시합니다.

모델 다이어그램과 실제 DDL 불일치도 정기 비교합니다.

키를 외부 이벤트에 발행하면 재키 마이그레이션이 훨씬 비싸집니다.

내부 ID와 공개 ID를 분리할 필요를 API 수명과 함께 검토합니다.

키 카탈로그와 변경 예제 데이터 만들기

  1. 키 카탈로그 — 모든 테이블의 대표 PK와 후보 키 근거를 한 줄씩 씁니다.
  2. 변경 시나리오 — 이메일·순서·태그명 변경의 영향 행 수를 예측하고 실행합니다.
  3. 교차-소유자 — 다른 회원의 게시글·자료를 잘못 연결하는 예제 데이터를 만듭니다.
  4. 전환 신호 — 조합 연결에 검토 답글이 추가될 때 대리 ID 도입 마이그레이션을 설계합니다.

다음 장은 완성한 논리 모델에서 부분·이행 종속을 제거하고, 이름·타입·인덱스·역정규화를 MySQL 물리 모델로 확정합니다.


논리 모델 검토 기준

판단 축확인할 질문
대표 키독립 엔티티 참조가 짧고 변경에 안정적인가?
업무 키자연·복합 후보 키를 UNIQUE로 보존했는가?
예외공유·조합 PK의 종속 수명 근거가 있는가?
소유권짧은 ID 사용에도 테넌트·소유자 경계를 지키는가?
변경재키·삭제·병합 시나리오와 마이그레이션 경로가 있는가?

비식별 관계를 기본으로 쓰되 식별 관계가 더 정확하고 단순한 곳은 예외로 남깁니다.

일관성은 모든 테이블의 PK 모양이 같은 것이 아니라 결정 기준과 검증 방법이 같은 것입니다.


연습 문제

게시글의 공유 링크는 독립 URL 토큰으로 참조되고 게시글당 여러 개이며 만료·폐기 상태가 있습니다.

post_id+순서 식별 키와 share_link_id 대리 키를 비교해 설계하세요.

해설과 예시 답안

외부 토큰·폐기 수명주기와 단독 참조가 있으므로 share_link_id 대리 PK, 토큰 UNIQUE, post_id FK를 선택합니다.

게시글별 표시 순서가 필요하면 조합 UNIQUE를 별도 둡니다.

CREATE TABLE post_share_link (
  share_link_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  post_id BIGINT UNSIGNED NOT NULL,
  public_token BINARY(16) NOT NULL,
  status VARCHAR(16) NOT NULL,
  expires_at DATETIME(6) NULL,
  PRIMARY KEY (share_link_id),
  UNIQUE (public_token),
  FOREIGN KEY (post_id) REFERENCES posts (id),
  CHECK (status IN ('ACTIVE', 'REVOKED', 'EXPIRED'))
);

토큰 중복과 고아 게시글이 거절되고 연결 폐기 뒤 게시글 식별자는 그대로인지 확인합니다.

토큰 조회 인덱스와 만료 배치 인덱스는 ch12에서 결정합니다.


핵심 정리

  • 대리 키 기본값은 업무 후보 키와 함께 사용합니다.
  • 공유 PK·조합 PK는 종속 수명이 강한 관계의 유효한 예외입니다.
  • 짧은 ID가 소유권·테넌트 검증을 자동 해결하지 않습니다.
  • 논리 모델은 변경 예제 데이터와 0행 일치 검사 쿼리로 검증합니다.

다음 장에서는 함수 종속과 정규형을 적용하고 논리 모델을 MySQL 8.4 물리 DDL로 완성합니다.