본문 바로가기

선택 심화: 분산·객체·멀티미디어 데이터

단편화와 복제, 객체와 JSON, 멀티미디어의 원본과 설명 데이터가 각각 무엇을 맡는지 구분하고, 한 서버의 작은 예제로 단편화의 복원 조건을 확인합니다. 저장 방식을 이름이 아니라 사실의 위치와 복원 조건, 실패하는 지점으로 비교합니다.

데이터가 커지거나 모양이 달라졌다고 관계형 데이터베이스를 바로 버려야 하는 것은 아닙니다. 어떤 사실을 저장하고 어떤 일관성과 조회, 수명의 규칙이 필요한지를 먼저 정합니다. 이 글은 실제 분산 클러스터를 구축하는 안내가 아니라, 저장 방식을 판단하기 위한 선택 심화입니다.

수평 단편화와 수직 단편화

수평 단편화(horizontal fragmentation)는 열 구조가 같은 행들을 나누는 것입니다. 회원 번호의 홀짝으로 두 조각을 나누면 각 회원은 한 조각에만 속하고, 두 조각을 합치면 전체 회원이 됩니다. 수직 단편화(vertical fragmentation)는 속성을 나누는 것으로, 연락처와 프로필을 나누더라도 같은 회원인지 JOIN할 수 있는 공통 키를 보존해야 합니다.

단편화할 때는 원래 사실이 하나도 빠지지 않는지(완전성), 다시 전체를 만들 수 있는지(재구성 가능성), 의도한 중복과 비중복의 조건이 지켜지는지를 확인합니다. 수직 분해에서 공통 키가 겹치는 것은 복원에 필요한 중복입니다. 다만 행을 나눈 표가 모두 실제로 다른 서버에 있다는 뜻은 아닙니다.

한 서버에서 단편화의 논리만 관찰
CREATE DATABASE IF NOT EXISTS cs_db_lab;
DROP TABLE IF EXISTS cs_db_lab.cs_frag_members;
CREATE TABLE cs_db_lab.cs_frag_members (
  id INT PRIMARY KEY,
  name VARCHAR(20) NOT NULL,
  city VARCHAR(20) NOT NULL
);
INSERT INTO cs_db_lab.cs_frag_members VALUES
  (1, '민', '서울'), (2, '준', '부산'), (3, '솔', '대전');

WITH odd_part AS (SELECT * FROM cs_db_lab.cs_frag_members WHERE MOD(id, 2) = 1),
even_part AS (SELECT * FROM cs_db_lab.cs_frag_members WHERE MOD(id, 2) = 0),
restored AS (SELECT * FROM odd_part UNION ALL SELECT * FROM even_part)
SELECT * FROM restored ORDER BY id;

WITH identity_part AS (SELECT id, name FROM cs_db_lab.cs_frag_members),
location_part AS (SELECT id, city FROM cs_db_lab.cs_frag_members)
SELECT i.id, i.name, l.city
FROM identity_part AS i
JOIN location_part AS l ON l.id = i.id
ORDER BY i.id;
id | name | city
1  | 민   | 서울
2  | 준   | 부산
3  | 솔   | 대전

두 쿼리는 모두 이 세 행을 돌려줍니다. 홀수 조각의 1과 3, 짝수 조각의 2를 UNION ALL로 합치면 수평 단편화가, 이름 조각과 도시 조각을 id로 JOIN하면 수직 단편화가 원래 표를 되살립니다.

이것은 한 서버의 CTE 조회이므로, 네트워크의 장애나 서버별 복제의 지연, 분산 트랜잭션을 검증한 것은 아닙니다. 번호의 분포가 한쪽으로 쏠리면 저장과 조회의 부하도 쏠리므로, 나누는 규칙은 실제 접근의 양상과 함께 검토합니다.

복제와 분산 트랜잭션

복제(replication)는 같은 사실의 사본을 여러 곳에 두는 것입니다. 단편화와 달리 같은 행이 의도적으로 여러 곳에 있을 수 있고, 주 서버에서 커밋한 직후에 지연된 사본을 읽으면 이전의 값이 보일 수 있습니다. "복제본이 있으니 언제나 최신이다"라는 결론은 복제의 방식과 읽기의 정책을 빠뜨린 것입니다.

