Skip to content

📌 『튜너의 1년』 독자는 먼저 ../README.md(고객사 카드)를 본다. 이 문서는 컨테이너를 하나만 쓰던 시절에 썼다. 컨테이너 oracle-free는 모두 ora-dist로 바꿔 읽는다. 그 밖에는 다를 게 없다. data/data_csv.zip을 같은 폴더에 압축 해제한 뒤 3단계부터 진행한다. 쓰는 화: 03 · 09.

『오라클 SQL 파워업』 실습 데이터셋 — 설치부터 실습까지 (전용 계정 pup) ​

이 폴더 하나만 보고 누구나 따라 할 수 있도록 만든 안내서다. Oracle이나 Docker를 처음 다뤄도 위에서부터 차례로 복사해 붙여 넣으면 된다. 도메인: 유통/리테일(상품 · 매장 · 판매 · 발주 · 입출고 · 재고), 테이블 18개. 대용량 테이블인 판매(CF10, 약 50만 건)와 판매상품(CF11, 약 110만 건)으로 조인, 인덱스, 집계 튜닝을 실습한다. 데이터 출처: 『오라클 SQL 파워업』(유일환) 실습 데이터(CSV). 책의 연습 쿼리 167개도 들어 있다(03_연습쿼리_해설.sql, 쿼리마다 위에 설명과 의도 주석을 달았다). 🗺 스키마 전체 그림(ERD·조감도·인덱스): 데이터_조감도_ERD_인덱스.md


0. 이 폴더 구성 ​

오라클SQL파워업/
├── README.md                 ← (지금 이 문서) 전체 안내·실행 순서
├── 01_pup_계정생성.sql        ← [1단계] 전용 계정 pup 만들기 (sys로 실행)
├── 02_테이블생성.sql          ← [2단계] 18개 테이블(CF01~CF18) 만들기 (pup로 실행)
├── 03_연습쿼리_해설.sql        ← [실습] 책 연습쿼리 167개 + 쿼리별 설명·의도 주석 (pup로 골라 실행)
├── 참고_데이터모델_개념.md     ← 18테이블 의미·배경 설명(읽을거리)
├── data/                     ← [3·4단계] 적재용 데이터 (한 폴더에 모음)
│   ├── CF01.csv ~ CF18.csv     · 실제 데이터(CP949 인코딩)
│   ├── CF01.ctl ~ CF18.ctl     · SQL*Loader 적재 설정(인코딩/줄바꿈 자동 처리)
│   └── load_all.sh             · 18개 한 번에 적재하는 스크립트
└── _원본백업/                 ← 받은 원본 zip 보관(손대지 않아도 됨)

핵심 흐름 한 줄: pup 계정 생성(1) → 빈 테이블 18개 생성(2) → data 폴더를 DB 컨테이너로 복사(3) → SQL\*Loader로 CSV 적재(4) → 통계·검증(5) → 실습(6).


1. 미리 준비할 것 (전제) ​

  1. Docker Desktop이 실행 중이고 Oracle 컨테이너 oracle-free가 떠 있어야 한다.
    • 아직 없으면 00. Oracle 실습 환경 구축가이드의 옵션 B로 컨테이너부터 만든다.
    • 확인:
      bash
      docker ps --filter name=oracle-free
      STATUS가 Up ... (healthy)면 된다. 멈춰 있으면 docker start oracle-free로 띄운다.
  2. 접속 정보(이 문서 전체에서 사용):
    항목값
    컨테이너 이름oracle-free
    관리자(SYS)sys / oracle
    DB 서비스(PDB)FREEPDB1
    실습 계정(이 데이터셋)pup / pup (1단계에서 생성)
  3. (선택) GUI로 쿼리하고 싶으면 SQL Developer나 SQLcl을 쓴다. 없어도 아래 명령줄(CLI)만으로 끝까지 할 수 있다.

💡 왜 CSV를 SQL*Loader로 넣나? 데이터가 INSERT문이 아니라 CSV 파일이라서 오라클 전용 고속 적재기인 SQL*Loader(sqlldr)로 넣는다. sqlldr는 컨테이너 안에 있으므로 데이터 파일을 컨테이너 안으로 복사한 뒤 컨테이너 안에서 실행한다.


2. 1단계 — 전용 계정 pup 만들기 (SYS로) ​

