Skip to content

📌 부록. 이 문서는 단일 컨테이너(oracle-free) 시절에 볼륨 없이 만든 환경을 데이터를 살린 채 옮기는 절차(§5)를 위해 보존한다. 새 PC에서 처음 만드는 사람에게는 이 문서가 필요 없다. 01_첫_고객사_ora-hanorder.md의 launch_ora가 처음부터 볼륨과 태그 고정, --shm-size를 갖추고 있기 때문이다. 이 문서에 나오는 L00 L03 L04 90_측정권한_5티어는 아직 작성하지 않은 계획 문서다. 해당 내용은 지금 05_Free와_EE_트랙G.md(가용성 정본·실측 점검)와 고객사/L1_ora-hanorder/00_측정권한_부여.sql(권한)에 있다.

L01 — 컨테이너 기동과 영속화 (트랙 L 기본 무대) ​

이 문서의 목표는 처음 보는 사람이 그대로 따라 해서 같은 결과에 도달하는 것이다. 각 단계에는 ① 무엇을 ② 어디서(호스트 셸 / 컨테이너 안 / SQL*Plus 안) ③ 복붙할 수 있는 명령 ④ 정상일 때 보이는 출력 ⑤ 실패 증상 → 원인 → 조치를 갖춘다. 기존 Oracle_실습환경_구축가이드.md를 대체하지 않고, 영속화, 사이징, 태그 고정, 트레이스 반출을 보강한다.

📌 이 문서의 모든 수치는 2026-09-01 실측값이다. 추정치가 아니다.


0. 이 문서를 먼저 읽어야 하는 이유 — 지금 상태는 위험하다 ​

실습 환경을 이미 만들어 둔 사람이라면 아래 명령으로 지금 바로 확인해야 한다.

bash
docker inspect oracle-free --format '{{json .HostConfig.Binds}}'
출력의미
null🔴 위험. 모든 데이터가 컨테이너 쓰기 레이어에 있다. docker rm 한 번이면 전부 사라진다
["oracle-data:/opt/oracle/oradata"]✅ 안전. 볼륨에 있다

실측 사례(이 저장소 작성자 환경, 2026-09-01): 결과는 null이었고, 컨테이너 쓰기 레이어가 17.4GB였다. 그 안에 DEV(ORD 100만 + ORD_ITEM 247만 + 파티션 5쌍), PUP(CF11 110만), SQLP(ORDERS 20만)가 들어 있었다. CSV 적재에만 몇 시간이 걸린 데이터다.

⚠️ 기동 중인 컨테이너에는 -v를 나중에 붙일 수 없다. 볼륨을 도입하려면 컨테이너를 다시 만들어야 하는데, 아무 준비 없이 다시 만들면 그 17.4GB가 사라진다. §5의 무손실 이관 절차를 반드시 따라야 한다.

0-1. ★ 디스크 계산부터 먼저 한다 (실측에서 실제로 걸렸다) ​

이관하는 동안 구 컨테이너와 신 컨테이너가 동시에 존재하는 구간이 있어서, 이때 순간 사용량이 가장 크다. 시작하기 전에 미리 계산해 둔다.

항목실측(이 저장소 환경)내 환경
구 컨테이너 쓰기 레이어17.4GBdocker ps -as
베이스 이미지2.9GBdocker images
① 스냅샷 이미지24.4GB구 컨테이너 + 베이스 정도
② expdp 덤프1~2GB (compression=ALL)데이터량의 5~10%
③ 신 컨테이너(impdp 후)≈ 17GB구 컨테이너와 비슷
동시 최대≈ 62GB

실측 사례: 스냅샷을 뜨자 여유 공간이 84GB → 48GB(91% 사용)로 떨어졌다. 그대로 이관을 시작했다면 신 컨테이너에 적재하던 중에 no space left on device로 멈췄을 것이다.

여유가 60GB 미만이면 다음 중 하나를 고른다.