여러 저장 위치를 한 작업으로 바꾸려면, 전체를 커밋할지를 조율해야 합니다. 2단계 커밋(two-phase commit)은 참여자를 먼저 준비시킨 뒤 결과를 확정하는 원리이며, 장애 중의 대기와 복구라는 문제와 운영의 비용이 있습니다. 단일 InnoDB 트랜잭션 두 개를 차례로 커밋한다고 분산된 원자성이 저절로 생기지는 않습니다.

반대로 모든 업무가 곧바로 전역 트랜잭션을 요구하지도 않습니다. 주문의 접수와 알림처럼 재시도와 멱등 처리가 가능한 후속 작업은, 각자가 맡을 일을 나누어 설계할 수 있습니다. 준비의 투표와 조정자의 영속적인 결정, 불확실한 상태의 대기를 실제 XA의 상태로 이어서 보려면 분산 트랜잭션과 XA 복구를 읽으며, 그 글은 별도의 새 데이터 디렉터리에서 참여자 하나의 엔진 복구를 실험하므로 이 글의 논리적 단편화 SQL이나 여러 서버의 네트워크 장애 실험과는 구분됩니다.

객체 모델과 저장 모델

객체지향 데이터 모델은 객체의 정체성과 속성, 메서드, 상속을 다룹니다. 객체관계 데이터베이스의 확장 타입과, 일반 SQL 테이블을 ORM으로 읽어 객체로 만드는 것은 서로 다른 층의 선택이며, MySQL 테이블에 type 열이나 JSON을 넣었다고 메서드의 실행과 객체의 정체성까지 지원하는 객체 데이터베이스가 되지는 않습니다.

17장의 상속 모델은 구체 테이블과 단일 테이블, 조인 테이블로 객체의 데이터를 관계 구조에 나타냅니다. 방식마다 외래 키와 유형의 제약, 조회의 비용이 다르므로, 객체와 테이블의 1:1 대응을 고정된 규칙으로 생각하지 않습니다.

멀티미디어의 원본과 설명 데이터

이미지의 원본 바이트와 해상도, 형식 같은 구조 정보, 검색용 제목과 태그, 권리와 출처 같은 설명 정보는 역할이 다릅니다. 원본을 BLOB로 데이터베이스 안에 둘 수도 있고, 별도의 객체 저장소에 두고 데이터베이스에는 식별자와 키, 해시, 수명만 저장할 수도 있습니다. 따로 저장하면 데이터베이스 행의 커밋과 원본의 업로드가 서로 다르게 실패할 수 있으므로, 후보와 완료의 상태, 재시도와 고아 정리의 규칙을 정해야 합니다.

방식장점이 드러나는 조건확인할 비용과 규칙
관계형 열과 자식 표값과 관계, 제약이 분명하다JOIN과 쓰기의 담당
JSON항목마다 확장 구조가 다르다스키마 검증과 조회 경로, 인덱스
데이터베이스 안의 BLOB원본과 행을 함께 관리하려 한다커지는 백업과 전송, 복원의 비용
외부 원본과 데이터베이스 메타데이터대용량 원본의 배포와 수명을 나눈다업로드와 행의 불일치, 삭제 순서

유형의 이름만으로 제품을 추천하지 않고, 실제 작업의 양상과 필요한 무결성, 복원의 방법을 비교합니다. 검색 인덱스와 요약이 원본에서 다시 만들 수 있는 파생물인지도 표시해 둡니다.

더 깊이: InnoDB가 큰 BLOB을 저장하고 주고받는 방식

BLOB 계열의 타입은 담을 수 있는 크기로 나뉩니다. TINYBLOB은 2^8바이트 미만, BLOB은 2^16바이트 미만, MEDIUMBLOB은 2^24바이트 미만, LONGBLOB은 2^32바이트 미만의 값을 담고, 길이를 적는 데 각각 1바이트에서 4바이트를 더 씁니다. MySQL의 행 크기 한도인 65,535바이트를 계산할 때 BLOB과 TEXT 열은 내용의 크기와 상관없이 9바이트에서 12바이트만 차지합니다. 내용은 행 버퍼와 다른 곳에서 따로 다루기 때문입니다.