명령 프롬프트(PowerShell/터미널)에서 호스트의 파일을 컨테이너의 sqlplus에 그대로 흘려보낸다.

bash
docker exec -i oracle-free sqlplus sys/oracle as sysdba < "01_pup_계정생성.sql"
  • 이 스크립트가 하는 일: FREEPDB1로 전환 → pup/pup 계정 생성 → 기본 권한 + 튜닝 측정 권한(SELECT_CATALOG_ROLE 등, 실행계획/AUTOTRACE/V$ 조회용) 부여.
  • 정상이면 User created., Grant succeeded.가 보인다.
  • (대안) SQL Developer에서 sys로 접속 → 01_pup_계정생성.sql 열기 → 전체 실행.

위 명령은 이 폴더 안에서 실행해야 한다(파일명을 상대 경로로 쓰기 때문이다). 폴더는 다음처럼 이동한다. cd "<이 README가 있는 폴더>"


3. 2단계 — 빈 테이블 18개 만들기 (pup로) ​

bash
docker exec -i oracle-free sqlplus pup/pup@localhost:1521/FREEPDB1 < "02_테이블생성.sql"
  • CF01(상품)~CF18(일별재고) 18개 테이블과 각 PK를 만든다. 보조 인덱스, 외래키, 뷰는 일부러 만들지 않았다. 인덱스는 실습에서 직접 추가한다.
  • 확인: PowerShell에서는 <<EOF(heredoc)를 쓸 수 없으므로 pup로 접속해 직접 조회한다.
    powershell
    docker exec -it oracle-free sqlplus pup/pup@localhost:1521/FREEPDB1
    접속되어 SQL>가 뜨면 아래를 붙여 넣는다.
    sql
    SELECT table_name FROM user_tables ORDER BY 1;   -- CF01 ~ CF18 (18행)이면 성공
    EXIT
  • (대안) SQL Developer에서 pup로 접속 → 02_테이블생성.sql 열기 → 전체 실행(F5).

📌 이 문서의 명령은 모두 PowerShell 기준이다. 파일 실행은 2단계처럼 ... < "파일.sql"로 하면 된다. 다만 여러 줄 SQL을 즉석에서 넣을 때는 <<EOF 대신 ① 접속 후 붙여 넣기나 ② @" ... "@ | docker exec -i ...(PowerShell here-string)를 쓴다(아래 단계에 예시가 있다).


4. 3단계 — 데이터 파일을 컨테이너 안으로 복사 ​

CSV와 ctl, load_all.sh는 지금 내 PC(호스트)의 data/ 폴더에 있다. 하지만 적재 도구 sqlldr는 DB 컨테이너 안에 있으므로 데이터를 컨테이너로 옮긴 뒤 거기서 실행한다. 그래서 복사 → 권한 → 적재 순서로 진행한다.

4-1. (선택) 복사 전 호스트 파일 개수 확인(PowerShell):

powershell
# CSV 18 + ctl 18 + load_all.sh = 37 이면 정상
(Get-ChildItem "<이 README가 있는 폴더>\data").Count

4-2. 컨테이너로 복사(경로는 내 PC의 실제 위치로 바꾼다):

powershell
docker cp "<이 README가 있는 폴더>\data" oracle-free:/opt/oracle/pup_data
  • 호스트 data 폴더의 내용물이 컨테이너의 /opt/oracle/pup_data/로 들어간다.
  • 성공하면 Successfully copied ...가 뜬다.
  • ⚠️ 경로 주의: 우리가 쓰는 gvenzl 이미지의 oracle 홈은 /opt/oracle 경로다. /home/oracle이 아니다. 그 경로로 복사하면 Could not find the file /home/oracle 에러가 난다. 다른 이미지를 쓴다면 docker exec oracle-free bash -lc 'echo $HOME'으로 홈을 확인해 그 아래에 복사한다.

4-3. 잘 들어갔는지 확인:

powershell
docker exec oracle-free ls /opt/oracle/pup_data

CF01.csv CF01.ctl ... CF18.csv CF18.ctl load_all.sh가 보이면 된다.

4-4. ★ 소유권 변경(빠뜨리면 5단계 적재가 실패한다):

