7주차 Unit 1.5 — JOIN 선택 가이드

Psj·2026년 6월 1일

F-lab

목록 보기
218/240

Unit 1.5 — JOIN 선택 가이드

F-LAB JAVA · 7주차 · Phase 1 · SQL JOIN: 관계형 DB의 본질
★ 깊이 파기 — Phase 1 완주, JOIN 학습의 정점


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • JOIN 선택 매트릭스 4가지 시나리오는?
  • 실무에서 LEFT JOIN 이 가장 빈번 한 이유는?
  • INNER JOIN 은 언제 유리 한가?
  • RIGHT JOIN 거의 안 쓰는 이유는?
  • 부서 없는 직원 찾기 쿼리 는?
  • 직원 없는 부서 찾기 쿼리 는?
  • JOIN 성능과 인덱스 의 관계는?
  • JOIN 안티 패턴 4가지 는?
  • JPA 와 JOIN 의 관계는?

🎯 핵심 한 문장

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 압도적, 인덱스가 성능 좌우.


🧭 9개 섹션 로드맵

1. JOIN 선택 매트릭스
2. 실무 빈도와 이유
3. INNER JOIN 의 유리
4. LEFT JOIN 의 압도적 빈번
5. RIGHT JOIN 거의 안 쓰는 이유
6. 차집합 패턴
7. JOIN 성능과 인덱스
8. JOIN 안티 패턴 4가지
9. Phase 1 완주 + JPA 연결

1️⃣ JOIN 선택 매트릭스

1.1 시나리오 매트릭스

시나리오결과 정의JOIN 종류
양쪽 다 있는 데이터만매칭만INNER JOIN
왼쪽 모든 데이터 + 매칭 부가왼쪽 보존LEFT JOIN
양쪽 모든 데이터 (정합성)합집합FULL OUTER JOIN
매칭 없는 왼쪽만 (차집합)왼쪽 - 매칭LEFT JOIN + WHERE IS NULL
매칭 없는 오른쪽만 (반대 차집합)오른쪽 - 매칭(B) LEFT JOIN (A) + WHERE IS NULL

1.2 결정 흐름

결정 흐름:

  Q: "결과에 어떤 행이 필요한가?"

  ├─ 양쪽 매칭만 → INNER
  ├─ 한쪽 (왼쪽) 기준 + 부가 → LEFT
  ├─ 양쪽 모두 → FULL OUTER (MySQL 우회)
  ├─ 한쪽에만 있는 행 → LEFT + IS NULL (차집합)
  └─ 카르테시안 의도 → CROSS JOIN (드묾)

1.3 의도 명확하기

의도 명확하기:

  잘못된 접근:
    - "INNER 가 빠르니까 일단 INNER"
    - "LEFT 가 안전하니까 무조건 LEFT"

  올바른 접근:
    - 결과 정의가 먼저
    - JOIN 종류는 결과
    - 의도 = 선택

1.4 SQL 직접 작성 시 도움

SQL 작성 도움:

  1. 결과 컬럼 정하기
  2. 어떤 행이 빠지면 안 되는가?
     → LEFT 보존
  3. 어떤 행이 있으면 안 되는가?
     → INNER 또는 WHERE
  4. JOIN 결정

1.5 ILIC 의 맥락

-- 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;
-- 배송 없는 고객 → 마케팅 대상

1.6 자기 점검 답변

JOIN 선택 매트릭스 4가지 시나리오는?

:
1. 양쪽 매칭만:

  • INNER
  1. 왼쪽 + 부가:

    • LEFT
  2. 양쪽 모두:

    • FULL OUTER
  3. 차집합:

    • LEFT + IS NULL

2️⃣ 실무 빈도와 이유

2.1 실무 빈도 (대략)

실무 빈도:

  LEFT JOIN:    ~70%
  INNER JOIN:   ~25%
  FULL OUTER:   ~3%
  RIGHT JOIN:   ~2%