InnoDB의 기본 행 형식인 DYNAMIC은 긴 가변 길이 값을 페이지 밖의 오버플로 페이지에 통째로 두고, 클러스터형 인덱스의 레코드에는 20바이트의 포인터만 남깁니다. 행이 페이지에 들어가지 않을 만큼 길면 가장 긴 열부터 페이지 밖으로 옮기고, 40바이트 이하인 BLOB과 TEXT는 행 안에 둡니다(InnoDB 행 형식). 그래서 원본 바이트를 같은 테이블에 두어도 설명 정보만 읽는 조회는 레코드를 작게 읽을 수 있지만, 원본을 읽을 때는 오버플로 페이지를 더 읽어야 합니다.

주고받는 크기에도 한도가 있습니다. 클라이언트와 서버는 각자 max_allowed_packet을 가지며, MySQL 8.4 서버의 기본값은 67108864바이트(64MB), mysql 클라이언트의 기본값은 16MB이고, 서버와 클라이언트가 주고받을 수 있는 가장 큰 패킷은 1073741824바이트(1GB)입니다. 매뉴얼은 큰 BLOB을 쓰려면 서버의 값을 쓰려는 가장 큰 BLOB만큼 키우고, 큰 패킷을 다루려면 클라이언트의 값도 함께 키워야 한다고 적습니다. LONGBLOB이 4GB 가까이 담을 수 있는 타입이라도 값 하나를 주고받는 크기는 이 한도를 넘을 수 없으므로, 아주 큰 원본은 표의 넷째 방식처럼 객체 저장소에 두는 편이 자연스럽습니다.

정리

  • 수평 단편화는 행을, 수직 단편화는 속성을 나누며, 둘 다 완전성과 재구성 가능성을 확인해야 합니다.
  • 수직 단편화에서 공통 키의 중복은 원래 표를 되살리는 데 필요합니다.
  • 복제의 사본은 지연될 수 있고, 여러 저장소의 원자적 커밋에는 2단계 커밋 같은 조율이 필요합니다.
  • 테이블에 유형 열이나 JSON을 넣는다고 객체 데이터베이스가 되지는 않습니다.
  • 멀티미디어는 원본과 구조 정보, 설명 정보를 나누고, 따로 저장한다면 두 저장소의 실패 순서를 설계합니다.

연습 문제

1. 공통 키가 없는 수직 조각

회원의 이름과 도시를 따로 저장하면서 도시 조각에서 id를 지우면, 무엇을 되살릴 수 없나요?

해설 보기

도시 조각에 id가 없으면, 어느 도시가 어느 회원의 사실인지 안전하게 JOIN할 수 없습니다. 행의 순서로 맞추는 것은 관계형 모델이 보장하지 않으므로, 세 회원과 세 도시의 조합을 결정할 방법이 사라집니다. 수직 단편화의 공통 키는 지워도 되는 중복이 아니라, 원래 표를 되살리는 조건입니다.

2. 업로드는 성공하고 등록은 실패했을 때

미디어 원본의 업로드는 성공했는데, 그 뒤 데이터베이스의 등록이 실패했습니다. 어떤 상태가 남고, 무엇을 정해 두어야 하나요?

해설 보기

데이터베이스가 참조하지 않는 원본이 객체 저장소에 남습니다. 요청 ID나 후보의 기록으로 같은 원본을 다시 등록할 수 있게 하거나, 보존 기간이 지난 뒤 참조되지 않는 원본을 정리하는 정책이 필요합니다. 반대 순서로 데이터베이스 행을 먼저 만든다면, 업로드가 끝나기 전까지 그 행을 후보 상태로 두어 화면에 보이지 않게 합니다.

3. 사본에서 보이지 않는 새 글

복제의 사본에서 새 글이 보이지 않습니다. 이것이 원래 서버에서 커밋이 실패했다는 뜻일까요?

해설 보기

반드시 그렇지는 않습니다. 원래 서버에서는 커밋되었지만 사본에 아직 반영되지 않은 지연일 수 있습니다. 원래 서버에서 커밋되었는지와 사본이 어디까지 적용했는지를 따로 확인해야 하며, 방금 쓴 글을 바로 읽어야 하는 화면이라면 그 읽기를 원래 서버로 보내는 정책을 둡니다.

다음 글 분산 트랜잭션과 XA 복구에서는 2단계 커밋의 준비와 결정, 불확실한 상태를 MySQL의 XA 문장과 재시작 뒤의 복구로 확인합니다.