powershell
docker exec -u 0 oracle-free chown -R oracle:oinstall /opt/oracle/pup_data
  • 왜 필요한가: docker cp로 들어온 파일은 root 소유다. 그런데 sqlldr는 DB의 oracle 계정으로 동작해서 root 파일을 읽지 못하고 SQL*Loader-704 / KUP-04... 오류를 낸다. 소유권을 oracle로 바꿔야 읽을 수 있다.
  • 확인:
    powershell
    docker exec oracle-free ls -l /opt/oracle/pup_data
    각 줄의 소유자가 oracle oinstall이면 된다.

흔한 문제

  • no such file or directory(복사 원본 경로 오타): 경로를 내 PC의 실제 위치로 고친다. 현재 위치는 PowerShell에서 pwd(=Get-Location)로 확인한다.
  • 예전에 한 번 복사해서 /opt/oracle/pup_data가 이미 있고 그 안에 data/ 폴더가 또 생겼다면, 먼저 비우고 다시 복사한다. docker exec -u 0 oracle-free rm -rf /opt/oracle/pup_data를 실행한 뒤 4-2부터 다시 한다.

5. 4단계 — 데이터 적재 (SQL*Loader 실행) ​

5-1. 컨테이너 안으로 들어가서 폴더로 이동:

powershell
docker exec -it oracle-free bash

프롬프트가 bash-4.4$나 [oracle@xxxx pup_data]$처럼 바뀐다. 지금부터는 컨테이너 안의 리눅스 셸이다(PowerShell이 아니다). 이어서 폴더로 이동한다.

bash
cd /opt/oracle/pup_data

5-2. 일괄 적재 실행: 인자 pup은 pup 계정의 비밀번호다(1단계에서 정한 값).

bash
sh load_all.sh pup
  • CF01→CF18 순서로 sqlldr가 18번 자동으로 실행된다. 비밀번호를 인자로 줬으니 따로 묻지 않는다.

5-3. 정상 출력 예시: 테이블마다 이렇게 지나간다.

================= loading CF01 =================
Path used:      Direct
  Table CF01:
   1098 Rows successfully loaded.
...
================= loading CF11 =================        ← 가장 큼(약 110만), 수십 초~1분+
  Table CF11:
   1108080 Rows successfully loaded.
...
=== 완료. 상세는 각 CFxx.log, 거부행은 CFxx.bad 확인 ===

핵심은 테이블마다 나오는 NNN Rows successfully loaded. 줄이다. 숫자가 0이 아니면 적재된 것이다.

5-4. 적재 결과 한 번에 점검(아직 컨테이너 셸 안에서):

bash
grep -E "successfully loaded|not loaded" CF*.log
  • 모든 줄이 ... Rows successfully loaded.이고 0 Rows not loaded이면 완벽하다.
  • 거부된 행이 있으면(예: ... not loaded) 그 테이블의 로그에서 사유를 본다(tail -30 CF11.log). 거부된 원본 줄은 CF11.bad에 있다.

5-5. 컨테이너 셸에서 나오기:

bash
exit

다시 PowerShell 프롬프트(PS C:\...>)로 돌아온다.

⏱ 소요 시간: 작은 테이블은 순식간에 끝나고, CF10(50만)과 CF11(110만), CF15는 수십 초 걸린다. 전체는 1~2분이다. DIRECT=TRUE(직접 경로 적재)라서 일반 INSERT보다 훨씬 빠르다.

흔한 에러

메시지원인 / 해결
SQL*Loader-704 ... KUP-04...4-4 chown 누락 → exit 후 4-4 실행, 다시 5단계
KUP-04063: unable to open file위치/파일 문제. pwd가 /opt/oracle/pup_data인지, ls로 csv/ctl이 있는지 확인
ORA-01017 invalid username/passwordpup 비번 틀림 → sh load_all.sh <진짜비번>
특정 표만 0 Rows/다수 .bad그 테이블 CSV의 인코딩·구분자 문제. CFxx.log에서 사유 확인

💡 ctl이 자동으로 처리하는 것(그냥 INSERT가 아니라 sqlldr를 쓰는 이유): CSV는 한글 CP949(CHARACTERSET KO16MSWIN949), 윈도우 CRLF 줄바꿈(STR x'0d0a'로 줄 끝 \r 제거), 첫 줄이 헤더(SKIP=1)이고, 값이 "로 감싸여 있다(OPTIONALLY ENCLOSED BY '"'). 이 처리가 없으면 한글이 깨지거나 날짜 컬럼 적재가 실패한다. 🔁 다시 적재(중복 PK 에러 방지): pup로 TRUNCATE TABLE CFxx;를 실행한 뒤 5단계를 다시 한다. 또는 11장대로 전체를 다시 설치한다.


