F-LAB JAVA · 7주차 · Phase 1 · SQL JOIN: 관계형 DB의 본질
★ 깊이 파기 — Phase 1 완주, JOIN 학습의 정점
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
JOIN 선택은 "필요한 결과 행의 정의" 에서 결정되며 — 양쪽 다 있는 데이터만 INNER, 한쪽 기준 + 부가 정보는 LEFT, 양쪽 모두 보존은 FULL OUTER, 차집합은 LEFT + IS NULL — 실무는 LEFT JOIN 압도적이고 INNER 는 카운트·집계에 RIGHT 는 거의 안 쓰며 FK 인덱스가 JOIN 성능의 핵심이고 의도치 않은 카르테시안·N+1·과도한 JOIN 같은 안티 패턴을 피해야 한다.
JOIN 선택의 핵심은 "내가 원하는 결과 행이 무엇인가?" — 양쪽 매칭 행만 (INNER), 왼쪽 기준 + 매칭 (LEFT), 양쪽 모두 (FULL OUTER), 차집합 (LEFT + IS NULL) 중 어느 것인지가 명확하면 JOIN 종류가 결정된다.
실무 사용 빈도는 LEFT JOIN (~70%) > INNER JOIN (~25%) > FULL OUTER (~3%) > RIGHT JOIN (~2%) 정도로 — LEFT 가 압도적인 이유는 사람의 사고가 "기준 + 부가" 흐름이라 자연스럽고, INNER 는 의도가 명확한 카운트·집계에서 유리하며, RIGHT 는 LEFT 로 변환되어 거의 안 쓰인다.
JOIN 성능의 핵심은 FK 컬럼에 인덱스 — 인덱스가 없으면 Nested Loop 의 비용이 N×M 으로 폭증한다.
피해야 할 안티 패턴은 — (1) 의도치 않은 카르테시안 (조건 누락), (2) N+1 문제 (JPA), (3) 과도한 JOIN (5+ 테이블), (4) ORM 에서 무분별한 fetch join — 이를 알아야 7주차의 ORM/JPA 학습이 견고해진다.
JOIN 선택 = 식당 메뉴:
손님 질문 = 시나리오:
Q1: "이 식당의 인기 메뉴만 보여줘"
→ INNER JOIN
(메뉴 + 주문 모두 있는 인기만)
Q2: "모든 메뉴 + 그날 주문량 (없으면 0)"
→ LEFT JOIN
(메뉴 기준 + 주문 부가)
Q3: "메뉴와 주문 모두 (양쪽 다)"
→ FULL OUTER JOIN
(정합성 검증)
Q4: "오늘 안 팔린 메뉴는?"
→ LEFT JOIN + WHERE IS NULL
(차집합)
선택 기준 = 결과 정의:
- 어떤 행이 결과에?
- "필요한 데이터" 가 핵심
실무 빈도:
- LEFT 가 70%
- INNER 가 25%
- 나머지 5%
성능:
- 인덱스 (FK) 가 결정
- 없으면 식당이 마비
안티 패턴:
- 음식 안 정해놓고 손님 받기 (카르테시안)
- 매번 주방에 가기 (N+1)
- 메뉴 너무 복잡 (5+ JOIN)
→ JOIN 선택 = 결과 정의, 실무는 LEFT 압도적, 인덱스가 성능 좌우.
1. JOIN 선택 매트릭스
2. 실무 빈도와 이유
3. INNER JOIN 의 유리
4. LEFT JOIN 의 압도적 빈번
5. RIGHT JOIN 거의 안 쓰는 이유
6. 차집합 패턴
7. JOIN 성능과 인덱스
8. JOIN 안티 패턴 4가지
9. Phase 1 완주 + JPA 연결
| 시나리오 | 결과 정의 | JOIN 종류 |
|---|---|---|
| 양쪽 다 있는 데이터만 | 매칭만 | INNER JOIN |
| 왼쪽 모든 데이터 + 매칭 부가 | 왼쪽 보존 | LEFT JOIN |
| 양쪽 모든 데이터 (정합성) | 합집합 | FULL OUTER JOIN |
| 매칭 없는 왼쪽만 (차집합) | 왼쪽 - 매칭 | LEFT JOIN + WHERE IS NULL |
| 매칭 없는 오른쪽만 (반대 차집합) | 오른쪽 - 매칭 | (B) LEFT JOIN (A) + WHERE IS NULL |
결정 흐름:
Q: "결과에 어떤 행이 필요한가?"
├─ 양쪽 매칭만 → INNER
├─ 한쪽 (왼쪽) 기준 + 부가 → LEFT
├─ 양쪽 모두 → FULL OUTER (MySQL 우회)
├─ 한쪽에만 있는 행 → LEFT + IS NULL (차집합)
└─ 카르테시안 의도 → CROSS JOIN (드묾)
의도 명확하기:
잘못된 접근:
- "INNER 가 빠르니까 일단 INNER"
- "LEFT 가 안전하니까 무조건 LEFT"
올바른 접근:
- 결과 정의가 먼저
- JOIN 종류는 결과
- 의도 = 선택
SQL 작성 도움:
1. 결과 컬럼 정하기
2. 어떤 행이 빠지면 안 되는가?
→ LEFT 보존
3. 어떤 행이 있으면 안 되는가?
→ INNER 또는 WHERE
4. JOIN 결정
-- ILIC 의 JOIN 선택 매트릭스
-- 시나리오 1: 정상 매칭 통계 (INNER)
SELECT c.region, COUNT(*)
FROM shipments s
INNER JOIN customers c ON s.customer_id = c.id
GROUP BY c.region;
-- 고객 연결된 배송만 (임시 배송 제외)
-- → 의도: 정상 통계만
-- 시나리오 2: 대시보드 (LEFT)
SELECT s.bl_no, c.name, COALESCE(f.amount, 0) AS freight
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id
LEFT JOIN freights f ON f.shipment_id = s.id;
-- 모든 배송 표시 (운임 미정도)
-- → 의도: 누락 X
-- 시나리오 3: 정합성 검증 (FULL OUTER, MySQL 우회)
SELECT s.id, c.id
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id
UNION
SELECT s.id, c.id
FROM shipments s
RIGHT JOIN customers c ON s.customer_id = c.id
WHERE s.id IS NULL OR c.id IS NULL;
-- 양방향 누락 식별
-- 시나리오 4: 미사용 고객 (차집합)
SELECT c.id, c.name
FROM customers c
LEFT JOIN shipments s ON s.customer_id = c.id
WHERE s.id IS NULL;
-- 배송 없는 고객 → 마케팅 대상
JOIN 선택 매트릭스 4가지 시나리오는?
답:
1. 양쪽 매칭만:
왼쪽 + 부가:
양쪽 모두:
차집합:
실무 빈도:
LEFT JOIN: ~70%
INNER JOIN: ~25%
FULL OUTER: ~3%
RIGHT JOIN: ~2%
→ LEFT 가 압도적
왜 LEFT?:
1. 사고 흐름:
- "이 데이터 + 부가 정보"
- 기준 + 추가
2. 안전:
- 누락 위험 X
- 데이터 보존
3. 비즈니스 의미:
- "모든 X 보여줘"
- 자주 요청
→ 자연스러움
왜 INNER 도 자주:
- 통계/집계 (확실한 매칭)
- 카운트 (양쪽 매칭만)
- 필터링 효과
- 명확한 의도
→ 분석 쿼리에서
FULL OUTER 드문 이유:
- 정합성 검증 외 사용 X
- MySQL 미지원 (귀찮)
- LEFT 로 대부분 해결
- 행 수 폭증 위험
RIGHT 거의 안 쓰는 이유:
- LEFT 로 동일 표현
- 가독성 ↓
- 사고 흐름 어색
- 코드 리뷰에서 LEFT 권장
→ 일관성
ILIC 의 JOIN 빈도
ILIC SQL 의 예상 분포:
- LEFT JOIN: 70%
(대시보드, 상세 조회)
- INNER JOIN: 25%
(통계, 카운트, 정상 매칭만)
- 기타: 5%
(정합성 검증, 차집합)
RIGHT JOIN: 0%
→ 모두 LEFT 로 표현
→ 실무 통계와 일치
실무에서 LEFT JOIN 이 가장 빈번한 이유는?
답:
1. 빈도:
이유:
INNER:
RIGHT/FULL:
INNER 가 유리한 경우:
1. 통계 / 집계
2. 카운트
3. 정상 매칭만 필요
4. 의미상 양쪽 다 있어야
-- 부서별 직원 수 (정상 매칭만)
SELECT d.dept_name, COUNT(*) AS emp_count
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id
GROUP BY d.id, d.dept_name;
-- 결과:
-- HR | 1 (Alice)
-- Engineering | 1 (Bob)
-- (Sales 는 직원 없음 → 제외)
-- (Charlie 는 부서 NULL → 제외)
-- 활성 고객의 배송 수
SELECT COUNT(*)
FROM shipments s
INNER JOIN customers c ON s.customer_id = c.id
WHERE c.active = TRUE;
-- 매칭된 행만 카운트
성능 이점 (INNER):
- 옵티마이저 친화적
- 행 수 ↓ (필터 효과)
- 인덱스 활용 좋음
- 가장 빠름 일반적
→ 가능하면 INNER
의도 명확:
INNER JOIN:
- "양쪽 매칭만"
- 코드 읽는 사람도 이해
- 명확한 시그널
→ 가독성
-- INNER 활용 (ILIC)
-- 1. 운영 통계
SELECT
s.status,
COUNT(*) AS count,
SUM(f.amount) AS total_freight
FROM shipments s
INNER JOIN freights f ON f.shipment_id = s.id
GROUP BY s.status;
-- 운임 책정된 배송만 (정상 케이스 통계)
-- 2. 활성 고객 배송 (정상)
SELECT s.bl_no, c.name
FROM shipments s
INNER JOIN customers c
ON s.customer_id = c.id
WHERE c.active = TRUE;
-- 활성 고객 + 정상 배송만
-- 3. 카운트 (의도 명확)
SELECT COUNT(*)
FROM shipments s
INNER JOIN bookings b ON s.booking_id = b.id
WHERE b.status = 'CONFIRMED';
INNER JOIN 은 언제 유리한가?
답:
1. 언제:
이유:
성능:
활용:
LEFT 시나리오:
1. 대시보드
2. 상세 조회
3. 리포트
4. 누락 데이터 식별
5. NULL 허용 컬럼 조회
-- 대시보드 (모든 배송 + 정보)
SELECT
s.bl_no,
s.status,
COALESCE(c.name, '미등록') AS customer,
COALESCE(p.port_name, '미정') AS port,
COALESCE(f.amount, 0) AS freight
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id
LEFT JOIN ports p ON s.port_loading_id = p.id
LEFT JOIN freights f ON f.shipment_id = s.id
ORDER BY s.created_at DESC
LIMIT 100;
-- 정보 누락 X
-- 모든 배송 보여줌
-- 한 배송의 모든 정보
SELECT s.*, c.name, p.port_name, f.amount
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id
LEFT JOIN ports p ON s.port_loading_id = p.id
LEFT JOIN freights f ON f.shipment_id = s.id
WHERE s.id = ?;
-- 일부 정보가 NULL 이어도 배송 보임
-- NULL → 기본값
COALESCE(c.name, '미등록') -- 표준
IFNULL(c.name, '미등록') -- MySQL
NVL(c.name, '미등록') -- Oracle
-- 여러 단계
COALESCE(c.name, b.name, '미등록')
-- 첫 NULL 아닌 값
-- 1:N (고객 + 배송 N개)
SELECT
c.name,
COUNT(s.id) AS shipment_count,
SUM(COALESCE(f.amount, 0)) AS total_freight
FROM customers c
LEFT JOIN shipments s ON s.customer_id = c.id
LEFT JOIN freights f ON f.shipment_id = s.id
GROUP BY c.id, c.name;
-- 모든 고객 (배송 0 인 고객도)
-- LEFT JOIN 종합 (ILIC, 5+ 테이블)
-- 배송 종합 대시보드
SELECT
s.id,
s.bl_no,
s.status,
COALESCE(c.name, '미등록') AS customer,
COALESCE(pl.port_name, '-') AS port_loading,
COALESCE(pd.port_name, '-') AS port_discharge,
COALESCE(f.amount, 0) AS freight,
COUNT(si.id) AS item_count
FROM shipments s
LEFT 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.status IN ('BOOKED', 'CONFIRMED', 'SHIPPED')
GROUP BY s.id, s.bl_no, s.status, c.name, pl.port_name, pd.port_name, f.amount
ORDER BY s.created_at DESC;
-- ILIC 의 표준 대시보드 패턴
-- 모든 활성 배송 + 부가 정보 (누락 없음)
LEFT JOIN 의 압도적 빈번은?
답:
1. 시나리오:
NULL 처리:
1:N:
종합:
-- RIGHT JOIN (안 좋음)
SELECT s.bl_no, c.name
FROM shipments s
RIGHT JOIN customers c ON s.customer_id = c.id;
-- 동치 LEFT JOIN (권장)
SELECT s.bl_no, c.name
FROM customers c
LEFT JOIN shipments s ON s.customer_id = c.id;
-- → 결과 같음
-- → LEFT 가 가독성 ↑
가독성 ↓:
RIGHT JOIN 의 문제:
- FROM 절 (왼쪽) 이 기준 아님
- 기준이 오른쪽 (헷갈림)
- 다중 JOIN 에서 더 복잡
LEFT JOIN:
- FROM 절이 기준
- 왼쪽으로 흐름
- 직관적
-- RIGHT JOIN 섞이면 (혼란)
SELECT *
FROM A
LEFT JOIN B ON ...
RIGHT JOIN C ON ...
LEFT JOIN D ON ...;
-- 기준이 어디인지 모름
-- 매우 혼란
-- 일관 LEFT JOIN (명확)
SELECT *
FROM C -- 기준 (RIGHT 의 오른쪽)
LEFT JOIN A ON ...
LEFT JOIN B ON ...
LEFT JOIN D ON ...;
코드 리뷰 표준:
팀 룰:
- "RIGHT JOIN 사용 X"
- 모두 LEFT 로 변환
- 일관성
→ 거의 모든 팀
-- RIGHT → LEFT 변환
-- 원본
SELECT * FROM A RIGHT JOIN B ON A.x = B.y;
-- 변환
SELECT * FROM B LEFT JOIN A ON A.x = B.y;
-- 1. FROM 절 순서 바꿈 (A↔B)
-- 2. RIGHT → LEFT
-- 3. ON 절 그대로
ILIC 의 RIGHT JOIN 정책
ILIC 코드 표준:
- RIGHT JOIN 사용 금지
- 모두 LEFT JOIN 으로
- 코드 리뷰에서 확인
이유:
1. 가독성
2. 일관성
3. 다중 JOIN 명확
변환 예:
원본 (외부 코드 가져옴):
FROM A RIGHT JOIN B ON ...
수정:
FROM B LEFT JOIN A ON ...
→ "RIGHT 발견 시 LEFT 로 변환" 룰
RIGHT JOIN 거의 안 쓰는 이유는?
답:
1. LEFT 로 표현:
가독성:
다중 JOIN:
표준:
차집합:
"한쪽에는 있지만 다른 쪽에는 없는 행":
- LEFT - 매칭 = 왼쪽만
- RIGHT - 매칭 = 오른쪽만
→ LEFT JOIN + WHERE IS NULL
-- 부서 없는 직원
SELECT e.id, e.name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id
WHERE d.id IS NULL;
-- Charlie 만
-- 동작:
-- 1. LEFT JOIN 결과 (모든 직원)
-- 2. WHERE d.id IS NULL (매칭 안 된 행만)
-- → 매칭 X 인 직원
-- 직원 없는 부서 (반대)
SELECT d.id, d.dept_name
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id
WHERE e.id IS NULL;
-- Sales 만
-- 키 포인트:
-- - 기준 테이블 (departments) 을 LEFT 의 왼쪽
-- - 매칭 안 되는 행만 (e.id IS NULL)
-- LEFT JOIN + IS NULL
SELECT c.id, c.name
FROM customers c
LEFT JOIN shipments s ON s.customer_id = c.id
WHERE s.id IS NULL;
-- 동치 NOT EXISTS
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM shipments s WHERE s.customer_id = c.id
);
-- 성능:
-- - 비슷 (옵티마이저가 비슷하게 처리)
-- - NOT EXISTS 가 의도 더 명확
-- - 큰 데이터에서 NOT EXISTS 가 약간 유리
-- NOT IN (주의!)
SELECT c.id, c.name
FROM customers c
WHERE c.id NOT IN (
SELECT customer_id FROM shipments
);
-- 주의: NULL 있으면 결과 X!
-- shipments.customer_id 에 NULL 있으면
-- NOT IN (..., NULL) → 항상 NULL
-- → 결과 0 행
-- NOT EXISTS 가 안전
-- 차집합 활용 (ILIC)
-- 1. 미사용 고객 (마케팅 대상)
SELECT c.id, c.name, c.email
FROM customers c
LEFT JOIN shipments s ON s.customer_id = c.id
WHERE s.id IS NULL;
-- → 가입했지만 한 번도 배송 X
-- 2. 운임 미책정 배송 (확인 필요)
SELECT s.id, s.bl_no
FROM shipments s
LEFT JOIN freights f ON f.shipment_id = s.id
WHERE f.id IS NULL;
-- → 운임 미정 (긴급 처리)
-- 3. 사용 안 된 항구 (정리?)
SELECT p.id, p.port_name
FROM ports p
LEFT JOIN shipments s
ON s.port_loading_id = p.id
OR s.port_discharge_id = p.id
WHERE s.id IS NULL;
-- 4. NOT EXISTS 버전 (큰 데이터)
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM shipments s
WHERE s.customer_id = c.id
);
-- → 큰 customers 테이블에 유리
부서 없는 직원 / 직원 없는 부서 찾기 쿼리는?
답:
1. 부서 없는 직원:
직원 없는 부서:
NOT EXISTS:
NOT IN 주의:
JOIN 알고리즘 (DB 내부):
1. Nested Loop Join:
- 외부 테이블 × 내부 테이블
- 작은 테이블 + 인덱스에 유리
2. Hash Join:
- 한쪽으로 해시 테이블
- 큰 테이블 + 동등 비교
3. Sort-Merge Join:
- 정렬 후 병합
- 정렬된 데이터 / 범위
→ 옵티마이저가 선택
FK 인덱스 중요:
ON s.customer_id = c.id
인덱스 없으면:
- shipments 의 customer_id 풀 스캔
- 또는 customers 풀 스캔
- N × M 비용
인덱스 있으면:
- O(N × log M) (B-Tree)
- 또는 더 빠름
- 10-100배 차이
→ FK 컬럼은 무조건 인덱스
PK 인덱스 자동:
PRIMARY KEY 자동 인덱스:
- customers.id
- 빠른 조회
FK 는 자동 X (MySQL):
- 명시적 인덱스 필요
- InnoDB 는 일부 자동 (확인 필요)
→ FK 인덱스 만들기
-- 쿼리 실행 계획 확인
EXPLAIN SELECT *
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id
WHERE s.status = 'SHIPPED';
-- 결과 확인:
-- - type: ref (인덱스), ALL (풀 스캔)
-- - key: 사용된 인덱스
-- - rows: 추정 행 수
-- - Extra: 추가 정보
-- ALL 이면 → 인덱스 추가 검토
조인 순서 (옵티마이저):
1. 작은 테이블 먼저
2. 선택도 높은 조건 먼저
3. 인덱스 활용 가능 우선
STRAIGHT_JOIN 으로 강제 가능 (MySQL):
SELECT /*+ STRAIGHT_JOIN */ ...
→ 보통 옵티마이저에 맡김
5+ JOIN 주의:
JOIN 개수 증가:
- 옵티마이저 선택 ↑
- 계산 비용 ↑
- 실행 계획 불안정
대안:
- 쿼리 분할
- 미리 계산
- 캐시
- 비정규화
-- 성능 최적화 (ILIC)
-- 1. FK 인덱스 확인/추가
CREATE INDEX idx_shipments_customer ON shipments(customer_id);
CREATE INDEX idx_shipments_port_loading ON shipments(port_loading_id);
CREATE INDEX idx_freights_shipment ON freights(shipment_id);
-- → JOIN 성능 ↑
-- 2. EXPLAIN 으로 확인
EXPLAIN SELECT s.*, c.name, f.amount
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id
LEFT JOIN freights f ON f.shipment_id = s.id
WHERE s.status = 'SHIPPED';
-- type: ref 가 좋음 (인덱스 사용)
-- 3. 5+ JOIN 의 분할 고려
-- Bad: 7 테이블 JOIN
-- Good: 2-3 테이블 + 별도 쿼리
-- 4. 자주 쓰는 통계는 캐시 / 요약 테이블
CREATE TABLE shipment_daily_summary (
date DATE PRIMARY KEY,
total_count INT,
total_freight DECIMAL(15,2)
);
-- 매일 배치로 갱신
-- 조회 시 JOIN X
JOIN 성능과 인덱스 의 관계는?
답:
1. 알고리즘:
FK 인덱스:
EXPLAIN:
5+ JOIN:
-- ❌ JOIN 조건 누락
SELECT *
FROM shipments s, customers c;
-- 또는
SELECT *
FROM shipments s
CROSS JOIN customers c;
-- → 모든 조합 (1000 × 500 = 500,000 행!)
-- → 의도치 않으면 재앙
-- ✓ 명시적 JOIN
SELECT *
FROM shipments s
INNER JOIN customers c ON s.customer_id = c.id;
N+1 문제:
목록 조회 (1 쿼리):
SELECT * FROM customers;
각 고객의 배송 (N 쿼리):
SELECT * FROM shipments WHERE customer_id = ?;
SELECT * FROM shipments WHERE customer_id = ?;
... (고객 수만큼)
→ 1 + N 쿼리 (느림!)
-- ❌ N+1
List<Customer> customers = ... ;
for (Customer c : customers) {
c.getShipments(); // 매번 쿼리!
}
-- ✓ JOIN 으로 한 번에
SELECT c.*, s.*
FROM customers c
LEFT JOIN shipments s ON s.customer_id = c.id;
-- 또는 JPA Fetch Join
SELECT c FROM Customer c JOIN FETCH c.shipments;
-- ❌ 너무 많은 JOIN (10+ 테이블)
SELECT *
FROM a
JOIN b ON ...
JOIN c ON ...
JOIN d ON ...
JOIN e ON ...
JOIN f ON ...
JOIN g ON ...
JOIN h ON ...
JOIN i ON ...
JOIN j ON ...;
-- 문제:
-- - 옵티마이저 혼란
-- - 인덱스 활용 어려움
-- - 유지보수 ↓
-- - 디버깅 어려움
-- ✓ 분할
-- 1) 핵심 JOIN (3-4 테이블)
-- 2) 별도 쿼리 + 애플리케이션 합치기
-- 3) 또는 미리 계산
// ❌ 모든 연관관계 EAGER (안 좋음)
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
// 조회 시 모든 것 자동 JOIN
// → 너무 많은 데이터
// ✓ LAZY 기본 + 필요 시 fetch join
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orders;
// 필요 시 명시적
@Query("SELECT c FROM Customer c JOIN FETCH c.orders")
List<Customer> findAllWithOrders();
-- ❌ SELECT *
SELECT *
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id;
-- 모든 컬럼 (필요 없는 것도)
-- 네트워크 / 메모리 낭비
-- ✓ 필요한 컬럼만
SELECT s.id, s.bl_no, c.name
FROM shipments s
LEFT JOIN customers c ON s.customer_id = c.id;
ILIC 의 안티 패턴 회피
ILIC 코드 표준:
1. JOIN 조건 명시 (카르테시안 X)
2. N+1 회피
- JPA fetch join
- Batch fetch
- 명시적 JOIN
3. JOIN 5 이하 (보통)
- 7+ 면 분할
4. EAGER 최소
- 기본 LAZY
- 필요 시 명시
5. SELECT 컬럼 명시
- SELECT * 지양
코드 리뷰 체크리스트:
□ JOIN 조건 있나?
□ N+1 가능성?
□ 컬럼 명시?
□ EXPLAIN 했나?
JOIN 안티 패턴 4가지는?
답:
1. 카르테시안:
N+1:
과도한 JOIN:
무분별 fetch:
Phase 1 — SQL JOIN
Unit 1.1 — JOIN 이 필요한 이유
- 정규화 → 분산 → JOIN
Unit 1.2 — INNER JOIN
- 양쪽 매칭 (교집합)
Unit 1.3 — LEFT / RIGHT JOIN
- 한쪽 보존 (OUTER)
Unit 1.4 — FULL OUTER JOIN
- 양쪽 보존 (합집합)
Unit 1.5 — JOIN 선택 가이드 ★ ← 여기
- 시나리오/실무/안티 패턴
Phase 1 핵심 메시지:
"JOIN 은 정규화의 필연적 결과이며
'결과 행의 정의' 가 JOIN 종류를 결정한다.
실무는 LEFT 위주, FK 인덱스가 성능 핵심,
안티 패턴 회피가 곧 좋은 SQL."
6주차 ↔ 7주차 연결:
6주차 (DB 접근):
- JDBC / JdbcTemplate
- 트랜잭션 ACID
- DataSource
7주차 Phase 1 (JOIN):
- SQL 의 본질
- 정규화 + 합치기
7주차 Phase 2-4 (ORM/JPA):
- JOIN 을 객체 관계로
- JPA 가 SQL 자동 생성
- @ManyToOne 등
JPA 와 JOIN 의 관계:
JPA 는 객체 그래프:
- Customer.shipments (List)
- Shipment.customer (참조)
JPA 가 자동 SQL:
- JOIN SQL 생성
- LEFT JOIN / INNER JOIN
- 개발자는 객체로 표현
하지만:
- JOIN 동작 알아야
- JPA SQL 확인 가능
- N+1 같은 문제 진단
→ Phase 1 = JPA 의 기초
Phase 1 → Phase 2:
Phase 2 — ORM 패러다임:
Unit 2.1 — 객체-관계 미스매치
Unit 2.2 — ORM 의 정의와 효과
주제:
- 객체 vs 관계 모델 차이
- ORM 의 필요성
- Hibernate, JPA 등
→ "왜 ORM 이 필요한가" 답
| Q | 핵심 답변 |
|---|---|
| JOIN 선택 기준? | 결과 행 정의 |
| LEFT 가 빈번? | 사고 흐름 |
| INNER 유리? | 통계/집계 |
| RIGHT 안 쓰는? | LEFT 표현 |
| 차집합? | LEFT + IS NULL |
| 성능 핵심? | FK 인덱스 |
| EXPLAIN? | 실행 계획 |
| N+1? | JPA 문제 |
| 안티 패턴? | 4가지 |
| Phase 1 → 2? | JPA 로 |
답:
답:
답:
답:
답:
1. JOIN 선택 = "결과 행 정의"
2. 실무 빈도와 성능
3. 안티 패턴 4가지 회피
📚 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 선택 가이드 ★깊이 ← 여기, Phase 1 완주
→ JOIN 4종 완전 정복
→ 실무 활용 + 안티 패턴 회피
→ JPA 학습의 기반
🔄 Phase 2 — ORM 패러다임
Unit 2.1 — 객체-관계 미스매치
Unit 2.2 — ORM 의 정의와 효과
Phase 2 주제:
🗂️ Part A — 데이터 모델링과 ORM
✅ Phase 1 — SQL JOIN (5) ← 완주!
⏭ Phase 2 — ORM 패러다임 (2)
⏭ Phase 3 — JPA 입문 (4)
⏭ Phase 4 — JPA 엔티티 매핑 (5)
🔄 Part B — 트랜잭션 추상화의 진화
⏭ Phase 5 — 수동 트랜잭션의 한계 (2)
⏭ Phase 6 — PlatformTransactionManager (3)
⏭ Phase 7 — @Transactional (3)
총: 5/24 Unit (Phase 1 완주!)
★ 깊이 파기 — Phase 1 완주, JOIN 학습의 정점