방법절차위험
A. 덤프 우선 (권장)② expdp → 호스트로 docker cp → 덤프 무결성 확인 → ① 스냅샷 이미지 삭제(docker rmi) → ③ 신 컨테이너 → ④ impdp낮음. 호스트 덤프와 구 컨테이너가 동시에 살아 있다
B. 스냅샷만① 스냅샷 → 구 컨테이너 삭제 → ③ 신 컨테이너 → 스냅샷에서 데이터파일 복사중간. 선택 복구 불가
C. 미루기볼륨 도입을 뒤로 미루고 주기적인 expdp만 호스트에 보관낮지만 docker rm 사고에는 여전히 노출

💡 덤프는 반드시 호스트로 빼 둔다. 컨테이너 안에만 두면 컨테이너와 함께 사라지므로 백업의 의미가 없다.

bash
docker cp oracle-free:/opt/oracle/dump ./_backup_dump

1. 전제 조건 ​

항목요구확인 명령정상 출력 예
Docker Desktop설치 후 실행 중docker version --format '{{.Server.Version}}'28.x.x (숫자면 OK)
디스크 여유최소 40GB (이미지 2.9GB + 데이터 17GB + 스냅샷 17GB)df -h /c | tail -1475G 391G 84G 83% /c
메모리호스트 8GB+ 권장——
WSL2 (Windows)Docker Desktop 설치 시 자동 안내wsl -l -vdocker-desktop Running 2

실패 증상 → 조치

증상원인조치
failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngineDocker Desktop이 꺼져 있음시작 메뉴에서 Docker Desktop 실행 → 고래 아이콘이 안정될 때까지 대기(1~2분)
no space left on device디스크 부족docker system df로 확인 → docker image prune

2. ★ 이미지 태그 고정 — 23-slim을 그대로 쓰면 안 되는 이유 ​

기존 가이드는 gvenzl/oracle-free:23-slim을 쓴다. 이 태그는 이동(moving) 태그다.

실측 (2026-09-01): 같은 23-slim 태그인데 내용물은 이렇다.

BANNER_FULL : Oracle AI Database 26ai Free Release 23.26.2.0.0
ORACLE_HOME : /opt/oracle/product/26ai/dbhomeFree

태그는 23이지만 실체는 26ai다. 오라클이 2025-10에 제품 이름을 Oracle Database 23ai에서 Oracle AI Database 26ai로 바꾸면서 이미지도 따라 바뀌었다.

이게 왜 중요할까? 재현성 문제에서 그치지 않고 실습 동작이 바뀌기 때문이다.

파라미터19c ~ 23ai26ai (현재)영향
awr_pdb_autoflush_enabledFALSETRUEPDB 레벨 AWR 자동 스냅샷 여부. L03(미작성) ⓪단계의 분기가 달라진다
공식문서 경로/19/, /23//26//23/으로 가면 26ai 내용이 나온다

태그가 움직인다는 증거: 같은 머신에서 실측한 결과다.

gvenzl/oracle-free:23-slim        ID fbbd3023...   3 months ago   2.87GB   ← 3개월 전 받아 둔 캐시
gvenzl/oracle-free:23.26.2-slim   ID 6c6b2555...   30 hours ago   2.86GB   ← 방금 받은 것

ID가 다르다. 즉 3개월 전에 23-slim을 받은 사람과 오늘 받는 사람은 서로 다른 이미지를 쓴다. (다행히 둘 다 ORACLE_HOME=/opt/oracle/product/26ai/dbhomeFree = Oracle 23.26.2.0.0으로 같았지만, 그것은 운이었을 뿐 보장된 것이 아니다.) 함께 공부하는 사람끼리 실습 결과가 맞지 않으면 SQL보다 여기부터 의심해야 한다.

그래서 이렇게 한다. 패치 버전까지 적은 태그를 쓰고, 다이제스트를 기록한다.

bash
docker pull gvenzl/oracle-free:23.26.2-slim
docker image inspect gvenzl/oracle-free:23.26.2-slim --format '{{index .RepoDigests 0}}'