6. 5단계 — 통계 수집 + 행수 검증 (필수) ​

적재가 끝나면 반드시 통계를 수집한다. 원본에는 통계가 없어서, 수집하지 않으면 옵티마이저가 행수를 모르는 채로 엉뚱한 실행계획을 세운다.

6-1. 통계 수집 + 행수 보기: 호스트(PowerShell)에서 pup로 접속해 실행한다. 둘 중 편한 방법을 고른다.

(a) 대화형(가장 쉬움): 접속한 뒤 붙여 넣는다.

powershell
docker exec -it oracle-free sqlplus pup/pup@localhost:1521/FREEPDB1

SQL>가 뜨면 다음을 붙여 넣는다.

sql
EXEC DBMS_STATS.GATHER_SCHEMA_STATS('PUP', degree=>4);
SET PAGESIZE 50
COL table_name FORMAT A24
SELECT table_name, num_rows FROM user_tables ORDER BY num_rows DESC NULLS LAST;
EXIT

(b) PowerShell here-string으로 한 번에(파일 없이):

powershell
@"
EXEC DBMS_STATS.GATHER_SCHEMA_STATS('PUP', degree=>4);
SET PAGESIZE 50
SELECT table_name, num_rows FROM user_tables ORDER BY num_rows DESC NULLS LAST;
EXIT
"@ | docker exec -i oracle-free sqlplus -s pup/pup@localhost:1521/FREEPDB1

@"와 닫는 "@는 각각 줄 맨 앞에 와야 한다(PowerShell here-string 규칙).

6-2. 기대 행수(대략이며, 생성 옵션에 따라 조금씩 다르다):

테이블의미대략 행수
CF11판매상품~110만
CF10판매~50만
CF15입출고상품수만
CF13발주상품수천~수만
CF18일별상품재고수천~수만
CF01~CF09, CF16, CF17상품·카테고리·거래처·매장·사원·코드 등 마스터수~수백
CF11과 CF10이 압도적으로 크면 정상이다. 특정 테이블만 비어 있으면 5-4의 로그를 다시 확인한다.

7. 6단계 — 접속해서 첫 쿼리 + 측정 켜기 ​

7-1. 접속:

  • CLI: docker exec -it oracle-free sqlplus pup/pup@localhost:1521/FREEPDB1
  • SQL Developer: 새 접속 PUP_local (Host localhost, Port 1521, Service FREEPDB1, 사용자 pup/pup). 실행계획은 F6(Autotrace)으로 본다.

7-2. 데이터 확인: 상품 × 카테고리 조인

sql
SELECT i.ITM_NO, i.ITM_NM, c.CTG_NM
FROM   CF01 i JOIN CF02 c ON c.CTG_NO = i.CTG_NO
WHERE  ROWNUM <= 10;

7-3. 측정 켜기: 1단계에서 pup에 측정 권한(SELECT_CATALOG_ROLE 등)을 줬으므로 바로 된다.

sql
SET TIMING ON
SET AUTOTRACE ON              -- 실행계획 + consistent gets(논리 블록 읽기) 표시
SELECT COUNT(*) FROM CF11;    -- 110만 풀스캔의 비용을 눈으로
SET AUTOTRACE OFF

User has no SELECT privilege on V$...가 뜨면 1단계 권한이 들어가지 않은 것이다. 01_pup_계정생성.sql을 다시 실행한 뒤 다시 접속한다(자세한 내용은 00 구축가이드 B-4).

➡ 이제 준비가 끝났다. 9장 실습 메뉴(대용량 조인 NL vs HASH, 인덱스 손익, 형변환 함정, 계층 질의)로 넘어간다.


8. 데이터 모델 한눈에 (18테이블) ​