→ LEFT 가 압도적

2.2 왜 LEFT 가 빈번한가

왜 LEFT?:

1. 사고 흐름:
   - "이 데이터 + 부가 정보"
   - 기준 + 추가

2. 안전:
   - 누락 위험 X
   - 데이터 보존

3. 비즈니스 의미:
   - "모든 X 보여줘"
   - 자주 요청

→ 자연스러움

2.3 왜 INNER 도 자주

왜 INNER 도 자주:

  - 통계/집계 (확실한 매칭)
  - 카운트 (양쪽 매칭만)
  - 필터링 효과
  - 명확한 의도

→ 분석 쿼리에서

2.4 FULL OUTER 드문 이유

FULL OUTER 드문 이유:

  - 정합성 검증 외 사용 X
  - MySQL 미지원 (귀찮)
  - LEFT 로 대부분 해결
  - 행 수 폭증 위험

2.5 RIGHT 거의 안 쓰는 이유

RIGHT 거의 안 쓰는 이유:

  - LEFT 로 동일 표현
  - 가독성 ↓
  - 사고 흐름 어색
  - 코드 리뷰에서 LEFT 권장

→ 일관성

2.6 ILIC 의 맥락

ILIC 의 JOIN 빈도

ILIC SQL 의 예상 분포:
  - LEFT JOIN: 70%
    (대시보드, 상세 조회)
  - INNER JOIN: 25%
    (통계, 카운트, 정상 매칭만)
  - 기타: 5%
    (정합성 검증, 차집합)

  RIGHT JOIN: 0%
    → 모두 LEFT 로 표현

→ 실무 통계와 일치

2.7 자기 점검 답변

실무에서 LEFT JOIN 이 가장 빈번한 이유는?

:
1. 빈도:

  • LEFT ~70%
  1. 이유:

    • 사고 흐름, 안전
  2. INNER:

    • 통계/집계
  3. RIGHT/FULL:

    • 드묾

3️⃣ INNER JOIN 의 유리

3.1 INNER 가 유리한 경우

INNER 가 유리한 경우:

  1. 통계 / 집계
  2. 카운트
  3. 정상 매칭만 필요
  4. 의미상 양쪽 다 있어야

3.2 통계 예시

-- 부서별 직원 수 (정상 매칭만)
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 → 제외)

3.3 카운트

-- 활성 고객의 배송 수
SELECT COUNT(*)
FROM shipments s
INNER JOIN customers c ON s.customer_id = c.id
WHERE c.active = TRUE;
-- 매칭된 행만 카운트

3.4 성능 이점

성능 이점 (INNER):

  - 옵티마이저 친화적
  - 행 수 ↓ (필터 효과)
  - 인덱스 활용 좋음
  - 가장 빠름 일반적

→ 가능하면 INNER

3.5 의도 명확

의도 명확:

  INNER JOIN:
    - "양쪽 매칭만"
    - 코드 읽는 사람도 이해
    - 명확한 시그널

→ 가독성

3.6 ILIC 의 맥락

-- 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';

3.7 자기 점검 답변

INNER JOIN 은 언제 유리한가?

:
1. 언제:

  • 통계/카운트/정상
  1. 이유:

    • 명확한 의도
  2. 성능:

    • 가장 빠름 일반적
  3. 활용:

    • 분석 쿼리

4️⃣ LEFT JOIN 의 압도적 빈번

4.1 LEFT 가 빈번한 시나리오

LEFT 시나리오:

  1. 대시보드
  2. 상세 조회
  3. 리포트
  4. 누락 데이터 식별
  5. NULL 허용 컬럼 조회

4.2 대시보드

-- 대시보드 (모든 배송 + 정보)
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
-- 모든 배송 보여줌

4.3 상세 조회

-- 한 배송의 모든 정보
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 이어도 배송 보임

4.4 NULL 처리 (COALESCE/IFNULL)