정상 출력 (2026-09-01 실측. 이 값이 이 저장소의 기준 환경이다):

gvenzl/oracle-free@sha256:6c6b2555e19b81beb79f3429ac34f241b196785c0137125b871f0af8507685b1

기록한 태그와 다이제스트, v$version 결과는 L04_옵션_실측_점검.sql(미작성: 실측 쿼리는 05_Free와_EE_트랙G.md §2) 실행 로그와 함께 남긴다. 태그를 올릴 때는 반드시 L04를 다시 돌린다.

💡 23.26.2-slim이 없다는 오류가 나면 docker search 대신 https://hub.docker.com/r/gvenzl/oracle-free/tags에서 지금 있는 패치 태그를 확인해 그 값으로 바꾼다. latest와 23-slim은 쓰지 않는다.


3. 새로 기동하기 (데이터가 아직 없는 경우) ​

기존 데이터가 있으면 §3을 건너뛰고 §5로 간다.

3-1. 컨테이너 기동 ​

어디서: 호스트 셸(PowerShell 또는 Git Bash)

bash
docker volume create oracle-data

docker run -d \
  --name oracle-free \
  -p 1521:1521 \
  --shm-size=2g \
  -v oracle-data:/opt/oracle/oradata \
  -e ORACLE_PASSWORD=oracle \
  -e APP_USER=sqlp \
  -e APP_USER_PASSWORD=sqlp \
  gvenzl/oracle-free:23.26.2-slim

각 옵션이 하는 일은 다음과 같다.

옵션왜 필요한가
-v oracle-data:/opt/oracle/oradata이 문서의 존재 이유. 데이터파일이 볼륨에 남으므로 docker rm 후에도 살아남는다
--shm-size=2g/dev/shm 크기. Docker 기본값은 64MB이고, 실측 환경도 64MB였다. 이대로 SGA를 키우면 ORA-00845: MEMORY_TARGET not supported가 난다
-p 1521:1521호스트에서 SQL Developer/SQLcl로 접속하기 위한 포트
-e APP_USER=sqlp실습 계정을 자동으로 만든다. 단, 기본 권한만 주어지므로 §4-3이 필요하다

3-2. ★ 기동 완료 대기 (그냥 기다리지 말고 판정한다) ​

최초 기동은 DB 생성까지 3~10분 걸린다. 이 사이에 접속하면 ORA-12514가 나는데, 고장이 아니라 아직 준비 중이라는 뜻이다.

bash
until docker logs oracle-free 2>&1 | grep -q "DATABASE IS READY TO USE!"; do
  echo "  ... 기동 대기 중 $(date +%H:%M:%S)"; sleep 10
done; echo "준비 완료"

정상 출력

  ... 기동 대기 중 12:03:11
  ... 기동 대기 중 12:03:21
준비 완료

Git Bash가 아니라 PowerShell이라면 이렇게 한다.

powershell
do { Start-Sleep 10; $log = docker logs oracle-free 2>&1 | Out-String } until ($log -match "DATABASE IS READY TO USE!")

3-3. 접속 확인 ​

어디서: 호스트 셸

bash
docker exec -i oracle-free sqlplus -s system/oracle@localhost/FREEPDB1 <<'SQL'
SELECT 'CONNECTED: ' || SYS_CONTEXT('USERENV','CON_NAME') AS status FROM dual;
EXIT
SQL

정상 출력

STATUS
--------------------------
CONNECTED: FREEPDB1

접속 정보 (이 저장소 전체에서 사용)

항목값
Host / Portlocalhost / 1521
CDB 서비스FREE ← AWR 실습은 여기
PDB 서비스FREEPDB1 ← 평소 실습은 여기
관리자system / oracle · sys / oracle (as sysdba)
실습 계정sqlp / sqlp
ORACLE_HOME/opt/oracle/product/26ai/dbhomeFree
데이터파일/opt/oracle/oradata/FREE/