테이블의미PK규모관계(주요 연결)
CF01상품ITM_NO소CTG_NO→CF02, PTNR_NO→CF06
CF02카테고리CTG_NO소UP_CTG_NO→CF02(계층)
CF03상품원재료ITM_NO,MTRL_ITM_NO소ITM_NO→CF01
CF04상품옵션ITM_NO,OPT_ITM_NO소ITM_NO→CF01
CF05상품가격(적용일자별)ITM_NO,UPRC_KN_CD,APL_DT소ITM_NO→CF01
CF06거래처PTNR_NO소
CF07매장SHP_NO소
CF08부서DEPT_NO소UP_DEPT_NO→CF08(계층)
CF09사원EMP_NO소DEPT_NO→CF08
CF10판매(헤더)SLE_NO~50만SHP_NO→CF07
CF11판매상품(상세)SLE_NO,ITM_NO~110만SLE_NO→CF10, ITM_NO→CF01
CF12발주ORD_NO중PTNR_NO→CF06
CF13발주상품ORD_NO,ITM_NO중ORD_NO→CF12
CF14입출고INOUT_NO중ORD_NO→CF12
CF15입출고상품INOUT_NO,ITM_NO중INOUT_NO→CF14
CF16공통코드CLS_CD,CD소
CF17재고집계(일자)INV_BS_DT소
CF18일별상품재고집계SHP_NO,ITM_NO,INV_BS_DT중SHP_NO→CF07, ITM_NO→CF01

자세한 컬럼 의미는 참고_데이터모델_개념.md에 있다. 날짜성 컬럼 SLE_DT/ORD_DT/APL_DT/INV_BS_DT/INOUT_DT는 문자형 VARCHAR2(8)에 'YYYYMMDD' 형식으로 들어 있다(진짜 DATE는 REG_DTT/MOD_DTT뿐이다).


9. ★ 실습 메뉴 (대용량 CF10/CF11로 직접 측정) ​

공통: SET AUTOTRACE ON 또는 /*+ GATHER_PLAN_STATISTICS */ + SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT=>'ALLSTATS LAST')). 인덱스나 조인을 바꾸기 전과 후의 consistent gets(Buffers)를 비교해야 튜닝 효과를 입증할 수 있다.

A. 대용량 조인 — NL vs HASH ​

sql
-- 판매(50만) ⋈ 판매상품(110만) → 일자별 매출. 대용량이라 해시 조인이 정석.
SET AUTOTRACE ON
SELECT /*+ USE_HASH(s d) */ s.SLE_DT, SUM(d.SLE_QTY * d.SLE_UPRC) AS amt
FROM   CF10 s JOIN CF11 d ON d.SLE_NO = s.SLE_NO
WHERE  s.SLE_DT BETWEEN '20230101' AND '20230131'
GROUP  BY s.SLE_DT ORDER BY s.SLE_DT;
-- 비교: /*+ USE_NL(s d) */ 로 바꿔 재실행 → consistent gets 폭증(대용량 NL의 함정) 관찰.

연계 학습: 딥다이브 강의5 조인 (공개 사이트에는 없음) · Hero Lab08 (공개 사이트에는 없음)

B. 인덱스 손익 — 상품별 판매 조회 (CF11 PK 선두가 아님) ​

sql
-- CF11 PK = (SLE_NO, ITM_NO). ITM_NO '단독' 조건은 PK 선두가 아니라 비효율(FULL/SKIP).
SET AUTOTRACE ON
SELECT COUNT(*), SUM(SLE_QTY) FROM CF11 WHERE ITM_NO = 1098;        -- [전]

CREATE INDEX CF11_X_ITM ON CF11(ITM_NO);                            -- 보조 인덱스 직접 생성
EXEC DBMS_STATS.GATHER_TABLE_STATS('PUP','CF11');

SELECT COUNT(*), SUM(SLE_QTY) FROM CF11 WHERE ITM_NO = 1098;        -- [후] INDEX RANGE SCAN
-- 관찰: consistent gets 수천 → 수십. 끝나면  DROP INDEX CF11_X_ITM;  로 원복 가능.

연계 학습: 딥다이브 강의4 인덱스 튜닝 (공개 사이트에는 없음)

C. 날짜 문자열 범위 & 형변환 함정 ​

sql
SELECT COUNT(*) FROM CF10 WHERE SLE_DT BETWEEN '20230601' AND '20230630';   -- 문자 비교(정상)
-- 함정: WHERE TO_NUMBER(SLE_DT) = ... 처럼 컬럼을 가공하면 인덱스를 못 탑니다(컬럼 가공 금지 원칙).
-- (CREATE INDEX CF10_X_DT ON CF10(SLE_DT); 후 위 범위조회 전후 비교)

