G-1: RDBMS는 어디까지 진화했나 — JSON·벡터·모던 SQL 둘러보기
목차 21
안녕하세요, 홍순구 튜터입니다. 지난 시간엔 인스타그램 쿼리를 Oracle 에서 MySQL 8.4 로 통째로 옮기면서, 빈 문자열 함정부터 DATE_FORMAT·CONCAT·ANSI JOIN·WITH RECURSIVE 까지 두 방언을 자유롭게 오가는 힘을 길렀죠. 같은 인스타그램을 어느 데이터베이스에서든 짤 수 있게 됐어요.
오늘은 데이터베이스 과목의 마지막 시간이에요. 그래서 시험 문법을 더 파는 대신, 한 발 물러서서 "요즘 데이터베이스가 어디까지 왔나" 를 둘러봐요. 표(행과 열)에 딱 맞지 않는 데이터를 한 칸에 담는 JSON, 그리고 이미지나 문장을 숫자로 바꿔 "비슷한 것" 을 찾는 벡터 검색까지요. 우리가 배운 관계형 SQL 이 모던 세계에서 어떻게 넓어지는지 보면서 과목을 마무리해요.
오늘의 여정
① 표를 넘어서는 데이터 — 왜 SQL 이 진화하나
② JSON 컬럼 — 한 칸에 통째로 담기
③ JSON 꺼내 쓰기 — 점 표기법과 JSON_TABLE
④ 표로 저장하고 문서로 읽기 — JSON Relational Duality
⑤ 표준이 따라잡은 것들 — BOOLEAN 과 그래프 질의
⑥ 비슷한 걸 찾는다는 것 — 임베딩과 벡터
⑦ 벡터 검색 — '비슷한 게시물' 찾기
🌟 이 G 카테고리는 SQLD 시험 범위가 아니에요. "데이터베이스가 어디까지 진화했나" 를 둘러보는 교양 시간이에요. 시험에 나오는 건 지금까지 배운 전통 SQL 이 전부이니, 오늘은 마음 편히 큰 그림만 잡아요.
💡 오늘 수업의 핵심 — "관계형 SQL 이 JSON(반정형)·벡터(비정형/AI)·모던 표준 기능을 어떻게 품는지 둘러보고, 전통 SQL 이 여전히 그 토대임을 확인한다"
🎯 학습 목표
- 표에 안 맞는 데이터(JSON)와 "비슷함" 을 찾는 데이터(벡터)를 관계형 데이터베이스가 어떻게 품는지 큰 그림을 잡는다.
- JSON 컬럼·점 표기법·
JSON_TABLE로 가변 구조 데이터를 담고 꺼낸다. - 임베딩과
VECTOR_DISTANCE로 "비슷한 게시물 찾기" 의 원리를 이해한다. (이 카테고리는 시험 범위 밖 — 동향/교양)
Step 1: "표를 넘어서는 데이터, 그리고 진화하는 SQL"
B-4 에서 정규화를 배울 때, 우리는 데이터를 깔끔한 표로 쪼개는 법을 익혔어요. 회원은 회원 표, 게시물은 게시물 표, 컬럼 하나에 값 하나씩. 관계형 데이터베이스는 이렇게 표(행과 열)에 딱 맞는 데이터 를 다루는 데 아주 강해요. 지금까지 배운 SQL 이 전부 그 위에서 돌아갔죠.
그런데 세상의 데이터가 다 표에 맞는 건 아니에요. 인스타그램 게시물을 떠올려 봐요. 어떤 게시물엔 촬영 위치가 있고, 어떤 건 카메라 기종이 있고, 어떤 건 필터 이름이 있고, 어떤 건 아무것도 없어요. 게시물마다 딸린 정보의 구조가 제각각이에요. 이걸 고정 컬럼으로 만들려면 location·device·filter·camera_model… 컬럼이 끝없이 늘어나고, 대부분 빈칸(NULL)으로 남아요.
데이터의 모양을 세 가지로 나눠 보면 이렇게 정리돼요.
| 데이터 모양 | 특징 | 인스타 예 | 다루는 곳 |
|---|---|---|---|
| 정형(structured) | 표에 딱 맞음 | 회원·게시물 (행과 열) | 지금까지 배운 SQL |
| 반정형(semi-structured) | 구조가 제각각 | 게시물 메타데이터 (JSON) | 오늘 ②③④ |
| 비정형(unstructured) | 표로 못 쪼갬 | 이미지·문장 (벡터) | 오늘 ⑥⑦ |
한때는 이런 반정형·비정형 데이터를 다루려고 NoSQL 이라는 다른 종류의 데이터베이스(문서 데이터베이스 등)가 따로 등장했어요. "관계형은 한계가 있다" 면서요. 그런데 재미있게도, 관계형 데이터베이스가 그 기능들을 하나씩 자기 안으로 흡수 하기 시작했어요. 표는 표대로 잘하면서, JSON 도 담고 벡터도 담는 쪽으로요.
SQL 표준도 같이 진화했어요. 가장 최신 표준인 SQL:2023(2023년 발표)에는 JSON 데이터 타입이 정식으로 들어왔고, 데이터를 그래프처럼 질의하는 기능(SQL/PGQ)도 새로 추가됐어요. Oracle 은 23ai/26ai 버전에서 JSON·벡터·표준 BOOLEAN 타입을 SQL 안으로 들여왔고요. 오늘 우리가 둘러볼 게 바로 그 모던 기능들이에요.
💡 한 줄 정리 — 관계형 데이터베이스는 표에 강하지만, 표에 안 맞는 반정형(JSON)·비정형(벡터) 데이터까지 품는 쪽으로 진화해 왔다. SQL:2023 과 Oracle 23ai/26ai 가 그 흐름의 한 장면이다.
🙋 학생 질문 — "튜터님, 그럼 이제 정규화는 안 해도 되는 거예요?"
아니에요, 정반대예요. 정규화로 깔끔하게 쪼갠 표가 여전히 중심이에요. JSON 이나 벡터는 "표로 만들기엔 구조가 너무 제각각이거나, 표로 쪼갤 수 없는 데이터" 를 보조로 담는 데 써요. 회원·게시물·팔로우 같은 핵심 데이터는 지금까지처럼 정규화된 표로 두고, 게시물에 딸린 들쭉날쭉한 메타데이터만 JSON 한 칸에 담는 식이죠. 오늘 보겠지만 Oracle 은 아예 "정규화된 표를 JSON 문서처럼 읽는" 기능(④번)까지 줘요. 표를 버리는 게 아니라, 표 위에 선택지를 얹는 거예요.
Step 2: "JSON 컬럼 — 한 칸에 통째로 담기"
게시물에 딸린 메타데이터를 담아 볼게요. 위치·기기·필터·태그처럼 게시물마다 있을 수도 없을 수도 있는 정보들이에요. 고정 컬럼을 잔뜩 만드는 대신, metadata 라는 JSON 타입 컬럼 하나를 붙여요. D-5 에서 category 컬럼을 ALTER TABLE ... ADD 로 붙였던 것과 똑같은 방식이에요.
-- sql/ddl/06_modern_json_vector.sql
-- 게시물 가변 메타데이터 (네이티브 JSON 타입)
ALTER TABLE post ADD (metadata JSON);
이제 게시물마다 구조가 다른 메타데이터를 한 칸에 통째로 넣어요. JSON 은 중괄호 {} 안에 "이름:값" 쌍을 적고, 값으로 배열 [] 이나 또 다른 중괄호도 넣을 수 있어요.
-- sql/seed/06_modern.sql
UPDATE post SET metadata = JSON('{"location":"제주","device":"iPhone 16 Pro","filter":"없음","tags":["여행","제주","바다"],"geo":{"lat":33.45,"lng":126.57}}') WHERE post_id = 12;
UPDATE post SET metadata = JSON('{"device":"필름 카메라","camera":{"model":"Canon AE-1","iso":400}}') WHERE post_id = 5;
제주 여행 게시물(12번)엔 위치·기기·필터·태그 배열·좌표까지 들어갔고, 필름 사진 게시물(5번)엔 위치도 태그도 없이 카메라 정보만 들어갔어요. 같은 컬럼인데 안에 든 구조가 완전히 다르죠. 이게 고정 컬럼으로는 안 되고 JSON 으로만 자연스러운 가변 스키마 예요.
저장된 12번의 메타데이터를 통째로 꺼내 보면, 우리가 넣은 그대로 중첩 구조까지 살아 있어요.
{
"location" : "제주",
"device" : "iPhone 16 Pro",
"filter" : "없음",
"tags" : [ "여행", "제주", "바다" ],
"geo" : { "lat" : 33.45, "lng" : 126.57 }
}
이번 모듈에선 게시물 50개 중 7개에만 메타데이터를 달았어요. 나머지 게시물의 metadata 는 NULL 이에요. "메타데이터를 안 단 게시물" 이 자연스럽게 섞여 있는 거죠. JSON 도 결국 한 컬럼이라, 값이 없으면 다른 컬럼처럼 NULL 로 비어 있어요.
💡 한 줄 정리 — 게시물마다 구조가 다른 메타데이터는 고정 컬럼 여러 개 대신
JSON컬럼 한 칸에 통째로 담는다. 배열·중첩 객체까지 들어가고, 안 단 게시물은 그냥 NULL 이다.
🙋 학생 질문 — "튜터님, 그러면 모든 걸 다 JSON 한 칸에 넣으면 편하지 않나요?"
편해 보이지만 위험해요. JSON 은 "구조가 정해지지 않은, 게시물마다 다른" 데이터에만 어울려요. 회원 이름이나 게시물 작성자처럼 모든 행에 똑같이 있고, 조건으로 자주 찾고, 다른 표와 이어 붙이는(JOIN) 데이터를 JSON 에 넣으면, 정규화가 주던 무결성·속도·제약조건을 다 잃어요. 외래키도 못 걸고, 타입 검사도 약해지고요. 그래서 실무에선 "핵심 데이터는 정규화된 컬럼으로, 들쭉날쭉한 부가 정보만 JSON 으로" 라는 선을 지켜요. 전부 JSON 에 넣는 건 정규화를 안 배운 사람이 하는 실수예요.
Step 3: "JSON 꺼내 쓰기 — 점 표기법과 JSON_TABLE"
담았으면 꺼내 써야죠. JSON 한 칸 안에서 원하는 값만 뽑는 가장 간단한 방법은 점 표기법(dot notation) 이에요. C-2 에서 함수로 값을 가공하던 결을 떠올리면 돼요. 컬럼 이름 뒤에 .키.타입() 을 붙이면 그 값이 나와요.
-- sql/queries/G1_modern.sql
-- 닷 노테이션으로 location 만 꺼내기 (없는 게시물은 NULL)
SELECT p.post_id, p.metadata.location.string() AS location
FROM post p
WHERE p.metadata IS NOT NULL
ORDER BY p.post_id;
p.metadata.location.string() 은 "metadata 안의 location 값을 글자로 꺼내라" 는 뜻이에요. 위치를 적은 게시물은 제주·한강·헬스장 처럼 나오고, 위치가 없는 게시물(필름 사진 5번 등)은 NULL 이 나와요. JSON 안에 없는 키를 꺼내면 에러가 아니라 NULL 이 나와요. C-3 에서 본 "값이 없으면 NULL" 이 JSON 안에서도 똑같이 통해요.
조건으로도 쓸 수 있어요. JSON_VALUE 는 값을 꺼내는 함수, JSON_EXISTS 는 "그 키가 있는지" 를 묻는 함수예요. 필터를 적은 게시물만 골라 볼게요.
SELECT post_id, JSON_VALUE(metadata, '$.filter') AS filter
FROM post
WHERE JSON_EXISTS(metadata, '$.filter')
ORDER BY post_id;
$.filter 의 $ 는 "이 JSON 의 뿌리" 를 뜻하고, $.filter 는 "뿌리 밑의 filter" 예요. 필터를 적은 세 게시물만 나와요. Clarendon·Lark·없음 처럼요. 필터 키가 아예 없는 게시물은 JSON_EXISTS 에서 걸러져서 빠져요.
이제 한 걸음 더 나가요. 12번 게시물의 tags 는 ["여행","제주","바다"] 처럼 배열 이었죠. 이 배열을 한 줄에 묶인 채 두지 않고, 한 태그가 한 행이 되도록 펼치는 게 JSON_TABLE 이에요.
-- JSON 배열을 관계형 행으로 펼치기
SELECT jt.post_id, t.tag
FROM post jt,
JSON_TABLE(jt.metadata, '$.tags[*]' COLUMNS (tag PATH '$')) t
WHERE jt.post_id = 12;
$.tags[*] 는 "tags 배열의 모든 원소" 라는 뜻이에요. 결과로 여행·제주·바다 가 각각 한 행씩, 3행이 나와요. JSON 배열 한 칸이 다시 관계형 표가 된 거죠. 흥미롭지 않나요? ②에서 표를 JSON 으로 담았는데, JSON_TABLE 은 그 JSON 을 다시 표로 되돌려요. 일단 표로 돌아오면, 지금까지 배운 GROUP BY·COUNT·조인을 그대로 쓸 수 있어요. 메타데이터를 단 게시물들의 태그를 전부 펼치면 11개 태그 행이 나오는데, 여기에 D-4 의 순위 함수든 D-6 의 LISTAGG 든 마음대로 얹을 수 있어요.
💡 한 줄 정리 — JSON 값은 점 표기법(
col.키.string())이나JSON_VALUE/JSON_EXISTS로 꺼낸다. 배열은JSON_TABLE로 행으로 펼치면, 그 순간부터 평범한 표처럼 기존 SQL 을 다 쓸 수 있다.
🙋 학생 질문 — "튜터님, JSON 안의 값으로 정렬하거나 검색하면 느리지 않아요?"
기본적으로는 표의 일반 컬럼보다 느릴 수 있어요. JSON 은 한 칸 안에 통째로 들었으니, 그 안을 들여다보는 일이 컬럼을 바로 비교하는 것보다 손이 더 가거든요. 그래서 데이터베이스는 "JSON 안의 특정 키" 에 색인(인덱스)을 걸 수 있게 해 줘요. 책 뒤의 찾아보기처럼요. 자주 조건으로 쓰는 키(예: $.location)에 인덱스를 만들어 두면 빨라져요. 다만 이쯤 되면 "그 값을 그렇게 자주 찾을 거면 처음부터 정규 컬럼으로 두는 게 낫지 않나?" 를 다시 따져 보게 돼요. JSON 과 정규 컬럼 사이의 선택은 늘 이 저울질이에요.
Step 4: "표로 저장하고 문서로 읽기 — JSON Relational Duality"
여기서부터가 Oracle 23ai 가 자랑하는 기능이에요. ②에서 한 학생이 "전부 JSON 에 넣으면 편하지 않냐" 고 물었고, 저는 "정규화를 잃는다" 고 답했죠. 그럼 이런 욕심이 생겨요. "저장은 정규화된 표로 안전하게 하고, 읽을 때만 JSON 문서로 편하게 받을 순 없을까?"
바로 그걸 해 주는 게 JSON Relational Duality View 예요. 이름이 길지만 뜻은 간단해요. duality 는 "두 모습" 이라는 뜻이에요. 같은 데이터를 정규화된 표 와 JSON 문서 두 모습으로 동시에 다루게 해 줘요. 게시물(post)과 작성자(member)는 지금처럼 따로 정규화된 표에 두되, 읽을 땐 작성자 정보가 게시물 문서 안에 쏙 들어간 하나의 JSON 으로 받는 거죠.
-- sql/ddl/06_modern_json_vector.sql
-- 정규화된 두 표(post·member)를 하나의 문서로 보는 뷰
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW post_dv AS
post @insert @update @delete {
_id : post_id
caption : caption
category : category
author : member @noinsert @noupdate @nodelete {
memberId : member_id
username : username
nickname : nickname
}
};
생긴 건 낯설지만 읽어 보면 짐작이 가요. 게시물의 post_id·caption·category 를 문서의 필드로 두고, author 에는 member 표의 정보를 중첩해 넣어요. post.member_id 가 member 를 가리키는 외래키(C-5)라서, 데이터베이스가 알아서 둘을 이어 줘요. 이제 이 뷰에서 게시물 하나를 읽으면, 작성자가 문서 안에 함께 따라와요.
{
"_id" : 12,
"caption" : "제주도 여행 #여행 #제주 #바다",
"category" : "맛집",
"author" : {
"memberId" : 6,
"username" : "jisoo",
"nickname" : "강지수"
}
}
게시물과 작성자가 원래는 두 표에 따로 정규화돼 있는데, 읽을 땐 작성자(강지수)가 게시물 문서 안에 들어와 있죠. 조인을 직접 쓰지 않고도요. 그런데 실제 저장은 여전히 정규화된 post·member 표예요. 무결성·제약조건·다른 쿼리와의 조인을 그대로 누리면서, 읽기는 문서로 편하게 하는 거예요. B-4 에서 "정규화냐 문서냐" 를 고민했다면, 이건 그 둘을 화해시킨 셈이에요.
💡 한 줄 정리 — JSON Relational Duality View 는 정규화된 표를 그대로 두면서, 읽을 땐 연관 데이터가 중첩된 하나의 JSON 문서로 받게 해 준다. "표로 저장하고 문서로 읽는다" 가 한 줄 요약이다.
🙋 학생 질문 — "튜터님, 그럼 이 문서를 고치면 원래 표도 같이 바뀌나요?"
네, 그게 duality 의 진짜 힘이에요. 이 뷰는 단순히 보기만 하는 게 아니라, 문서를 고쳐서 다시 쓰면 밑에 있는 정규화된 표가 자동으로 갱신돼요. 위 정의에서 post 옆의 @insert @update @delete 가 "이 게시물 부분은 이 뷰로 추가·수정·삭제할 수 있다" 는 표시예요. 반대로 author 옆의 @noinsert @noupdate @nodelete 는 "작성자(회원) 정보는 이 뷰로는 못 바꾼다, 읽기만 한다" 는 뜻이고요. 한 문서를 다루는 것처럼 보여도 밑에선 여러 표의 정합성이 지켜지는 거죠. 다만 이건 동향 소개예요. 실무 적용은 더 깊은 학습이 필요하니, 오늘은 "이런 게 가능해졌다" 정도로 받아 두세요.
Step 5: "표준이 따라잡은 것들 — BOOLEAN 과 그래프 질의"
JSON 만 들어온 게 아니에요. SQL 표준과 Oracle 이 뒤늦게 따라잡은 소품 두 가지를 짧게 보고 가요.
첫째는 BOOLEAN 타입 이에요. 참/거짓을 담는 흔한 타입인데, 놀랍게도 Oracle 은 오랫동안 이걸 SQL 컬럼 타입으로 안 줬어요. 그래서 사람들이 NUMBER(1) 에 0/1 을 넣거나 CHAR(1) 에 'Y'/'N' 을 넣는 관행으로 버텼죠. SQL:2023 표준과 Oracle 23ai 부터 드디어 표준 BOOLEAN 이 들어왔어요. 인스타 회원에 "비공개 계정 여부" 를 붙여 볼게요.
-- sql/ddl/06_modern_json_vector.sql
ALTER TABLE member ADD (is_private BOOLEAN);
-- sql/seed/06_modern.sql
UPDATE member SET is_private = TRUE WHERE member_id IN (5, 8);
UPDATE member SET is_private = FALSE WHERE member_id IN (1, 2, 3, 4, 6, 9, 11, 12);
-- member 7, 10 은 NULL (비공개 설정을 아직 안 함 — "알 수 없음")
이제 WHERE is_private = TRUE 처럼 자연스럽게 쓸 수 있어요. 0 이냐 'Y' 냐 약속을 따로 외울 필요 없이요. 비공개 계정을 세면 2명(5번·8번)이 나와요. 그런데 여기 C-3 에서 배운 함정이 그대로 살아 있어요. 비공개 여부를 정한 회원은 TRUE 2명, FALSE 8명인데, 7번·10번은 설정을 안 해서 NULL 이에요. BOOLEAN 도 결국 참·거짓·NULL 세 가지 상태가 있어요.
SELECT
COUNT(CASE WHEN is_private = TRUE THEN 1 END) AS eq_true, -- 2
COUNT(CASE WHEN is_private != TRUE THEN 1 END) AS ne_true, -- 8
COUNT(*) AS total -- 12
FROM member;
= TRUE 가 2명, != TRUE 가 8명, 합이 10명이에요. 전체는 12명인데요. NULL 인 7번·10번은 = TRUE 에도 != TRUE 에도 안 잡혀서 양쪽 모두에서 빠져요. C-3 에서 "NULL 은 같다고도 다르다고도 할 수 없다" 던 3치 논리가 BOOLEAN 에서도 똑같이 반복돼요. 타입이 새로 생겼다고 NULL 의 성질이 바뀌는 건 아니에요.
둘째는 그래프 질의(SQL/PGQ) 예요. 이건 개념만 짚고 가요. C-5 에서 만든 팔로우(follow) 표를 떠올려 봐요. 회원이 회원을 가리키는 자기참조 관계였죠. 이런 "이어진 관계" 를 따라가는 질문 — "내가 팔로우한 사람이 팔로우한 사람", 즉 친구의 친구 — 을 우리는 D-3 의 계층형 질의나 조인으로 풀었어요. SQL:2023 의 SQL/PGQ 는 이런 데이터를 아예 그래프(점과 선) 로 보고 질의하는 표준 문법이에요. 회원을 점, 팔로우를 선으로 두고 "A 에서 B 로 2칸 안에 닿는 경로" 를 찾는 식이죠. 전문 그래프 데이터베이스가 하던 일을 관계형 SQL 표준이 끌어안은 거예요. 깊이는 시험 범위 밖이라 "이런 방향으로도 넓어졌다" 만 기억하면 돼요.
💡 한 줄 정리 — Oracle 23ai 부터 표준
BOOLEAN(참·거짓·NULL)이 들어와NUMBER(1)/CHAR(1)관행을 대체한다.SQL/PGQ는 팔로우 같은 관계를 그래프로 질의하는 SQL:2023 표준이다(개념만).
🙋 학생 질문 — "튜터님, 예전 프로젝트는 다 NUMBER(1) 로 짰을 텐데 그건 어떡해요?"
그대로 잘 돌아가요. 새 BOOLEAN 이 생겼다고 기존 NUMBER(1)·CHAR(1) 방식이 막히는 건 아니에요. 이미 그렇게 짜인 수많은 시스템은 계속 그 방식으로 동작하고, 새로 시작하는 데이터베이스에서 더 깔끔하게 BOOLEAN 을 고를 수 있는 선택지가 늘어난 거예요. 데이터베이스의 진화는 대개 이런 식이에요. 옛 방식을 끊어 내기보다, 새 선택지를 얹으면서 천천히 옮겨 가요. 그래서 현업에선 한동안 두 방식이 공존해요. 면접에서 "Oracle 의 BOOLEAN 은 비교적 최근에야 표준 타입으로 들어왔고, 그전엔 NUMBER(1) 관행을 썼다" 정도를 알면 충분히 깊은 답이에요.
Step 6: "비슷한 걸 찾는다는 것 — 임베딩과 벡터"
이제 오늘의 마지막 큰 주제, 벡터 검색이에요. 여긴 SQL 문법보다 기반 원리 를 먼저 잡고 가야 해요. 코드는 다음 Step 으로 미루고, 여기선 "비슷한 걸 찾는다는 게 데이터베이스에서 무슨 뜻인가" 만 또렷이 잡아요.
질문 하나로 시작해요. 컴퓨터가 "이 사진과 저 사진이 비슷한가?" 를 어떻게 알까요? 사람은 눈으로 보면 알지만, 데이터베이스는 글자와 숫자만 다뤄요. 그래서 먼저 사진이나 문장을 숫자 목록 으로 바꿔요. 이 숫자 목록이 벡터(vector) 이고, 바꾸는 일을 임베딩(embedding) 이라고 해요. 핵심 약속은 이거예요. 비슷한 내용은 비슷한 숫자 목록이 된다.
게시물 내용을 세 개의 숫자로 아주 단순하게 임베딩했다고 해 봐요. 축을 [여행, 음식, 일상] 의 정도라고 치면요.
게시물을 숫자 벡터로 바꾸면, 비슷한 내용은 가까이 모인다
(여행 ↑)
│ ● 제주 바다 [0.9, 0.1, 0.2]
│ ● 노을 [0.8, 0.1, 0.3] ← 둘 다 여행 → 가까이
│
│ ● 점심 [0.2, 0.9, 0.1]
│ ● 디저트 [0.1, 0.8, 0.3] ← 둘 다 음식 → 가까이
└──────────────────────────────→ (음식 →)
제주 바다 사진과 노을 사진은 둘 다 여행 쪽이라 숫자가 비슷하고, 그래서 이 공간에서 가까이 모여요. 점심과 디저트는 음식 쪽이라 또 자기들끼리 가까이 모이고요. "비슷하다" 가 "벡터 공간에서 가깝다" 로 바뀐 거예요. 그럼 비슷한 걸 찾는 일은 가까운 점을 찾는 일 이 돼요. 가까운 정도를 재는 자를 거리(distance) 라고 하고, 방향이 얼마나 닮았는지를 보는 코사인 거리, 직선으로 얼마나 떨어졌는지를 보는 유클리드 거리 등이 있어요.
여기서 "왜 이걸 데이터베이스가 하나" 가 중요해요. 예전엔 이런 벡터를 다루는 전용 데이터베이스(벡터 데이터베이스)가 따로 있었어요. 그런데 Oracle 23ai 는 VECTOR 라는 타입을 SQL 안에 들여서, 벡터를 그냥 게시물 표의 한 컬럼으로 저장하고 SQL 로 "비슷한 것" 을 찾게 했어요. 덕분에 벡터 검색 결과를 회원·카테고리 같은 정규화된 데이터와 한 쿼리에서 바로 이어 붙일 수 있죠. 다음 Step 에서 그걸 직접 해 봐요.
💡 한 줄 정리 — 임베딩은 이미지·문장을 숫자 목록(벡터)으로 바꾸는 일이고, 비슷한 내용은 벡터 공간에서 가까이 모인다. 그래서 "비슷한 것 찾기" 는 "가까운 벡터 찾기(거리 재기)" 가 된다.
🙋 학생 질문 — "튜터님, 그 숫자 목록은 누가 어떻게 만들어요?"
오늘은 제가 게시물마다 손으로 3개짜리 숫자를 정해서 넣었어요. 직관을 보려는 교육용이라서요. 하지만 실제로는 사람이 정하지 않아요. 이미지나 문장을 입력하면 그에 맞는 수백~수천 개짜리 벡터를 만들어 주는 임베딩 모델 이 따로 있어요. 그 모델을 불러서 임베딩을 만드는 일 자체는 데이터베이스의 몫이 아니라 애플리케이션과 AI 쪽 일이라, 이 백과사전에선 spring-ai 과목에서 본격적으로 다뤄요. 오늘 우리가 보는 건 그렇게 만들어진 벡터를 데이터베이스가 저장하고 비슷한 걸 찾아 주는 부분이에요. 역할이 나뉘어 있는 거죠.
Step 7: "벡터 검색 — '비슷한 게시물' 찾기"
원리를 잡았으니 직접 찾아봐요. 먼저 게시물에 벡터를 담을 컬럼을 붙여요. VECTOR(3, FLOAT32) 는 "실수 3개짜리 벡터" 라는 뜻이에요. 교육용이라 3차원이지만, 실제 임베딩은 수백~수천 차원이에요.
-- sql/ddl/06_modern_json_vector.sql
ALTER TABLE post ADD (embedding VECTOR(3, FLOAT32));
-- sql/seed/06_modern.sql (축 의미 = [여행, 음식, 일상])
UPDATE post SET embedding = TO_VECTOR('[0.9, 0.1, 0.2]') WHERE post_id = 12; -- 제주 바다
UPDATE post SET embedding = TO_VECTOR('[0.8, 0.1, 0.3]') WHERE post_id = 6; -- 노을
UPDATE post SET embedding = TO_VECTOR('[0.2, 0.9, 0.1]') WHERE post_id = 8; -- 점심
이제 핵심이에요. "제주 바다 게시물(12번)과 비슷한 게시물" 을 찾아요. VECTOR_DISTANCE 함수로 각 게시물의 벡터가 12번의 벡터에서 얼마나 떨어졌는지(코사인 거리)를 재서, 가까운 순서로 줄 세워요. 거리가 작을수록 비슷한 거예요.
-- sql/queries/G1_modern.sql
SELECT p.post_id,
SUBSTR(p.caption, 1, 18) AS caption,
ROUND(VECTOR_DISTANCE(p.embedding,
(SELECT embedding FROM post WHERE post_id = 12), COSINE), 4) AS cos_dist
FROM post p
WHERE p.embedding IS NOT NULL
ORDER BY cos_dist
FETCH FIRST 5 ROWS ONLY;
결과가 직관과 딱 맞아요.
| post_id | caption | cos_dist |
|---|---|---|
| 12 | 제주도 여행 #여행 #제주 #바다 | 0 |
| 6 | 노을 #하늘 | 0.0097 |
| 3 | 주말 산책 | 0.2529 |
| 1 | 첫 게시물입니다 #안녕 #일상 | 0.5657 |
| 8 | 오늘의 점심 | 0.6628 |
자기 자신(12번)은 거리가 0 이에요. 같은 벡터니까요. 그다음으로 가까운 게 노을(6번, 0.0097) 이에요. 둘 다 여행/풍경이라 벡터가 거의 붙어 있죠. 주말 산책이 그다음이고, 음식인 점심(8번)은 0.66 으로 멀리 떨어져요. 데이터베이스가 캡션의 글자를 비교한 게 아니라, 내용의 의미를 벡터 거리로 견줘서 비슷한 게시물을 찾아낸 거예요. #제주 같은 똑같은 글자가 없어도 "여행 느낌이 비슷한" 게시물을 골라요. 이게 키워드 검색과 다른 점이에요.
Oracle 은 자주 쓰는 거리에 단축 연산자도 줘요. 코사인 거리는 <=> 로 짧게 쓸 수 있어요. 같은 결과를, 자기 자신만 빼고 가까운 셋만 보면요.
SELECT p.post_id,
ROUND(p.embedding <=> (SELECT embedding FROM post WHERE post_id = 12), 4) AS cos_dist
FROM post p
WHERE p.embedding IS NOT NULL
AND p.post_id <> 12
ORDER BY cos_dist
FETCH FIRST 3 ROWS ONLY;
노을(0.0097)·주말 산책(0.2529)·첫 게시물(0.5657) 순서로 똑같이 나와요. 그리고 이 벡터 컬럼은 결국 post 표의 일부라, 검색 결과에 작성자나 카테고리를 조인해 붙이는 것도 지금까지 배운 SQL 그대로예요. "비슷한 것 찾기" 와 "관계형 데이터" 가 한 쿼리에서 만나는 거죠.
💡 한 줄 정리 —
VECTOR컬럼에 임베딩을 담고VECTOR_DISTANCE(..., COSINE)(또는<=>)로 거리를 재 가까운 순으로 줄 세우면, 글자가 아니라 의미가 비슷한 게시물을 찾는다. 그 결과를 관계형 데이터와 한 쿼리에서 조인할 수 있다.
🙋 학생 질문 — "튜터님, 이러면 그냥 LIKE 로 #여행 검색하는 거랑 뭐가 달라요?"
LIKE '%여행%' 은 글자가 똑같이 들어 있는 게시물만 찾아요. 캡션에 '여행' 이라는 글자가 없으면, 아무리 내용이 여행이어도 못 찾죠. 노을 사진 캡션이 노을 #하늘 이라 '여행' 글자가 없는데도 제주 바다와 가깝게 잡힌 게 그 차이예요. 벡터 검색은 글자가 아니라 의미의 가까움 으로 찾아요. 그래서 "비슷한 게시물 추천", "비슷한 이미지 찾기", 문장으로 묻고 관련 문서를 찾는 검색 같은 데 쓰여요. 물론 정확히 그 단어를 찾을 땐 여전히 LIKE 나 전문 검색이 맞고요. 둘은 경쟁이 아니라 쓰임이 달라요.
마무리
오늘은 우리가 배운 관계형 SQL 이 모던 세계에서 어떻게 넓어지는지를 둘러봤어요. 표에 안 맞는 데이터를 담는 JSON 컬럼, 표로 저장하고 문서로 읽는 Duality, 표준이 따라잡은 BOOLEAN, 그리고 의미가 비슷한 것을 찾는 벡터 검색까지요. 그러면서도 그 모든 게 정규화된 표 위에 얹힌다는 것, 전통 SQL 이 여전히 토대라는 걸 확인했어요.
오늘 배운 핵심 세 가지
- 💡 JSON 으로 반정형 품기 — 게시물마다 구조가 다른 메타데이터는
JSON컬럼 한 칸에 담고, 점 표기법·JSON_TABLE로 꺼낸다.JSON_TABLE로 펼치면 다시 평범한 표가 된다. - 💡 표와 문서의 화해 — JSON Relational Duality 는 정규화된 표를 그대로 두고 읽을 때만 문서로 받게 해 준다. 표준
BOOLEAN도 들어왔지만 NULL 의 3치 논리는 그대로다. - 💡 벡터로 비정형 품기 — 이미지·문장을 임베딩(벡터)으로 바꾸면 "비슷한 것 찾기" 가 "가까운 벡터 찾기" 가 된다.
VECTOR_DISTANCE로 의미가 비슷한 게시물을 찾고, 그 결과를 관계형 데이터와 한 쿼리에서 잇는다.
데이터베이스 과목을 마치며
긴 여정이었어요. 환경을 잡고(A), 인스타그램을 종이 위에 설계하고(B), 그 설계를 CREATE TABLE 로 구현해 조회·함수·집계·조인을 쌓았죠(C). 거기서 집합·서브쿼리·계층형·윈도우로 분석 쿼리까지 올라가고(D), 같은 쿼리를 Oracle·ANSI·MySQL 세 방언으로 오가고(E·F), 오늘 모던 동향(G)으로 마무리했어요. 그동안 한 번도 인스타그램이라는 한 프로젝트를 벗어나지 않았죠. 같은 회원 12명, 게시물 50개를 두고 SQL 의 거의 모든 결을 훑은 거예요.
기억할 한 가지는 이거예요. JSON 도 벡터도 결국 정규화된 표 위에 얹힌 선택지 라는 것. 화려한 신기능이 나와도, 데이터를 정확히 모델링하고 SELECT·조인·서브쿼리로 또렷하게 질의하는 힘이 모든 것의 바탕이에요. 그 힘을 우리는 이미 갖췄어요.
다음 과목 예고
이제 우리는 SQL 을 자유롭게 쓸 수 있어요. 그런데 지금까지는 sqlplus 에 SQL 을 직접 타이핑해서 결과를 봤죠. 다음 과목인 spring-boot 에선, 이 SQL 을 자바 애플리케이션이 대신 만들고 실행하는 세계로 넘어가요. 사용자가 인스타그램 앱에서 버튼을 누르면, 자바 프로그램이 그에 맞는 SQL 을 만들어 데이터베이스에 보내고, 받은 결과를 화면으로 돌려주는 흐름이에요. 우리가 오늘까지 다진 SQL 실력이 바로 그 세계의 단단한 바닥이 돼요. 같은 인스타그램으로, 이번엔 백엔드 애플리케이션을 지으러 가요. 그동안 정말 고생 많으셨어요.
과제
오늘 둘러본 모던 기능을 직접 손으로 다뤄 봐요. 시험 범위는 아니지만, 쿼리를 실행하기 전에 결과를 먼저 예측한 뒤 맞혀 보면 원리가 또렷해져요. (Oracle 23ai/26ai 에서 실행해요.)
[기초] JSON 메타데이터 담고 꺼내기
(가) post 에 붙인 metadata JSON 컬럼에서, 위치(location)를 적은 게시물만 골라 게시물 번호와 위치를 출력하세요. 점 표기법과 JSON_EXISTS 두 가지 방법으로 각각 적어 보세요. (나) 12번 게시물의 tags 배열을 JSON_TABLE 로 펼쳐 한 태그가 한 행이 되게 출력하세요. (다) 메타데이터를 단 게시물은 50개 중 몇 개인지 세고, 안 단 게시물의 metadata 가 왜 NULL 인지 한 줄로 설명하세요.
[응용] BOOLEAN 과 3치 논리
(가) member.is_private 에서 비공개(TRUE)인 회원 수와 공개(FALSE)인 회원 수를 각각 세세요. (나) is_private = TRUE 인 회원과 is_private != TRUE 인 회원을 합치면 전체 12명이 되는지 확인하고, 안 된다면 몇 명이 어디로 빠졌는지 설명하세요. (다) 이 빠짐이 C-3 에서 배운 NULL 의 어떤 성질 때문인지, 빠진 회원을 함께 세려면 조건을 어떻게 고쳐야 하는지 적으세요.
[심화] 비슷한 게시물 찾기
(가) embedding 벡터가 있는 게시물 중, 음식 게시물인 8번(오늘의 점심)과 비슷한 순서로 게시물을 줄 세우세요. VECTOR_DISTANCE(..., COSINE) 를 쓰고, 가까운 5개만 뽑으세요. (나) 결과 맨 위(자기 자신 다음)에 어떤 게시물이 오는지 예측하고, 왜 그 게시물이 8번과 가까운지 벡터 값으로 설명하세요. (다) 같은 질의를 단축 연산자 <=> 로 바꿔 적고, 결과가 같은지 확인하세요.
생각해볼 주제
1. 핵심 데이터를 JSON 한 칸에 몰아넣어도 될까
게시물의 들쭉날쭉한 메타데이터를 JSON 으로 담는 건 자연스러웠어요. 그런데 회원 이름이나 게시물 작성자처럼 모든 행에 똑같이 있고 자주 조건으로 찾는 데이터까지 JSON 한 칸에 몰아넣으면 어떻게 될까요? 정규화된 컬럼이 주던 무결성·속도·조인·제약조건의 관점에서, "무엇을 정규 컬럼으로 두고 무엇을 JSON 으로 둘지" 의 경계를 어디에 그어야 할지 생각해 보세요.
2. 벡터 검색은 키워드 검색을 대체할까
벡터 검색은 글자가 아니라 의미의 비슷함으로 찾고, LIKE 나 전문 검색은 글자가 똑같이 들어 있는 걸 찾아요. "비슷한 게시물 추천" 엔 벡터가 빛나지만, "정확히 이 해시태그가 달린 글" 을 찾을 땐 어느 쪽이 맞을까요? 두 방식이 경쟁 관계인지 보완 관계인지, 인스타그램의 어떤 화면에서 각각이 더 어울릴지 견줘 보세요.
3. 신기능이 쏟아져도 전통 SQL 이 토대인 이유
JSON·Duality·벡터처럼 화려한 기능이 계속 들어와도, 그 모든 게 정규화된 표 위에 얹혀요. 만약 이런 신기능만 잔뜩 쓰고 정규화·조인·서브쿼리 같은 기본기를 소홀히 한 사람이 있다면, 어떤 문제에 부딪힐까요? "도구가 늘어날수록 기본기가 더 중요해진다" 는 말이 데이터베이스에서 구체적으로 무엇을 뜻하는지 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 이 카테고리는 SQLD 시험 범위가 아니라 동향/교양이라, 정답을 외우기보다 "관계형 데이터베이스가 JSON·벡터를 어떻게 품는가", "그 위에서 전통 SQL 이 어떻게 그대로 쓰이는가" 의 감각을 잡는 게 목표예요. (Oracle 23ai/26ai 에서 실행해요.)
🎯 [과제 1 예시답안] JSON 메타데이터 담고 꺼내기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) 위치 꺼내기 — 점 표기법 + JSON_EXISTS |
40% | 5건(4·6·11·12·13) |
(나) JSON_TABLE 로 tags 펼치기 |
35% | 3행 여행/제주/바다 |
| (다) 메타데이터 단 게시물 수 + NULL 이유 | 25% | 7건 · JSON 도 한 컬럼 |
풀이 예시
(가) 위치를 적은 게시물 — 두 가지 방법
-- 방법 1: 점 표기법
SELECT p.post_id, p.metadata.location.string() AS loc
FROM post p
WHERE p.metadata.location.string() IS NOT NULL
ORDER BY p.post_id;
-- 방법 2: JSON_EXISTS + JSON_VALUE
SELECT post_id, JSON_VALUE(metadata, '$.location') AS loc
FROM post
WHERE JSON_EXISTS(metadata, '$.location')
ORDER BY post_id;
둘 다 위치를 적은 5개 게시물이 나와요. 4번 성수동 카페, 6번 한강, 11번 헬스장, 12번 제주, 13번 이태원 맛집이에요. 메타데이터를 단 게시물은 7개인데 그중 위치를 적은 건 5개예요. 1번(태그만 있음)과 5번(카메라 정보만 있음)은 location 키가 아예 없어서 빠져요. 없는 키를 꺼내면 NULL 이라 첫 번째 방법의 IS NOT NULL 에서, JSON_EXISTS 에서 각각 걸러지는 거죠.
(나) 12번 게시물의 tags 배열 펼치기
SELECT t.tag
FROM post jt,
JSON_TABLE(jt.metadata, '$.tags[*]' COLUMNS (tag PATH '$')) t
WHERE jt.post_id = 12;
여행·제주·바다 가 각각 한 행씩, 3행으로 나와요. 한 칸에 배열로 들었던 태그가 다시 평범한 표의 행이 된 거예요.
(다) 메타데이터를 단 게시물 수
SELECT COUNT(*) AS meta_cnt FROM post WHERE metadata IS NOT NULL; -- 7
50개 중 7개에만 메타데이터를 달았어요. 나머지 43개의 metadata 는 NULL 이에요. JSON 도 결국 post 표의 한 컬럼이라, 값을 안 넣으면 다른 컬럼처럼 NULL 로 비어 있는 거예요. "JSON 이라 특별히 빈 객체 {} 가 들어간다" 같은 게 아니라, 그냥 컬럼이 비어 NULL 이에요.
💡 튜터의 한마디 — JSON 한 칸 안의 키는 "있을 수도 없을 수도" 있어요. 그래서 위치를 꺼낼 때 JSON_EXISTS 로 "그 키가 있는 게시물" 만 고르는 습관이 중요해요. 키가 없으면 에러가 아니라 NULL 이 나오니, NULL 처리(C-3)가 JSON 안에서도 그대로 따라온다는 걸 기억하세요.
🎯 [과제 2 예시답안] BOOLEAN 과 3치 논리
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
| (가) 비공개·공개 회원 수 | 30% | TRUE 2 · FALSE 8 |
(나) = TRUE + != TRUE = 10 ≠ 12 |
40% | NULL 2명(7·10) 빠짐 |
| (다) C-3 3치 논리 + 보정 | 30% | IS NULL 을 더해야 함 |
풀이 예시
(가) 비공개·공개 회원 수
SELECT COUNT(*) AS private_cnt FROM member WHERE is_private = TRUE; -- 2
SELECT COUNT(*) AS public_cnt FROM member WHERE is_private = FALSE; -- 8
비공개(TRUE)가 2명(5번·8번), 공개(FALSE)가 8명이에요.
(나) 둘을 합치면 12명이 될까
SELECT
COUNT(CASE WHEN is_private = TRUE THEN 1 END) AS eq_true, -- 2
COUNT(CASE WHEN is_private != TRUE THEN 1 END) AS ne_true, -- 8
COUNT(*) AS total -- 12
FROM member;
= TRUE 가 2명, != TRUE 가 8명, 합이 10명이에요. 그런데 전체는 12명이죠. 2명이 양쪽 모두에서 빠졌어요. 바로 is_private 가 NULL 인 7번·10번이에요. 비공개 설정을 아직 안 한 회원이라 값이 NULL 이고, 그래서 "비공개다(= TRUE)" 에도 "비공개가 아니다(!= TRUE)" 에도 안 잡혀요.
(다) 왜 빠지고, 어떻게 고치나
C-3 에서 배운 NULL 의 3치 논리 때문이에요. NULL 은 "알 수 없음" 이라, NULL = TRUE 도 NULL != TRUE 도 참이 아니라 UNKNOWN 이에요. 조건이 참일 때만 세어지니 양쪽에서 빠지는 거죠. NULL 회원까지 "공개거나 미설정" 으로 함께 세려면 IS NULL 을 더해야 해요.
SELECT COUNT(*) AS public_or_unset
FROM member
WHERE is_private = FALSE OR is_private IS NULL; -- 10
이러면 공개 8명 + 미설정 2명 = 10명이 나와요.
💡 튜터의 한마디 — 타입이 NUMBER 에서 BOOLEAN 으로 바뀌어도 NULL 의 성질은 그대로예요. "참도 거짓도 아닌 제3의 상태(NULL)" 가 항상 따라붙죠. C-3 의 3치 논리는 새 타입이 들어와도 변하지 않는 SQL 의 토대라는 걸 보여주는 예예요.
🎯 [과제 3 예시답안] 비슷한 게시물 찾기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) VECTOR_DISTANCE 로 8번 기준 정렬 |
40% | 8(0)·7·14 순 |
| (나) 최근접 예측 + 벡터로 설명 | 35% | 맛집(7) — 음식축이 닮음 |
(다) 단축 연산자 <=> 동일 |
25% | 같은 결과 |
풀이 예시
(가) 점심(8번)과 비슷한 게시물 줄 세우기
SELECT p.post_id, SUBSTR(p.caption, 1, 16) AS caption,
ROUND(VECTOR_DISTANCE(p.embedding,
(SELECT embedding FROM post WHERE post_id = 8), COSINE), 4) AS cos_dist
FROM post p
WHERE p.embedding IS NOT NULL
ORDER BY cos_dist
FETCH FIRST 5 ROWS ONLY;
| post_id | caption | cos_dist |
|---|---|---|
| 8 | 오늘의 점심 | 0 |
| 7 | 맛집 탐방 | 0.0116 |
| 14 | 오늘의 디저트 | 0.0348 |
| 1 | 첫 게시물입니다 #안녕 #일상 | 0.6457 |
| 6 | 노을 #하늘 | 0.649 |
(나) 자기 다음으로 가까운 게시물과 이유
자기 자신(8번, 거리 0) 다음으로 가까운 건 맛집 탐방(7번, 0.0116) 이에요. 점심 8번의 벡터는 [0.2, 0.9, 0.1], 맛집 7번은 [0.1, 0.9, 0.2] 예요. 둘 다 가운데 음식축이 0.9 로 크고 방향이 거의 같아서, 코사인 거리가 0 에 가깝게 작아요. 그다음 디저트(14번)도 음식이라 가깝고요. 반대로 노을(6번, [0.8, 0.1, 0.3])은 여행축이 큰 게시물이라 점심과 방향이 달라 0.649 로 멀어요. 글자가 아니라 의미(음식이냐 여행이냐)의 방향이 거리를 가른 거예요.
(다) 단축 연산자로 바꾸기
SELECT p.post_id,
ROUND(p.embedding <=> (SELECT embedding FROM post WHERE post_id = 8), 4) AS cos_dist
FROM post p
WHERE p.embedding IS NOT NULL
ORDER BY cos_dist
FETCH FIRST 5 ROWS ONLY;
<=> 는 코사인 거리의 단축 연산자라, VECTOR_DISTANCE(..., COSINE) 와 결과가 똑같아요. 8(0)·7(0.0116)·14(0.0348) 순서 그대로 나와요.
💡 튜터의 한마디 — 벡터 검색의 핵심은 "비슷함 = 가까운 거리" 예요. 점심과 맛집은 캡션에 겹치는 글자가 없는데도 가깝게 잡혀요. 의미가 비슷하면 벡터가 가까워지니까요. 이게 LIKE 같은 글자 검색과 결정적으로 다른 점이에요.
생각해볼 주제
1. 핵심 데이터를 JSON 한 칸에 몰아넣어도 될까
문제 상황 요약
게시물의 들쭉날쭉한 메타데이터를 JSON 으로 담는 건 자연스러웠어요. 그런데 회원 이름이나 게시물 작성자처럼 모든 행에 똑같이 있고 자주 조건으로 찾는 데이터까지 JSON 한 칸에 몰아넣으면 어떻게 될까요? 무엇을 정규 컬럼으로, 무엇을 JSON 으로 둘지 경계를 어디에 그어야 할까요?
튜터의 가이드 및 해설
판단의 기준은 "그 데이터를 어떻게 쓰느냐" 예요. 모든 행에 똑같이 있고, 조건(WHERE)으로 자주 찾고, 다른 표와 이어 붙이고(JOIN), 무결성을 지켜야 하는 데이터라면 정규 컬럼이 맞아요. 정규화가 주는 외래키·제약조건·인덱스·빠른 비교를 다 누릴 수 있으니까요. 회원 이름을 JSON 에 넣으면 외래키도 못 걸고, 이름으로 찾을 때마다 JSON 안을 들여다봐야 하고, 타입 검사도 약해져요.
반대로 게시물마다 구조가 제각각이고, 있을 수도 없을 수도 있고, 조건으로 자주 찾지 않는 부가 정보(촬영 기기·필터·좌표 등)는 JSON 이 어울려요. 이런 걸 고정 컬럼으로 만들면 대부분 NULL 인 컬럼이 끝없이 늘어나거든요. 경계는 결국 "구조가 고정이고 자주 질의하면 정규 컬럼, 구조가 가변이고 부가적이면 JSON" 이에요. JSON 은 정규화를 대체하는 게 아니라 보완해요.
🎯 면접에선 이렇게 나온다
"JSON 컬럼은 '구조가 가변적이고, 조건으로 자주 검색하지 않으며, 다른 테이블과 조인할 필요가 없는 부가 데이터'에 씁니다. 반대로 모든 행에 공통이고 자주 필터링하거나 조인하는 핵심 데이터는 정규화된 컬럼으로 두어 무결성·인덱스·제약조건을 살립니다. JSON 의 특정 키에 인덱스를 걸 수도 있지만, 그렇게 자주 검색할 데이터라면 처음부터 정규 컬럼이 낫다는 트레이드오프를 함께 짚으면 깊은 답이 됩니다. JSON 은 정규화의 대체가 아니라 보완입니다."
💡 실무에선
대부분의 팀은 "핵심 데이터는 정규 컬럼, 들쭉날쭉한 부가 정보만 JSON" 이라는 선을 지키고, JSON 비중이 너무 커지면 설계를 다시 의심해요.
2. 벡터 검색은 키워드 검색을 대체할까
문제 상황 요약
벡터 검색은 글자가 아니라 의미의 비슷함으로 찾고, LIKE 나 전문 검색은 글자가 똑같이 들어 있는 걸 찾아요. "비슷한 게시물 추천" 엔 벡터가 빛나지만, "정확히 이 해시태그가 달린 글" 을 찾을 땐 어느 쪽이 맞을까요? 둘은 경쟁 관계일까요, 보완 관계일까요?
튜터의 가이드 및 해설
두 검색은 묻는 질문 자체가 달라요. 키워드 검색은 "이 글자가 들어 있는가" 를 정확히 따져요. #제주 해시태그가 달린 게시물, 특정 단어가 포함된 글을 빠르고 정확하게 찾죠. 반면 벡터 검색은 "내용이 비슷한가" 를 따져요. 글자가 한 자도 안 겹쳐도 분위기가 닮은 걸 찾아내요. 노을 사진이 '여행' 글자 없이도 제주 바다와 가깝게 잡힌 게 그 예예요.
그래서 둘은 경쟁이 아니라 보완이에요. "정확히 이 태그" 를 찾을 땐 키워드가, "비슷한 느낌" 을 찾을 땐 벡터가 맞아요. 실제 서비스는 둘을 섞어 써요. 사용자가 입력한 키워드로 후보를 빠르게 좁힌 뒤, 그 안에서 벡터로 의미가 비슷한 순서로 다시 줄 세우는 식으로요. 인스타그램으로 치면, 검색창의 정확한 해시태그 검색은 키워드, "이 게시물과 비슷한 추천" 은 벡터가 어울려요.
🎯 면접에선 이렇게 나온다
"키워드 검색은 정확한 일치(exact match)에, 벡터 검색은 의미적 유사도(semantic similarity)에 강합니다. 둘은 대체 관계가 아니라 보완 관계라, 실무에선 키워드로 후보를 좁히고 벡터로 의미 순위를 매기는 하이브리드 검색을 자주 씁니다. 정확한 식별자나 태그 검색에 벡터를 쓰면 오히려 부정확하고, 추천이나 유사 문서 검색에 키워드만 쓰면 표현이 다른 비슷한 내용을 놓친다는 점을 함께 말하면 좋습니다."
💡 실무에선
검색·추천 시스템은 키워드(정확)와 벡터(유사)를 함께 얹는 하이브리드로 가는 추세예요.
3. 신기능이 쏟아져도 전통 SQL 이 토대인 이유
문제 상황 요약
JSON·Duality·벡터처럼 화려한 기능이 계속 들어와도, 그 모든 게 정규화된 표 위에 얹혀요. 만약 이런 신기능만 잔뜩 쓰고 정규화·조인·서브쿼리 같은 기본기를 소홀히 한 사람이 있다면, 어떤 문제에 부딪힐까요?
튜터의 가이드 및 해설
오늘 본 모던 기능들을 다시 떠올려 봐요. JSON 을 JSON_TABLE 로 펼치면 그 순간부터 GROUP BY·조인이 필요했어요. Duality 뷰는 정규화된 표와 외래키 위에 서 있었고요. 벡터 검색 결과도 회원·카테고리와 조인해야 쓸모가 생겼어요. 신기능은 전부 전통 SQL 을 토대로 쓰여요. 토대가 약하면 신기능도 제대로 못 써요.
기본기를 소홀히 한 사람은 이런 데서 막혀요. JSON 을 꺼내긴 했는데 그걸 다른 데이터와 어떻게 이어 붙일지 모르고, 벡터로 비슷한 걸 찾긴 했는데 그 결과에 작성자 정보를 붙이는 조인을 못 짜요. 정규화를 모르면 무엇을 JSON 에 넣고 무엇을 컬럼으로 둘지조차 판단하지 못하고요. 도구가 늘어날수록, 그 도구들을 엮는 기본기(모델링·조인·서브쿼리·NULL 처리)의 중요성이 오히려 커져요. 화려한 기능은 기본기 위에서만 빛나요.
🎯 면접에선 이렇게 나온다
"JSON·벡터·Duality 같은 모던 기능은 모두 관계형 모델 위에 얹힌 확장입니다.
JSON_TABLE의 결과를 집계하려면GROUP BY가, 벡터 검색 결과를 활용하려면 조인이 필요하죠. 그래서 신기능을 잘 쓰는 사람일수록 정규화·조인·서브쿼리·NULL 처리 같은 기본기가 탄탄합니다. 저는 새 기능을 쫓기 전에 데이터 모델링과 표준 SQL 을 단단히 다지는 것을 우선합니다. 기본기가 없으면 신기능도 겉핥기로 끝납니다."
💡 실무에선
새 기능 도입을 검토할 때도 "이게 정규화된 표 설계를 어떻게 보완하나" 를 먼저 따지지, 기본기를 건너뛰고 신기능부터 얹지 않아요.