-- NULL → 기본값
COALESCE(c.name, '미등록')   -- 표준
IFNULL(c.name, '미등록')     -- MySQL
NVL(c.name, '미등록')        -- Oracle

-- 여러 단계
COALESCE(c.name, b.name, '미등록')
-- 첫 NULL 아닌 값

4.5 1:N 관계

-- 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 인 고객도)

4.6 ILIC 의 맥락

-- 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 의 표준 대시보드 패턴
-- 모든 활성 배송 + 부가 정보 (누락 없음)

4.7 자기 점검 답변

LEFT JOIN 의 압도적 빈번은?

:
1. 시나리오:

  • 대시보드/상세/리포트
  1. NULL 처리:

    • COALESCE
  2. 1:N:

    • 모든 부모
  3. 종합:

    • 5+ 테이블 LEFT

5️⃣ RIGHT JOIN 거의 안 쓰는 이유

5.1 LEFT 로 동일 표현

-- 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 가 가독성 ↑

5.2 가독성 ↓

가독성 ↓:

  RIGHT JOIN 의 문제:
    - FROM 절 (왼쪽) 이 기준 아님
    - 기준이 오른쪽 (헷갈림)
    - 다중 JOIN 에서 더 복잡

  LEFT JOIN:
    - FROM 절이 기준
    - 왼쪽으로 흐름
    - 직관적

5.3 다중 JOIN 에서 혼란

-- 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 ...;

5.4 코드 리뷰 표준

코드 리뷰 표준:

  팀 룰:
    - "RIGHT JOIN 사용 X"
    - 모두 LEFT 로 변환
    - 일관성

→ 거의 모든 팀

5.5 변환 방법

-- 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 절 그대로

5.6 ILIC 의 맥락

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 로 변환" 룰

5.7 자기 점검 답변

RIGHT JOIN 거의 안 쓰는 이유는?

:
1. LEFT 로 표현:

  • 동치
  1. 가독성:

  2. 다중 JOIN:

    • 혼란
  3. 표준:

    • LEFT 만

6️⃣ 차집합 패턴

6.1 차집합 정의

차집합:

  "한쪽에는 있지만 다른 쪽에는 없는 행":
    - LEFT - 매칭 = 왼쪽만
    - RIGHT - 매칭 = 오른쪽만

→ LEFT JOIN + WHERE IS NULL

6.2 부서 없는 직원

-- 부서 없는 직원
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 인 직원

6.3 직원 없는 부서

-- 직원 없는 부서 (반대)
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)

6.4 NOT EXISTS 대안

-- 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 가 약간 유리

6.5 NOT IN 비교

-- 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 가 안전

6.6 ILIC 의 맥락