4. gvenzl 이미지 함정 4종 (실측으로 확인된 것만) ​

4-1. 홈 경로가 /opt/oracle이다 ​

/u01/app/oracle이 아니다. 인터넷 예제를 그대로 붙여 넣으면 경로에서 막힌다.

4-2. docker cp로 넣은 파일은 소유자가 root다 ​

bash
docker cp ./data oracle-free:/opt/oracle/pup_data
docker exec -u 0 oracle-free chown -R oracle:oinstall /opt/oracle/pup_data   # ← 이 줄을 빼면 sqlldr 이 못 읽는다

실패 증상: SQL*Loader-500: Unable to open file / Permission denied

4-3. ★ APP_USER(sqlp)는 기본 권한만 있어 실행계획을 못 본다 ​

이대로 두면 DBMS_XPLAN.DISPLAY_CURSOR와 AUTOTRACE가 막힌다.

실패 증상

ORA-01031: insufficient privileges
또는  User has no SELECT privilege on V$SQL

조치: SYS로 한 번만 실행한다.

bash
docker exec -i oracle-free sqlplus -s / as sysdba <<'SQL'
ALTER SESSION SET CONTAINER = FREEPDB1;
GRANT SELECT_CATALOG_ROLE, SELECT ANY DICTIONARY, ALTER SESSION TO sqlp;
SQL

★ 권한을 준 뒤에는 sqlp 세션에 반드시 다시 접속해야 롤이 활성화된다. 다시 접속하지 않으면 같은 오류가 계속 난다. 10046/10053 트레이스, X$, AWR까지 다루려면 권한이 더 필요하다. 그 권한은 90_측정권한_5티어.sql(미작성)에서 다룰 예정이고, 지금은 00_측정권한_부여.sql에 있다.

4-4. NLS / 한글 ​

이미지가 이미 NLS_LANG=.AL32UTF8로 기동한다(실측). 따로 설정할 필요가 없다. CP949 CSV를 sqlldr로 넣을 때만 컨트롤 파일에 CHARACTERSET KO16MSWIN949를 준다.

세 데이터 생성기 README에 흩어져 있던 인코딩 처리는 이 절로 일원화한다.


5. ★★ 기존 데이터를 살려서 볼륨으로 옮기기 (무손실 이관) ​

§0에서 Binds: null이 나온 사람만 해당한다. 순서를 바꾸면 데이터가 사라진다.

절대 규칙: ④ 재적재 검증이 끝나기 전에는 구 컨테이너를 docker rm하지 않는다.

① 안전망 — 이미지 스냅샷 (가장 싸고 확실한 보험) ​

⚠️ 반드시 DB를 먼저 정지한 뒤 커밋한다. 기동 중인 오라클을 그대로 docker commit하면 데이터파일을 쓰는 도중에 찍게 되어 크래시 상태(crash-consistent)의 스냅샷이 된다. 복구할 때 인스턴스 리커버리로 대개 살아나지만 보장은 없다.docker stop은 컨테이너에 SIGTERM을 보내고, gvenzl 이미지는 이 신호를 받아 정상 셧다운을 한다.

bash
docker stop oracle-free                                    # ① 정상 셧다운 (10~30초)
docker commit oracle-free sqlp-backup:pre-volume-$(date +%Y%m%d)   # ② 커밋
docker start oracle-free                                   # ③ 다시 기동
docker images | grep sqlp-backup

정상 출력: 컨테이너 쓰기 레이어 크기만큼의 이미지가 생긴다(실측 17.4GB, 수 분 소요).

sqlp-backup   pre-volume-20260901   a1b2c3d4e5f6   2 minutes ago   19.4GB

주의: 커밋하는 동안에는 디스크가 그만큼 더 필요하다. 실측 환경에서는 여유 84GB 중 17GB를 더 썼다.

이 스냅샷 하나로 언제든 원상 복구할 수 있다. docker run -d --name oracle-free-restored -p 1521:1521 sqlp-backup:pre-volume-20260901

