3-8 연습 문제 — 처음 보는 테이블도
다섯 단계로 읽어냅니다
섹션 3의 마지막 연습입니다. 오늘은 새로 배운 게 없으니 지금까지 배운 전부를 씁니다 — 한 행의 주인공, 컬럼의 약속, 데이터 타입, 기본키, 외래키, 그리고 구조를 바꾸고 지우는 명령까지. 종합 진단이라고 생각하고 풀어보세요.
섹션 3 에서의 위치
완료 ✓
강의 완료 ✓
지금 여기
다음 여정
2-4에서 만든 AI 과외 선생님 세션을 그대로 쓰세요. 오늘은 특히 이렇게 물어보면 좋습니다 — “내가 이 테이블을 다섯 단계로 이렇게 읽었는데, 빠뜨린 단계나 잘못 읽은 곳이 있어?” 정답을 받아 적는 것보다 본인이 먼저 다섯 칸을 채우고 검증받는 것이 훨씬 남습니다.
문제 1. 처음 보는 테이블을 다섯 단계로 읽으세요
배송을 관리하는 테이블 하나를 새로 받았다고 해봅시다. 이 테이블은 지금까지 한 번도 본 적이 없습니다.
| 컬럼 | 타입 | 규칙 |
|---|---|---|
| delivery_id | INT | PRIMARY KEY · AUTO_INCREMENT |
| order_id | INT | NOT NULL · FOREIGN KEY → orders(order_id) |
| address | VARCHAR(200) | NOT NULL |
| status | VARCHAR(20) | NOT NULL · DEFAULT '준비중' |
| delivered_at | DATE | NULL 허용 |
deliveries 는 실제 shop 스키마에 없습니다. 처음 보는 테이블을 읽는
훈련을 위해 만든 가상의 테이블이에요.
다섯 칸을 모두 채운 뒤, 아래 두 가지도 함께 답해보세요.
- 새 배송 행을 저장하면서
status를 생략하면 무엇이 저장될까요? delivered_at이NULL인 행은 무슨 뜻일까요?
정답 보기
| 단계 | 답 |
|---|---|
| ① 테이블 이름 | deliveries — 배송을 저장하는 테이블 |
| ② 한 행의 의미 | 배송 한 건 (주문 한 건에 대한 배송) |
| ③ 컬럼과 규칙 | 주소와 상태는 필수 · 상태의 기본값은 준비중 · 배송완료일은 비워둘 수 있음 |
| ④ 기본키 | delivery_id — 컬럼 하나로 구성 |
| ⑤ 외래키 | order_id → orders.order_id (자식: deliveries, 부모: orders) |
② 가 가장 중요합니다. 한 행의 주인공은 배송 한 건이에요. 주문 번호가 들어 있다고 해서 주인공이 주문이 되는 건 아닙니다 — orders의 한 행 안에 회원 번호가 있어도 주인공이 주문이었던 것과 같습니다.
status를 생략하면 → 준비중이 저장됩니다. DEFAULT가 설정된 컬럼은 값을 생략했을 때 미리 정해둔 값이 들어갑니다.
delivered_at이 NULL이면 → 아직 배송이 완료되지 않았다는 뜻입니다. 배송일이 기록되지 않은 상태 그 자체예요. 이 자리에 0이나 '없음'을 넣으면 기록된 값이 있다는 뜻이 되어버립니다.
⑤ 를 읽었다면 이것도 보입니다. order_id에 orders에 없는 주문 번호를 넣으면 저장이 거부됩니다. 참조 무결성 — 주소를 적었다면 실제로 존재하는 곳이어야 한다는 약속이죠.
문제 2. 실제 order_items 를 다섯 단계로 읽으세요
이번엔 진짜 우리 shop 테이블입니다. 아래는 order_items의 데이터 일부예요.
| order_id | product_id | quantity |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 16 | 2 |
| 2 | 22 | 1 |
| 3 | 9 | 1 |
| 3 | 11 | 1 |
| ⋮ | ||
다섯 단계로 읽은 뒤, 아래 세 질문에 답하세요.
| # | 질문 |
|---|---|
| ① | 기본키는 무엇인가요? order_id 하나만으로는 왜 안 될까요? |
| ② | 이 테이블의 외래키는 몇 개이고 각각 어디를 가리키나요? |
| ③ | 주문 1번에 상품 1번을 두 줄로 넣을 수 있을까요? |
데이터를 세로로 훑어보세요. order_id 열에서 1이 두 번, 3이 두 번
나옵니다. 그렇다면 한 컬럼만으로 각 행을 구별할 수 있을까요?
정답 보기
| 단계 | 답 |
|---|---|
| ① 테이블 이름 | order_items — 주문에 담긴 상품을 저장하는 테이블 |
| ② 한 행의 의미 | 주문에 포함된 상품 한 종류 |
| ③ 컬럼과 규칙 | quantity INT NOT NULL — 그 상품을 몇 개 담았는가 |
| ④ 기본키 | (order_id, product_id) — 두 컬럼이 함께 구성한 하나의 기본키 |
| ⑤ 외래키 | order_id → orders.order_id · product_id → products.product_id |
① order_id만으로는 안 됩니다. 주문 1번에 상품 1번과 16번이 들어 있어 order_id 값이 중복돼요. product_id만 봐도 같은 상품이 여러 주문에 들어가니 중복입니다. 하지만 두 컬럼을 한 쌍으로 묶으면 (1,1), (1,16), (2,22), (3,9), (3,11) — 겹치는 쌍이 없어 각 행이 구별됩니다.
여기서 자주 나오는 오해 하나 — 기본키가 두 개인 게 아닙니다. 기본키는 하나이고, 그 하나를 두 컬럼이 함께 구성하는 것뿐이에요. 이걸 복합키라고 부릅니다.
② 외래키는 두 개입니다. order_id는 orders를, product_id는 products를 가리킵니다. 이 테이블은 두 부모를 동시에 가리키는 자식인 셈이에요. 그래서 존재하지 않는 주문 번호나 상품 번호는 저장할 수 없습니다.
③ 넣을 수 없습니다. (1, 1) 쌍이 이미 기본키로 저장돼 있으니 같은 쌍을 한 번 더 저장하면 중복이라 거부됩니다. 같은 상품을 세 개 샀다면 행을 세 번 만드는 대신 quantity에 3을 저장하는 거예요 — 실제로 (1, 16, 2) 행이 그 예입니다. 상품 16번을 2개 담았다는 뜻이죠.
문제 3. AI 가 만든 구조를 검토하세요
AI에게 “쇼핑몰 쿠폰 테이블을 만들어줘” 라고 요청했더니 아래 구조가 나왔습니다. 실행하기 전에 다섯 항목으로 검토하세요.
| 컬럼 | 타입 | 규칙 · 값 예시 |
|---|---|---|
| coupon_name | VARCHAR(50) | NOT NULL — 신규가입 3천원 할인 |
| discount | VARCHAR(10) | NOT NULL — '3000' |
| member_name | VARCHAR(50) | NOT NULL — 김철수 |
| issued_expires | VARCHAR(30) | NOT NULL — 2026-05-01~2026-06-01 |
이 쿠폰 테이블은 실제 shop 스키마에 없습니다. 검토 훈련을 위해 일부러
잘못 만든 가상의 구조예요. 이대로 만들지 마세요.
다섯 항목(이름 · 한 행의 의미 · 컬럼과 타입 · 필수값과 기본값 · 기본키와 외래키) 중 무엇이 잘못됐는지 찾고, 각각 어떻게 고칠지 적어보세요.
정답 보기
| 항목 | 문제 | 이렇게 고칩니다 |
|---|---|---|
| ① 테이블 이름 | tbl1 — 무엇을 저장하는지 알 수 없다 | coupons |
| ② 한 행의 의미 | 쿠폰 종류 하나인지, 발급된 쿠폰 한 장인지 불분명 | 먼저 정한다 — 여기서는 발급된 쿠폰 한 장 |
| ③ 컬럼과 타입 | discount 가 문자열 · 두 날짜가 한 칸에 | discount INT · issued_at DATE + expires_at DATE |
| ⑤ 기본키와 외래키 | 기본키 없음 · 회원을 이름으로 기록 | coupon_id PK · member_id FK → members(member_id) |
① tbl1은 간판이 없는 건물입니다. 다섯 단계의 첫 칸부터 채울 수 없어요. 컬럼 이름이 표지판이라면 테이블 이름은 간판입니다.
② 는 사람이 결정해야 하는 부분입니다. 쿠폰 종류를 담는 테이블인지, 회원에게 발급된 쿠폰 한 장을 담는 테이블인지에 따라 필요한 컬럼이 완전히 달라집니다. AI는 이걸 대신 정해주지 않아요. member_name이 들어 있는 걸 보면 발급된 쿠폰 한 장을 의도한 것 같지만 — 추측이 아니라 결정이 필요합니다.
③ 은 3-2와 3-3 위반이 하나씩 있습니다.
discount VARCHAR→ 할인 금액은 더하고 비교할 값이라INT가 맞습니다. 문자열로 두면 정렬할 때'3000'이'500'보다 앞에 오는 일이 벌어져요.issued_expires→ 발급일과 만료일이 한 칸에 들어 있습니다. 따로 찾고 비교하고 바꿔야 하는 값이라면 각각의 컬럼으로.issued_at DATE와expires_at DATE두 개로 나눠야 “오늘 기준 만료된 쿠폰” 같은 조건도 쓸 수 있습니다.
⑤ 가 가장 큽니다. 기본키가 없으면 똑같은 행이 두 번 저장돼도 막을 수 없고, 쿠폰 한 장을 정확히 가리킬 수도 없습니다. 그리고 회원을 이름으로 기록하면 — 동명이인을 구별할 수 없고, 이름이 바뀌면 연결이 끊깁니다. member_id로 members를 참조해야 존재하지 않는 회원의 쿠폰이 저장되는 것도 막을 수 있어요.
④ 필수값과 기본값은 그럭저럭입니다. 다만 사용 여부를 담는 컬럼을 둔다면 DEFAULT를 함께 정하는 게 좋겠죠 — 생략했을 때 무엇이 들어갈지 미리 정해두는 겁니다.
AI는 그럴듯한 구조를 빠르게 만들어줍니다. 하지만 어떤 데이터를 어떤 규칙으로 저장할지는 결국 사람이 결정합니다. 생성은 도구가 — 검토는 사람이.
문제 4 (도전). 섹션 3 종합 — 참일까요, 거짓일까요
섹션 3 전체를 아우르는 진술 여덟 개입니다. 각각 참·거짓을 판정하고 근거 한 줄을 붙이세요. 근거를 쓰지 못하면 아직 그 강의로 돌아갈 때입니다.
| # | 진술 |
|---|---|
| ① | 테이블의 한 행은 주인공 하나에 대한 기록이고, 컬럼은 모든 행에 던지는 같은 질문이다 |
| ② | 나이를 아직 모르는 회원의 age 에는 0 을 넣어두면 된다 |
| ③ | 우편번호처럼 숫자로만 이루어진 값은 항상 INT 로 저장하는 게 맞다 |
| ④ | order_items 는 기본키가 두 개다 |
| ⑤ | orders.member_id 에 members 에 없는 999 를 넣으면 저장은 되고, 나중에 조회할 때 오류가 난다 |
| ⑥ | 우리 shop DB 에서 주문이 있는 김철수(1번) 회원 행을 지우려 하면 MySQL 이 막는다 |
| ⑦ | DELETE 로 order_items 의 모든 행을 지워도 order_items 의 컬럼과 규칙은 그대로 남는다 |
| ⑧ | NULL 을 허용하고 DEFAULT 도 있는 컬럼에 NULL 을 명시해 넣으면 NULL 이 저장된다 |
정답 보기
| # | 판정 | 근거 | 강의 |
|---|---|---|---|
| ① | 참 | 행은 주인공 하나에 대한 기록 한 세트, 열은 모든 주인공에게 던지는 같은 질문 | 3-1 |
| ② | 거짓 | 모르는 것과 0은 다릅니다 — 기록되지 않은 상태는 NULL | 3-2 |
| ③ | 거짓 | 06234를 INT에 넣으면 앞의 0이 사라집니다 — 계산하지 않는 번호는 문자열 | 3-3 |
| ④ | 거짓 | 기본키는 하나입니다 — 그 하나를 order_id와 product_id가 함께 구성 | 3-4 |
| ⑤ | 거짓 | 외래키가 설정돼 있으면 저장 시점에 막힙니다 — 참조 무결성 | 3-5 |
| ⑥ | 참 | 삭제 옵션을 따로 설정하지 않아 자식이 있는 부모는 삭제가 차단됩니다 | 3-5 |
| ⑦ | 참 | DELETE는 행만 지웁니다 — 테이블 자체를 없애는 건 DROP | 3-7 |
| ⑧ | 참 | DEFAULT는 값을 생략했을 때만 동작합니다 | 3-2 |
②와 ③은 같은 함정입니다. 겉모양에 속아 그릇을 잘못 고르는 것 — 값이 없는 상태를 0으로 대신하거나, 숫자처럼 보인다고 숫자 타입에 넣는 거죠. 둘 다 오류 없이 저장되고, 나중에 그럴듯하게 틀린 결과로 돌아옵니다.
⑤가 오늘의 핵심 오해입니다. 외래키는 잘못된 값이 들어간 뒤에 알려주는 경고가 아니라 — 저장 자체를 거부하는 규칙입니다. 그래서 *“누구의 주문인지 영원히 알 수 없는 기록”*이 애초에 생기지 않아요.
⑥과 ⑦은 함께 기억하세요. ⑥은 참조 무결성이 삭제에도 작동한다는 뜻이고, ⑦은 삭제 명령마다 사라지는 범위가 다르다는 뜻입니다. DELETE는 행, TRUNCATE는 모든 행, DROP은 테이블 전체 — 실행 뒤 무엇이 남는지부터 생각하는 습관이 오래 갑니다.
실습 DB에서 members 1번을 지워봐도 괜찮습니다 — 오류가 나면서 삭제가
막히므로 데이터는 그대로예요. 다만 자식 행을 먼저 지우는 식으로 더 실험했다면
init.sql을 다시 실행해 원래 상태로 되돌려 두세요.
다 풀었다면
처음 보는 테이블 앞에서 컬럼부터 훑지 않고 — 이름 · 한 행의 의미 · 컬럼과 규칙 · 기본키 · 외래키를 순서대로 묻게 됐다면 섹션 3의 목표는 전부 달성입니다.
DBeaver 좌측 트리에서 shop → Tables 를 열고 products 와 categories 를
차례로 더블클릭해 보세요. Columns 탭에서 ③ 컬럼과 규칙을, Constraints
탭에서 ④ 기본키를, Foreign Keys 탭에서 ⑤ 외래키를 확인할 수 있습니다.
오늘 배운 다섯 칸을 이 두 테이블에 대해서도 채워보세요.
이제 이미 있는 테이블을 읽는 힘은 갖췄습니다. 다음은 반대 방향이에요 — 표가 하나도 없는 상태에서 업무 이야기만 듣고, 어떤 테이블을 몇 개 만들고 어떻게 연결할지 정하는 데이터베이스 모델링입니다.