-- 차집합 활용 (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 테이블에 유리

6.7 자기 점검 답변

부서 없는 직원 / 직원 없는 부서 찾기 쿼리는?

:
1. 부서 없는 직원:

  • LEFT JOIN + IS NULL
  1. 직원 없는 부서:

    • 기준 바꿈
  2. NOT EXISTS:

    • 대안
  3. NOT IN 주의:

    • NULL 위험

7️⃣ JOIN 성능과 인덱스

7.1 JOIN 알고리즘

JOIN 알고리즘 (DB 내부):

  1. Nested Loop Join:
     - 외부 테이블 × 내부 테이블
     - 작은 테이블 + 인덱스에 유리

  2. Hash Join:
     - 한쪽으로 해시 테이블
     - 큰 테이블 + 동등 비교

  3. Sort-Merge Join:
     - 정렬 후 병합
     - 정렬된 데이터 / 범위

→ 옵티마이저가 선택

7.2 FK 인덱스 중요

FK 인덱스 중요:

  ON s.customer_id = c.id

  인덱스 없으면:
    - shipments 의 customer_id 풀 스캔
    - 또는 customers 풀 스캔
    - N × M 비용

  인덱스 있으면:
    - O(N × log M) (B-Tree)
    - 또는 더 빠름
    - 10-100배 차이

→ FK 컬럼은 무조건 인덱스

7.3 PK 인덱스 자동

PK 인덱스 자동:

  PRIMARY KEY 자동 인덱스:
    - customers.id
    - 빠른 조회

  FK 는 자동 X (MySQL):
    - 명시적 인덱스 필요
    - InnoDB 는 일부 자동 (확인 필요)

→ FK 인덱스 만들기

7.4 EXPLAIN 활용

-- 쿼리 실행 계획 확인
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 이면 → 인덱스 추가 검토

7.5 조인 순서

조인 순서 (옵티마이저):

  1. 작은 테이블 먼저
  2. 선택도 높은 조건 먼저
  3. 인덱스 활용 가능 우선

  STRAIGHT_JOIN 으로 강제 가능 (MySQL):
    SELECT /*+ STRAIGHT_JOIN */ ...

→ 보통 옵티마이저에 맡김

7.6 5+ JOIN 주의

5+ JOIN 주의:

  JOIN 개수 증가:
    - 옵티마이저 선택 ↑
    - 계산 비용 ↑
    - 실행 계획 불안정

  대안:
    - 쿼리 분할
    - 미리 계산
    - 캐시
    - 비정규화

7.7 ILIC 의 맥락

-- 성능 최적화 (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

7.8 자기 점검 답변

JOIN 성능과 인덱스 의 관계는?

:
1. 알고리즘:

  • Nested/Hash/Sort-Merge
  1. FK 인덱스:

    • 필수
  2. EXPLAIN:

    • 활용
  3. 5+ JOIN:

    • 주의

8️⃣ JOIN 안티 패턴 4가지

8.1 안티 패턴 1 — 의도치 않은 카르테시안

-- ❌ 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;

8.2 안티 패턴 2 — N+1 문제

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;

8.3 안티 패턴 3 — 과도한 JOIN

-- ❌ 너무 많은 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) 또는 미리 계산

8.4 안티 패턴 4 — 무분별한 fetch join (JPA)

// ❌ 모든 연관관계 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();

8.5 안티 패턴 5 — SELECT * 남용

-- ❌ 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;

8.6 ILIC 의 맥락

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 했나?

8.7 자기 점검 답변

JOIN 안티 패턴 4가지는?

:
1. 카르테시안:

  • 조건 누락
  1. N+1:

    • 반복 쿼리
  2. 과도한 JOIN:

    • 5+
  3. 무분별 fetch:

    • EAGER 남용

9️⃣ Phase 1 완주 + JPA 연결

9.1 Phase 1 학습 종합

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 선택 가이드 ★ ← 여기
  - 시나리오/실무/안티 패턴

9.2 핵심 메시지

Phase 1 핵심 메시지:

  "JOIN 은 정규화의 필연적 결과이며
   '결과 행의 정의' 가 JOIN 종류를 결정한다.
   실무는 LEFT 위주, FK 인덱스가 성능 핵심,
   안티 패턴 회피가 곧 좋은 SQL."

9.3 6주차 ↔ 7주차 연결

6주차 ↔ 7주차 연결:

6주차 (DB 접근):
  - JDBC / JdbcTemplate
  - 트랜잭션 ACID
  - DataSource

7주차 Phase 1 (JOIN):
  - SQL 의 본질
  - 정규화 + 합치기

7주차 Phase 2-4 (ORM/JPA):
  - JOIN 을 객체 관계로
  - JPA 가 SQL 자동 생성
  - @ManyToOne 등

9.4 JPA 와 JOIN 의 관계

JPA 와 JOIN 의 관계:

  JPA 는 객체 그래프:
    - Customer.shipments (List)
    - Shipment.customer (참조)

  JPA 가 자동 SQL:
    - JOIN SQL 생성
    - LEFT JOIN / INNER JOIN
    - 개발자는 객체로 표현

  하지만:
    - JOIN 동작 알아야
    - JPA SQL 확인 가능
    - N+1 같은 문제 진단

→ Phase 1 = JPA 의 기초

9.5 다음 Phase 예고

Phase 1 → Phase 2:

  Phase 2 — ORM 패러다임:
    Unit 2.1 — 객체-관계 미스매치
    Unit 2.2 — ORM 의 정의와 효과

  주제:
    - 객체 vs 관계 모델 차이
    - ORM 의 필요성
    - Hibernate, JPA 등

→ "왜 ORM 이 필요한가" 답

9.6 면접 단골 질문 매핑

Q핵심 답변
JOIN 선택 기준?결과 행 정의
LEFT 가 빈번?사고 흐름
INNER 유리?통계/집계
RIGHT 안 쓰는?LEFT 표현
차집합?LEFT + IS NULL
성능 핵심?FK 인덱스
EXPLAIN?실행 계획
N+1?JPA 문제
안티 패턴?4가지
Phase 1 → 2?JPA 로

9.7 추가 심화 질문

Q1: Lateral JOIN?

답:

  • 동적 서브쿼리 JOIN
  • 각 행에 대해 JOIN
  • PostgreSQL, MySQL 8.0+
  • 복잡 쿼리에 강력

Q2: 옵티마이저 힌트?

답:

  • /+ INDEX(...) /
  • /+ USE_HASH(...) /
  • MySQL: STRAIGHT_JOIN
  • 옵티마이저 무시 시 사용 (주의)

Q3: JOIN 과 서브쿼리?

답:

  • JOIN: 합치기
  • 서브쿼리: 중첩 쿼리
  • 옵티마이저가 같게 변환 (보통)
  • 가독성 따라 선택

Q4: 인덱스 카디널리티?

답:

  • 컬럼의 고유값 비율
  • 높으면 인덱스 효과 ↑
  • 성별 같은 낮은 카디널리티는 인덱스 X
  • FK 는 보통 좋음

Q5: SQL 튜닝의 첫 단계?

답:

  • EXPLAIN
  • 인덱스 확인
  • WHERE 조건 인덱스
  • JOIN 순서
  • 옵티마이저 통계 (ANALYZE)

🎯 핵심 요약 — 3줄 정리

1. JOIN 선택 = "결과 행 정의"

  • 양쪽 매칭만 → INNER, 왼쪽 기준 + 부가 → LEFT
  • 양쪽 모두 → FULL OUTER, 차집합 → LEFT + IS NULL
  • 의도 명확 = 선택 명확

2. 실무 빈도와 성능

  • LEFT 70% > INNER 25% > FULL/RIGHT 5%
  • RIGHT 거의 안 씀 (LEFT 로 변환)
  • FK 인덱스가 성능 핵심, EXPLAIN 으로 확인

3. 안티 패턴 4가지 회피

  • (1) 의도치 않은 카르테시안 (조건 누락)
  • (2) N+1 문제 (JPA fetch)
  • (3) 5+ 과도한 JOIN
  • (4) 무분별한 EAGER fetch
  • → 좋은 SQL 의 기본

🏆 Phase 1 완주 — SQL 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 선택 가이드 ★깊이 ← 여기, Phase 1 완주

→ JOIN 4종 완전 정복
→ 실무 활용 + 안티 패턴 회피
→ JPA 학습의 기반

📚 다음으로...

Phase 2 — ORM 패러다임

🔄 Phase 2 — ORM 패러다임
  Unit 2.1 — 객체-관계 미스매치
  Unit 2.2 — ORM 의 정의와 효과

Phase 2 주제:

  • 객체와 관계형 DB 의 5가지 미스매치
  • ORM 이 어떻게 매핑을 자동화
  • Hibernate / JPA / SQLAlchemy 등
  • ORM 의 한계 (SQL 완전 대체 X)

7주차 누적 진행

🗂️ 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 학습의 정점

profile
Software Developer

0개의 댓글