💡 스냅샷은 "통째로 되돌리기"에는 강하지만 선택적 복구가 안 된다(특정 스키마만 되살리기 불가). 그래서 ②의 expdp를 함께 한다. 둘은 용도가 다르다.

② 논리 백업 — expdp ​

스냅샷은 "통째로 되돌리기"용이고, expdp는 "새 컨테이너에 옮겨 심기"용이다. 둘 다 한다.

bash
docker exec -i oracle-free bash -lc '
  mkdir -p /opt/oracle/dump && chown oracle:oinstall /opt/oracle/dump
  sqlplus -s / as sysdba <<SQL
ALTER SESSION SET CONTAINER = FREEPDB1;
CREATE OR REPLACE DIRECTORY DP_DIR AS '"'"'/opt/oracle/dump'"'"';
GRANT READ, WRITE ON DIRECTORY DP_DIR TO system;
SQL
  expdp system/oracle@localhost/FREEPDB1 \
        directory=DP_DIR dumpfile=sqlp_all.dmp logfile=exp.log \
        schemas=SQLP,DEV,PUP
'

정상 출력의 마지막 줄: 이 문장이 없으면 실패다.

Job "SYSTEM"."SYS_EXPORT_SCHEMA_01" successfully completed at ...

무결성 확인 (덤프가 0바이트가 아닌지):

bash
docker exec -i oracle-free ls -lh /opt/oracle/dump/

호스트로도 빼 둔다.

bash
docker cp oracle-free:/opt/oracle/dump ./_backup_dump

③ 새 컨테이너 기동 (볼륨 + 태그 고정 + shm) ​

bash
docker stop oracle-free && docker rename oracle-free oracle-free-old
docker volume create oracle-data
docker run -d --name oracle-free -p 1521:1521 --shm-size=2g \
  -v oracle-data:/opt/oracle/oradata \
  -e ORACLE_PASSWORD=oracle -e APP_USER=sqlp -e APP_USER_PASSWORD=sqlp \
  gvenzl/oracle-free:23.26.2-slim

그다음 §3-2의 대기 루프로 DATABASE IS READY TO USE!를 확인한다.

구 컨테이너는 지우지 않고(docker rm 아님) 이름만 바꿔(docker rename) 살려 둔다.

④ 재적재 — impdp ​

bash
docker cp ./_backup_dump oracle-free:/opt/oracle/dump
docker exec -u 0 oracle-free chown -R oracle:oinstall /opt/oracle/dump
docker exec -i oracle-free bash -lc '
  sqlplus -s / as sysdba <<SQL
ALTER SESSION SET CONTAINER = FREEPDB1;
CREATE OR REPLACE DIRECTORY DP_DIR AS '"'"'/opt/oracle/dump'"'"';
GRANT READ, WRITE ON DIRECTORY DP_DIR TO system;
SQL
  impdp system/oracle@localhost/FREEPDB1 \
        directory=DP_DIR dumpfile=sqlp_all.dmp logfile=imp.log
'

⑤ 검증 — 행수 대조 (이게 통과해야 ⑥으로 간다) ​

bash
docker exec -i oracle-free sqlplus -s system/oracle@localhost/FREEPDB1 <<'SQL'
SET LINESIZE 120 PAGESIZE 50
SELECT owner, COUNT(*) AS tabs FROM dba_tables
 WHERE owner IN ('SQLP','DEV','PUP') GROUP BY owner ORDER BY owner;
SELECT COUNT(*) AS dev_ord FROM dev.ord;
SELECT COUNT(*) AS pup_cf11 FROM pup.cf11;
SELECT COUNT(*) AS sqlp_orders FROM sqlp.orders;
SQL

기대값 (이관 전 실측치와 일치해야 한다)

대상기대
DEV 테이블 수17
PUP 테이블 수18
dev.ord1,000,000
pup.cf111,096,708
sqlp.orders200,000

