F-LAB JAVA · 7주차 · Phase 1 · SQL JOIN: 관계형 DB의 본질
📚 7주차 시작 — Part A: 데이터 모델링과 ORM
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
관계형 DB 는 정규화 (Normalization) 를 통해 데이터 중복을 제거하기 때문에 하나의 정보가 여러 테이블에 분산되며, "이 직원은 어느 부서?" 같은 자연스러운 질문에 답하려면 흩어진 테이블을 다시 합쳐야 해서 JOIN 이 필요하다.
관계형 DB 의 핵심 설계 원칙은 정규화 — 데이터 중복을 제거하고 무결성을 높이기 위해 큰 테이블을 작은 테이블로 분할한다.
예를 들어 직원 정보를 한 테이블에 다 넣지 않고 employees (직원) 테이블과 departments (부서) 테이블 로 나누면, 부서명을 한 곳에서만 관리해 부서명 변경 시 한 번만 수정하면 된다 (중복 제거).
하지만 비용도 있다 — "Alice 가 어느 부서인가?" 같은 자연스러운 질문에 답하려면 두 테이블을 다시 합쳐야 하므로 JOIN 연산이 필요해진다.
즉 JOIN 은 단순한 SQL 문법이 아니라 정규화의 필연적 결과 이며, 7주차의 모든 학습 (ORM/JPA/연관관계 매핑) 의 출발점이다.
정규화 = 정리된 책장:
정규화 전 (하나의 큰 테이블):
- 한 책상에 책/저자정보/출판사정보 모두
- "민음사" 정보가 책 100권에 100번
- 민음사 주소 바뀌면 100번 수정
- 중복 ↑
정규화 후 (책장 분리):
- 책 책장 (제목, 저자_id, 출판사_id)
- 저자 책장 (저자_id, 저자명)
- 출판사 책장 (출판사_id, 주소)
- 민음사 주소 바뀌면 1번 수정
- 중복 ↓
JOIN (책 찾기):
- "이 책의 저자명?" 답하려면
- 책 책장 → 저자_id 확인
- 저자 책장에서 그 id 찾기
- 두 책장 합치기 = JOIN
ILIC 의 정규화:
- shipments (배송)
- bookings (예약)
- customers (고객)
- 모두 분리 → 정규화
- "이 배송의 고객명?" → JOIN
트레이드오프:
- 중복 ↓, 무결성 ↑
- JOIN 비용 ↑
- 정규화 = 절약 + 성능 비용
→ JOIN = 정규화의 결과 (분산된 데이터를 합쳐야), 트레이드오프 (무결성 ↑ vs 성능 ↓).
1. JOIN 이 필요한 이유
2. 정규화의 정의
3. 정규화의 효과 (중복 제거)
4. 정규화의 비용 (JOIN 필요)
5. employees / departments 예시
6. 여러 테이블 분산
7. 비정규화 (역정규화)
8. JOIN 등장 배경
9. 면접 + 자기 점검
JOIN 이 필요한 이유:
"왜 하나의 정보가
여러 테이블에 흩어져 있나?"
→ 정규화 때문
→ 그래서 합쳐야
일상 질문:
"Alice 의 부서는?"
"이 주문의 고객명은?"
"이 배송의 운임은?"
→ 모두 두 개 이상 테이블 정보
→ JOIN 필요
한 테이블이면? (반정상):
직원_정보:
- 사번, 이름, 부서명, 부서주소, ...
문제:
- 부서 정보가 직원마다 중복
- 부서명 바뀌면 모든 행 수정
- 부서만 있는 정보 저장 X
- 데이터 무결성 ↓
→ 그래서 정규화
RDB 의 설계 정신:
- 중복 제거 (정규화)
- 무결성 우선
- JOIN 으로 합침
→ JOIN = RDB 의 본질
JOIN 필요성 (ILIC)
ILIC 102 테이블:
- shipments (배송)
- bookings (예약)
- customers (고객)
- freights (운임)
- shipment_items (배송 품목)
- 모두 정규화로 분리
"이 배송의 고객명/운임/품목 전부?":
- shipments + bookings + customers + freights + shipment_items
- JOIN 5개 테이블
→ 자연스러운 질문에 JOIN 필수
→ ILIC = 정규화 + JOIN 의 실제
JOIN 이 왜 필요한가?
답:
1. 핵심:
일상 질문:
한 테이블이면:
본질:
정규화 (Normalization):
데이터 중복을 제거하기 위해:
- 큰 테이블을 분할
- 작은 테이블로
- 관계 설정 (FK)
→ 1NF, 2NF, 3NF, BCNF, 4NF, 5NF
정규형:
1NF (제1정규형):
- 원자값 (한 칸에 한 값)
- 반복 그룹 X
2NF (제2정규형):
- 1NF + 부분 함수 종속 제거
3NF (제3정규형):
- 2NF + 이행 함수 종속 제거
BCNF:
- 3NF 의 더 엄격한 버전
→ 실무는 보통 3NF 까지
정규화 목적:
1. 데이터 중복 제거
2. 데이터 무결성 ↑
3. 이상 현상 방지
- 삽입 이상
- 수정 이상
- 삭제 이상
이상 현상 (정규화 안 했을 때):
수정 이상:
- 부서명 바뀌면 100 행 수정
- 누락 시 데이터 불일치
삽입 이상:
- 직원 없는 부서 못 넣음
- (직원 테이블에만 부서 정보)
삭제 이상:
- 마지막 직원 삭제 시
- 부서 정보도 사라짐
정규화 (ILIC)
ILIC 가 정규화 안 했다면:
shipment_flat (하나의 큰 테이블):
- id, bl_no, status,
- customer_id, customer_name, customer_address, customer_phone,
- port_loading, port_loading_country,
- freight_amount, freight_currency,
- item_1_name, item_1_quantity,
- item_2_name, item_2_quantity,
- ...
문제:
- 한 고객 100 배송 → 고객명 100 번 중복
- 고객 주소 바뀌면 100 번 수정
- 품목 가변 → 컬럼 무한 증가
정규화 후 (ILIC 실제):
- shipments, customers, ports, freights, shipment_items
- 분리, FK 로 연결
- JOIN 으로 합치기
정규화 (Normalization) 의 정의는?
답:
1. 정규화:
정규형:
목적:
이상 현상:
중복 제거:
같은 정보:
- 한 곳에서만 저장
- 변경 시 한 번만
- 일관성 ↑
부서 정보 (정규화 전):
직원 테이블:
| id | name | dept_name | dept_addr |
|----|-------|--------------|----------------|
| 1 | Alice | Engineering | 서울 강남 |
| 2 | Bob | Engineering | 서울 강남 |
| 3 | Carol | Engineering | 서울 강남 |
→ "Engineering, 서울 강남" 이 3번 중복
→ 부서 이전 시 3번 수정
정규화 후:
직원: { id, name, dept_id }
부서: { id, dept_name, dept_addr }
→ 부서 정보 1번만
→ 이전 시 1번 수정
무결성 ↑:
중복 X:
- 데이터 불일치 X
- "Engineering" vs "engineering"
- 누락 위험 X
- 신뢰
저장 공간:
중복 ↓ = 공간 ↓:
- 디스크 절약
- 캐시 효율
- 백업/복원 빠름
하지만:
- 현대는 공간 < 무결성
- 무결성이 더 중요
-- 중복 제거 효과 (ILIC)
-- 고객 테이블 (정규화 후)
CREATE TABLE customers (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
address VARCHAR(200),
phone VARCHAR(20)
);
-- 배송 테이블 (FK 로 참조)
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50),
customer_id BIGINT,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
-- 효과:
-- ABC 무역이 1000 배송 등록 시:
-- - 정규화 X: "ABC 무역" 정보 1000 번 중복
-- - 정규화 O: customers 에 1번, shipments 에 customer_id 만
-- ABC 무역 주소 변경:
-- - 정규화 X: 1000 행 UPDATE
-- - 정규화 O: 1 행 UPDATE
-- → 압도적 차이
정규화의 효과 (중복 제거) 는?
답:
1. 중복 제거:
예시:
무결성:
공간:
정규화의 비용:
데이터 분산:
- 여러 테이블
- 자연 질문에 JOIN 필요
- 성능 ↓ (JOIN 비용)
JOIN 성능:
- 두 테이블의 매칭 (조인 알고리즘)
- 인덱스 활용 중요
- JOIN 개수 ↑ → 비용 ↑
- 5+ JOIN 은 느림
트레이드오프:
정규화 ↑:
- 무결성 ↑
- 중복 ↓
- JOIN 비용 ↑
정규화 ↓ (비정규화):
- 무결성 ↓
- 중복 ↑
- JOIN 비용 ↓ (조회 빠름)
→ 균형
현실의 선택:
OLTP (트랜잭션):
- 3NF 정도 정규화
- 무결성 우선
OLAP (분석):
- 비정규화
- 조회 성능 우선
- Star Schema, Snowflake
→ 시스템 성격
JOIN 비용 (ILIC)
ILIC OLTP 시스템:
- 3NF 정도
- JOIN 필요
배송 상세 조회:
SELECT s.*, c.name, p.port_name, f.amount
FROM shipments s
JOIN customers c ON s.customer_id = c.id
JOIN ports p ON s.port_id = p.id
LEFT JOIN freights f ON f.shipment_id = s.id
WHERE s.id = ?
→ 4 테이블 JOIN
→ 인덱스 (FK) 잘 만들면 빠름
→ ILIC 의 표준 패턴
통계 조회 (느림):
- 10+ 테이블 JOIN
- 별도 분석 DB / 캐시 / 비정규화 고려
정규화의 비용 (JOIN 필요) 는?
답:
1. 비용:
성능:
트레이드오프:
현실:
employees (직원):
┌────┬─────────┬─────────────┐
│ id │ name │ dept_id │
├────┼─────────┼─────────────┤
│ 1 │ Alice │ 101 │
│ 2 │ Bob │ 102 │
│ 3 │ Charlie │ NULL │
└────┴─────────┴─────────────┘
departments (부서):
┌─────┬──────────────┐
│ id │ dept_name │
├─────┼──────────────┤
│ 101 │ HR │
│ 102 │ Engineering │
│ 103 │ Sales │
└─────┴──────────────┘
관찰:
Alice 는 dept_id=101 → HR
Bob 은 dept_id=102 → Engineering
Charlie 는 dept_id=NULL → 부서 미배정
Sales (103) 부서는 직원 없음
→ 정규화 결과
"Alice 의 부서?":
1. employees 에서 Alice 찾음 → dept_id=101
2. departments 에서 id=101 찾음 → "HR"
→ 두 테이블 조회
→ JOIN
-- JOIN 으로 답
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE e.name = 'Alice';
-- 결과
-- name | dept_name
-- ------|----------
-- Alice | HR
4가지 JOIN (다음 Unit 들):
INNER JOIN:
- 양쪽 매칭만 (Alice, Bob)
- Charlie 제외 (dept_id NULL)
LEFT JOIN:
- 왼쪽 전부 + 매칭 (Alice, Bob, Charlie)
RIGHT JOIN:
- 오른쪽 전부 + 매칭 (HR, Eng, Sales)
FULL OUTER:
- 양쪽 전부 (모두 등장)
-- ILIC 의 직원/부서 패턴
-- (실제 ILIC 는 employees 아니라 다른 테이블)
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50),
customer_id BIGINT,
port_id BIGINT
);
CREATE TABLE customers (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE ports (
id BIGINT PRIMARY KEY,
port_name VARCHAR(100)
);
-- 배송 정보 + 고객명 + 항구명
SELECT s.bl_no, c.name AS customer_name, p.port_name
FROM shipments s
JOIN customers c ON s.customer_id = c.id
JOIN ports p ON s.port_id = p.id;
-- → ILIC 의 표준 패턴 (정규화 + JOIN)
employees / departments 예시 분석은?
답:
1. 두 테이블:
관계:
질문:
4가지 JOIN:
여러 테이블 분산:
하나의 도메인 정보:
- 여러 테이블에 나뉨
- FK 로 연결
- JOIN 으로 합침
→ 정규화의 본질
주문 정보 (4개 테이블):
orders (주문):
- id, customer_id, order_date
customers (고객):
- id, name, email
order_items (주문 품목):
- id, order_id, product_id, quantity
products (상품):
- id, name, price
→ "주문 1번의 모든 정보":
4 테이블 JOIN
1:N 관계 (대표):
customer 1 : N orders
order 1 : N order_items
product 1 : N order_items
→ FK 로 1:N 표현
N:M 관계 (중간 테이블):
학생 N : M 과목 (수강)
students (학생)
courses (과목)
enrollments (수강) — 중간 테이블
- student_id, course_id
→ N:M = 1:N + 1:N 분해
-- ILIC 의 도메인 분산 예시
-- 한 배송의 모든 정보 (분산):
-- shipments (1) + customers (1) + ports (2) + freights (N) + shipment_items (N)
-- + bookings (1) + bill_of_lading (1) + ...
-- 한 배송 조회 시:
SELECT
s.*,
c.name AS customer_name,
pl.port_name AS port_loading,
pd.port_name AS port_discharge,
f.amount AS freight_amount,
si.item_name
FROM shipments s
JOIN customers c ON s.customer_id = c.id
LEFT JOIN ports pl ON s.port_loading_id = pl.id
LEFT JOIN ports pd ON s.port_discharge_id = pd.id
LEFT JOIN freights f ON f.shipment_id = s.id
LEFT JOIN shipment_items si ON si.shipment_id = s.id
WHERE s.id = ?;
-- → 5+ 테이블 JOIN
-- → ILIC 의 일반적인 조회 패턴
-- → 인덱스 (FK) 잘 만들면 충분히 빠름
하나의 정보가 여러 테이블에 분산의 의미는?
답:
1. 분산:
연결:
1:N:
N:M:
비정규화 (Denormalization):
정규화 반대:
- 의도적 중복 허용
- JOIN 줄임
- 조회 성능 ↑
- 무결성 ↓ (트레이드)
언제 사용:
- 조회 매우 빈번 (읽기 위주)
- JOIN 너무 많아 느림
- 분석 / 리포트 / 캐시
- 데이터 변경 적음
→ 신중한 선택
비정규화 방법:
1. 컬럼 추가:
- shipments 에 customer_name 도 저장
- JOIN 없이 조회
2. 요약 테이블:
- 일별 통계 미리 계산
- shipment_daily_summary
3. 캐시:
- Redis 에 최종 결과
- DB JOIN 안 함
위험:
- 데이터 불일치
- customer_name 바뀌면 shipments 도 갱신
- 누락 시 불일치
- 갱신 복잡
- 한 정보 여러 곳
- 모두 동기화
→ 트리거 / 애플리케이션 로직 보완
-- ILIC 의 비정규화 예시 (가상)
-- 정규화 (기본):
SELECT s.*, c.name FROM shipments s JOIN customers c ON s.customer_id = c.id
-- 매 조회 JOIN
-- 비정규화 (조회 매우 빈번 시):
ALTER TABLE shipments ADD COLUMN customer_name VARCHAR(100);
-- shipments 에 고객명도 저장
-- 조회 시 JOIN X
SELECT * FROM shipments WHERE customer_id = 1;
-- 비용:
-- - 고객명 변경 시 customers + shipments 모두 갱신
-- - 트리거 또는 애플리케이션 로직
-- ILIC 실무:
-- - OLTP 는 정규화 기본
-- - 통계/리포트는 별도 요약 테이블 (비정규화)
-- - 또는 Redis 캐시
비정규화 (역정규화) 시나리오는?
답:
1. 비정규화:
언제:
방법:
위험:
JOIN 등장 흐름:
1. RDB 가 정규화 도입 (중복 제거)
2. 데이터가 여러 테이블 분산
3. 자연 질문은 여러 테이블 정보 필요
4. → JOIN 연산 등장
→ RDB 의 필수 기능
SQL 표준:
ANSI SQL:
- INNER JOIN
- LEFT/RIGHT/FULL OUTER JOIN
- CROSS JOIN
- SELF JOIN (같은 테이블)
→ 거의 모든 RDB 지원
7주차 학습 흐름:
Phase 1 (현재):
- JOIN 4가지 (1.1 → 1.5)
Phase 2-4:
- ORM 패러다임
- JPA 입문
- 엔티티 매핑
→ JOIN 이해 = JPA 의 출발
→ "객체와 관계 모델 미스매치" 의 핵심
JOIN ≠ JPA 의 끝:
JPA:
- 자동으로 JOIN SQL 생성
- 객체 그래프로 표현
- @OneToMany, @ManyToOne 등
하지만:
- JOIN 동작은 알아야
- JPA 가 만든 SQL 확인 위해
- N+1 문제 등 진단
→ JOIN 기초 → JPA 깊이
7주차 학습 → ILIC
ILIC 가 JPA 전:
- JdbcTemplate + SQL 직접
- JOIN 직접 작성
ILIC 가 JPA 후:
- @ManyToOne, @OneToMany 어노테이션
- JOIN SQL 자동 생성
- 객체 그래프로 탐색
Shipment shipment = repository.findById(1);
Customer customer = shipment.getCustomer();
// 내부적으로 JOIN SQL 자동
// 또는 Lazy Loading
→ JOIN 이해가 JPA 활용의 기반
JOIN 등장 배경은?
답:
1. 흐름:
SQL 표준:
7주차:
JPA 도:
| Q | 핵심 답변 |
|---|---|
| JOIN 필요한 이유? | 정규화 분산 |
| 정규화? | 중복 제거 |
| 정규형? | 1NF~5NF |
| 이상 현상? | 삽입/수정/삭제 |
| 중복 제거 효과? | 무결성 ↑ |
| JOIN 비용? | 분산 합치기 |
| 트레이드오프? | 무결성 vs 성능 |
| 비정규화? | 의도적 중복 |
| OLTP vs OLAP? | 정규화 vs 비정규화 |
| 4가지 JOIN? | INNER/LEFT/RIGHT/FULL |
답:
답:
답:
답:
답:
1. JOIN = 정규화의 결과
2. 정규화의 트레이드오프
3. 7주차의 출발
이번 Unit에서 JOIN 의 배경을 봤다면, 다음은 INNER JOIN (첫 번째 종류).
📚 Phase 1 — SQL JOIN
✅ Unit 1.1 JOIN 이 필요한 이유 ← 여기
⏭ Unit 1.2 INNER JOIN
⏭ Unit 1.3 LEFT/RIGHT JOIN
⏭ Unit 1.4 FULL OUTER JOIN
⏭ Unit 1.5 JOIN 선택 가이드 ★깊이
🗂️ Part A — 데이터 모델링과 ORM
📚 Phase 1 (1/5 진행)
총: 1/24 Unit
📚 7주차 시작 — Part A: 데이터 모델링과 ORM