D. 계층 질의 (카테고리/부서 self join) ​

sql
SELECT LPAD(' ', 2*(LEVEL-1)) || CTG_NM AS tree, CTG_NO, UP_CTG_NO
FROM   CF02
START WITH UP_CTG_NO IS NULL
CONNECT BY PRIOR CTG_NO = UP_CTG_NO;

+ 책 연습쿼리 167개 ​

03_연습쿼리_해설.sql에 책의 연습 쿼리 167개가 들어 있고, 쿼리마다 위에 [쿼리 N] 설명 / 의도 / 대상 주석을 달았다. pup으로 골라 실행하면서 위의 측정 도구로 실행계획과 Buffers를 확인한다.

+ 시험준비 8주차 커리큘럼 연계 (PUP 평행 실습) ​

시험준비 각 주차 폴더에 같은 PUP(유통) 데이터로 평행 실습하는 파일(0N_실습_PUP유통.sql)을 두었다. 주차 학습을 마친 뒤 이 파일로 유통 도메인에 적용해 보자(기존 0N_실습.sql은 스키마 A/B용이고, 이 파일은 PUP용이다).

주차주제PUP 실습 파일
01데이터모델링·SQL기본01_실습_PUP유통.sql (공개 사이트에는 없음)
02SQL활용 심화02_실습_PUP유통.sql (공개 사이트에는 없음)
03수행구조·분석도구03_실습_PUP유통.sql (공개 사이트에는 없음)
04인덱스 튜닝04_실습_PUP유통.sql (공개 사이트에는 없음)
05조인 튜닝05_실습_PUP유통.sql (공개 사이트에는 없음)
06옵티마이저·고급튜닝06_실습_PUP유통.sql (공개 사이트에는 없음)
07Lock·트랜잭션·실기07_실습_PUP유통.sql (공개 사이트에는 없음)
08총정리·모의고사08_실습_PUP유통.sql (공개 사이트에는 없음)

10. 트러블슈팅 ​

증상원인 / 해결
docker: ... cannot connectDocker Desktop이 안 떠 있음 → Docker Desktop 실행 후 docker start oracle-free.
2단계 ORA-01017 invalid username/password1단계 pup 생성이 안 됨/비번 불일치 → 1단계 재실행, 또는 sys로 ALTER USER pup IDENTIFIED BY pup;.
4단계 SQL*Loader-704 / KUP-04... 권한3-(2) chown 누락 → docker exec -u 0 oracle-free chown -R oracle:oinstall /opt/oracle/pup_data 재실행.
한글이 ???/깨짐ctl의 CHARACTERSET KO16MSWIN949(이미 적용). 조회 클라이언트 인코딩도 UTF-8/MSWIN949로.
날짜 컬럼 ORA-01858/01861줄바꿈(CRLF) 문제 → ctl의 INFILE '...' "STR x'0d0a'"(이미 적용).
일부 행이 .bad로 빠짐CFxx.log에서 사유 확인(길이/형식).
적재 후 실행계획이 이상6단계 통계수집 누락 → GATHER_SCHEMA_STATS('PUP').
다시 적재하고 싶음(중복 PK)pup로 TRUNCATE TABLE CFxx; 후 다시 4단계, 또는 전체 재설치(아래).

11. 정리 / 다시 설치 ​

sql
-- (pup) 데이터만 비우기:   TRUNCATE TABLE CF01;  ... CF18;
bash
# (완전 삭제) sys로: 계정·테이블 통째 제거
docker exec -i oracle-free sqlplus sys/oracle as sysdba <<'EOF'
ALTER SESSION SET CONTAINER = FREEPDB1;
DROP USER pup CASCADE;
EXIT
EOF
# 이후 다시 1→5단계 반복하면 재설치됩니다. 원본 데이터는 _원본백업/ 에 zip으로 보관돼 있습니다.

요약: 1) 01_pup_계정생성.sql(sys) → 2) 02_테이블생성.sql(pup) → 3) docker cp data + chown → 4) load_all.sh → 5) 통계수집·검증 → 6) 실습. 막히면 10장 트러블슈팅을 본다.