⚠️ 통계는 함께 넘어오지 않을 수 있다. 이관한 뒤 DBMS_STATS.GATHER_SCHEMA_STATS를 다시 돌리고, 특히 04화 CF 대조군이 살아 있는지 확인해야 한다. 이 값이 이 교재의 교보재다.

sql
SELECT table_name, index_name, clustering_factor
  FROM dba_indexes WHERE owner='DEV' AND table_name IN ('ORD','ORD_RANDOM');
-- 기대: ORD_X01 ≈ 22,223 (좋음) / ORD_RANDOM_X01 ≈ 999,965 (나쁨)

⑥ 구 컨테이너 폐기 ​

⑤가 전부 통과한 뒤에만 실행한다.

bash
docker rm oracle-free-old
# 스냅샷 이미지는 한동안 남겨 둔다. 확신이 서면: docker rmi sqlp-backup:pre-volume-20260901

6. 일상 운용 ​

bash
docker start oracle-free     # 켜기
docker stop  oracle-free     # 끄기 (볼륨이 있으면 데이터 안전)
docker logs -f oracle-free   # 기동 로그

알아 둘 로그: 실측 환경에서 실제로 나온 메시지다.

Checkpoint not complete
Thread 1 advanced to log sequence 686 (LGWR switch)

이것은 고장이 아니라 리두 로그가 작아 로그 스위치가 잦다는 신호다. 13화(Redo·Undo)에서 log file sync / log file parallel write와 함께 다룰 실제 소재다. 지금은 무시해도 된다.


7. 트레이스 파일 반출 (15화 전제) ​

10046 raw 트레이스를 읽으려면 컨테이너 안의 파일을 꺼내야 한다.

bash
# 1) 경로 확인 (SQL*Plus 안)
#    SELECT value FROM v$diag_info WHERE name='Default Trace File';
# 2) 컨테이너 안에서 바로 tkprof (tkprof 는 slim 이미지에 포함되어 있다 — 실측 확인)
docker exec -i oracle-free bash -lc 'tkprof /opt/oracle/diag/rdbms/free/FREE/trace/<파일>.trc /tmp/out.txt sys=no'
docker cp oracle-free:/tmp/out.txt ./out.txt

8. 이 문서를 마친 뒤 ​

  1. ☐ L04_옵션_실측_점검.sql(미작성: 실측 쿼리는 05_Free와_EE_트랙G.md §2) 실행 → 결과를 L00_에디션별_기능가용성_정본.md(미작성: 정본 표는 05_Free와_EE_트랙G.md §2)에 기록
  2. ☐ 90_측정권한_5티어.sql(미작성: 지금은 00_측정권한_부여.sql)로 권한 부여
  3. ☐ 데이터셋 구축 → 00_데이터셋_종합안내.md
  4. ☐ L03_AWR_활성확인과_스냅설정.md(미작성: AWR 활성 확인은 05_Free와_EE_트랙G.md §2)로 AWR 실습 준비

부록 — 실패 증상 빠른 조회표 ​

증상원인조치
ORA-12514: TNS:listener does not currently know of service기동이 아직 안 끝남§3-2 대기 루프. 5~10분 기다린다
ORA-01031: insufficient privileges (DISPLAY_CURSOR)sqlp 권한 부족§4-3 → 재접속
ORA-00845: MEMORY_TARGET not supported/dev/shm 부족(기본 64MB)--shm-size=2g로 컨테이너 재생성
SQL*Loader-500: Unable to open filedocker cp로 넣은 파일의 소유자가 root§4-2 chown oracle:oinstall
컨테이너 재생성 후 스키마가 사라짐볼륨 없이 docker rm스냅샷(§5-①)에서 복구. 없으면 재구축
docker: Error response from daemon: ... port is already allocated1521을 다른 컨테이너가 점유docker ps로 확인한 뒤 중지하거나, -p 1522:1521로 변경
failed to connect to the docker APIDocker Desktop 꺼짐Docker Desktop 실행