프레임워크 = SQL 생성기 + 커넥션 관리자
ORM과 프레임워크가 하는 일을 “어떤 SQL을 몇 번 보내고, 커넥션을 어떻게 빌려 쓰는가”로 봅니다. 객체 코드를 짤 때도 머릿속에 SQL이 보이게 만드는 것이 목표입니다.
LEVEL 1기초 백엔드 · 수업 교재 커리큘럼 안내서
SQL·실행계획·Lock을 이미 아는 사람이, 그 위에 백엔드를 한 층씩 직접 쌓아 올리는 과정입니다. FastAPI와 Django, Oracle 26ai, Docker로 동작하는 주문 백엔드를 만들며, 모든 프레임워크 동작을 “오라클에 어떤 SQL이, 몇 번, 어떤 실행계획으로 나가는가”로 풀어 봅니다. 결론은 전부 직접 잰 숫자로 남깁니다.
대부분의 백엔드 강좌는 DB를 블랙박스로 둡니다. 이 과정은 정반대에서 출발합니다. DB를 SQLP 수준으로 아는 사람에게, 그 지식이 백엔드의 어느 지점에서 무기가 되는지를 Quest마다 직접 재서 보여 줍니다.
ORM과 프레임워크가 하는 일을 “어떤 SQL을 몇 번 보내고, 커넥션을 어떻게 빌려 쓰는가”로 봅니다. 객체 코드를 짤 때도 머릿속에 SQL이 보이게 만드는 것이 목표입니다.
추측은 금지입니다. 성능을 다루는 Quest는 개선 전(naive)과 후의 숫자를 표로 남겨야 통과합니다. 실행계획의 Buffers와 요청당 발행 SQL 수, 두 개의 자를 씁니다.
완성본을 읽는 대신 빈 파일에서 한 블록씩 쌓습니다. 블록마다 어디에 넣는지, 무엇이 켜지는지, 바로 돌려서 무엇을 확인하는지를 따라갑니다.
주문 백엔드(Order Service) 하나가 Quest를 따라 진화합니다. 상품 조회에서 시작해 주문·재고·인증·큐·배포까지, 앞 결과물에 살을 붙여 나갑니다.
“이 느린 HTTP 요청이 실행한 그 SQL은 오라클에서 어떤 실행계획과 Buffers였나?” 앱 trace와 오라클 v$sql을 SQL_ID로 잇는 법을 Quest 00에서 처음 맛보고, Quest 07에서 OpenTelemetry로 정식으로 만들며, Quest 14 부하 분석에서 실전에 씁니다.
모두 이 교재의 검증 환경에서 잰 값입니다
v$sql → 실행계획Q07DB는 아는데 서버는 처음인 사람
GATHER_PLAN_STATISTICS로 전/후 Buffers 비교DB가 처음이라면 SQLP(오라클 SQL 튜닝) 과정을 먼저 진행하세요. 이 과정은 오라클 도커와 실습 데이터셋이 이미 갖춰져 있다고 가정합니다.
새 인프라를 크게 늘리지 않습니다. 이미 띄워 둔 Oracle 26ai Free 컨테이너 위에 앱 계층만 올리고, 노트북 한 대에서 개발·측정·배포·CI까지 전 과정을 돌립니다.
curl · httpx · 브라우저
자동 문서 /docs
로컬 venv에서 실행, Quest 13부터는 컨테이너로
PDB FREEPDB1 · 실습 계정 sqlp
앱이 보낸 SQL을 오라클 내부 지표로 되돌아봅니다 — 요청당 SQL 수(query_counter), Buffers(DISPLAY_CURSOR), SQL_ID(v$sql).
ruff(린트) → mypy(타입) → pytest(테스트) → docker build. 빠르고 흔한 실패부터 막습니다.
칩을 누르면 해당 Quest로 이동합니다
보유한 오라클 도커를 재사용하고, venv와 12-factor .env로 설정을 코드에서 분리합니다. 비밀은 저장소에 올리지 않습니다.
Instant Client가 필요 없는 thin 모드. 풀 크기가 곧 오라클 세션 수이고, processes 한도·DRCP와 맞물리는 이유를 다룹니다.
이벤트 루프와 스레드풀, python-oracledb의 진짜 async. 동시성이 결국 커넥션 풀에 묶인다는 사실을 측정으로 확인합니다.
상시 메시지 브로커 대신 DB 잡 테이블과 SKIP LOCKED로 큐를 만듭니다. 2GB RAM 한도에 맞춘 선택입니다.
JSON 구조화 로그, request_id, X-SQL-Count 헤더, 그리고 요청과 v$sql의 상관. 요청이 스스로 비용을 말하게 합니다.
레이어 캐시를 살린 Dockerfile, compose, liveness/readiness 헬스체크, SIGTERM에 처리 중 요청을 끝내고 내려가는 graceful shutdown.
실 오라클 위의 롤백 격리 테스트, Alembic 마이그레이션, GitHub Actions로 린트·타입·테스트·빌드를 자동 검증합니다.
p50/p95와 처리량, 풀을 키우면 기다림이 어디로 옮겨 가는지, 요청 시간을 앱·풀·DB 3계층으로 쪼개 재는 법(Server-Timing).
풀을 키우면 오히려 느려지는 현상(Q05), 상시 브로커 대신 DB 큐를 쓰는 이유(Q10), 데이터셋을 필요한 것만 적재하는 운영 습관까지 — 작은 한도가 현실의 용량 계획을 미리 연습시킵니다.
설치 직전 공식 문서로 버전을 고정합니다
| 영역 | 선택 | 메모 |
|---|---|---|
| 언어 | Python 3.12+ | 검증 환경 3.13 |
| 웹 프레임워크 | FastAPI · Starlette · Pydantic v2 | Tier 1의 주축. 진짜 async I/O의 주체 |
| ORM | SQLAlchemy 2.0 | 비동기는 oracle+oracledb_async (2.0.25+) |
| 오라클 드라이버 | python-oracledb (thin 모드) | pip install oracledb · cx_Oracle 대체 · Instant Client 불필요 |
| 데이터베이스 | Oracle 26ai Free | gvenzl/oracle-free:23.26.2-slim · PDB FREEPDB1 |
| 병행 프레임워크 | Django 6.0 또는 5.2 LTS | Quest 08에서 첫 투입, Tier 2에서 본격 대조 |
| 마이그레이션 | Alembic · Django migrations | 무중단(expand-contract)은 Tier 2 |
| 테스트 | pytest · httpx (TestClient) | 단위·통합·E2E·Mock, 롤백 격리 |
| 컨테이너 | Docker · docker compose | 앱 컨테이너 → 호스트 오라클 |
| CI | GitHub Actions | ruff → mypy → pytest → build |
검증 환경 패키지: fastapi 0.138 · sqlalchemy 2.0.51 · oracledb 4.0.1 · pydantic 2.13 · uvicorn 0.49
누적 프로젝트의 기준 스키마는 데이터셋 A 하나입니다
| 데이터셋 | 내용 | 쓰임 |
|---|---|---|
A · sqlp | PRODUCTS 2천 · CUSTOMERS 2만 · ORDERS 20만 | Tier 1 전 구간, 주문 백엔드의 기준(write) 스키마 |
B · sqlp | T_ORD_BIG 100만 건, 인덱스 없음 | 대용량·풀스캔 비용 체감 (선택) |
| C · D · E | 튜닝 실습 · 유통 바스켓 · 이커머스 30테이블 | Tier 2 도메인 탐구용 read-only 데이터 |
각 Quest 폴더는 문서 5종과 실행 가능한 app/으로 이루어집니다. 아래 순서대로 읽고, 만들고, 돌리고, 재고, 확장합니다.
목표·핵심 개념·체크리스트·함정·심화 화두. 이 Quest가 왜 필요한지부터.
빈 파일에서 한 블록씩 쌓습니다. 블록마다 바로 실행해서 효과를 확인합니다.
실행 명령과 검증 환경에서 나온 실제 출력. 내 결과와 비교합니다.
전(naive)→후(개선) 측정표. 발행 SQL 수·Buffers·p95를 남깁니다.
🟢 기본 · 🟡 응용 · 🔴 심화 난이도별 도전 과제. 답도 측정으로 증명합니다.
app/그 Quest 시점의 동작하는 참조 구현(스냅샷). 베끼지 말고, 막혔을 때 대조용으로 씁니다.Quest 02 · 오버셀을 막는 한 줄
app/main.py — naive 엔드포인트 아래에 추가합니다. naive와 딱 한 줄 다릅니다.
stock = db.execute(select(ProductStock.stock)
.where(ProductStock.prod_id == body.prod_id)
.with_for_update()).scalar_one_or_none() # ★ 행을 잠근다
time.sleep(settings.sim_work_delay) # 락을 쥔 채 업무 → 남은 요청은 대기
if stock < body.qty: raise HTTPException(409, "out of stock")
.with_for_update()가 SELECT … FOR UPDATE를 만들어 재고 행을 트랜잭션이 끝날 때까지 잠급니다. 두 번째 요청은 이 SELECT에서 기다렸다가 갱신된 재고를 읽습니다. 읽기-확인-쓰기가 원자적이 되어 오버셀이 사라집니다.
[안전 — FOR UPDATE] /orders/safe (초기재고 5, 동시요청 20)
성공(201)=5 품절(409)=15 최종 재고 = 0 생성된 주문 = 5 → 정상
어디에 넣는지(파일과 위치) → 그 블록이 무엇을 켜는지(한 단락) → 바로 돌려서 효과를 확인. 한 블록 쓰고, 돌려 보고, “왜 이 모양인지” 말할 수 있을 때 다음 블록으로 넘어갑니다.
원본 WebDevCurriculum의 Quest 구조를 계승했습니다.
각 Quest의 질문에 왜 → 어떻게 → 장단점·대안 순서로, 내 언어로 답합니다. 답변은 산출물로 남겨 면접 답변처럼 자가 점검에 씁니다.
성능을 다루는 Quest에서 필수
쿼리를 실제로 실행한 뒤 실행계획을 떠서 Buffers, A-Rows 대 E-Rows, 실제 계획을 봅니다.
SELECT /*+ GATHER_PLAN_STATISTICS */ …;
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
format => 'ALLSTATS LAST'));
SQLAlchemy 이벤트로 한 요청이 DB에 몇 번 왕복하는지 셉니다. N+1은 이 숫자로만 잡힙니다.
@event.listens_for(
engine, "before_cursor_execute")
def _count(*args):
counter["n"] += 1
p95 지연, 앱 trace와 SQL_ID의 상관, 동시성 충돌률. 핀테크 캡스톤에서는 잔액 정합성과 데드락률까지 측정합니다.
테스트(Q08)·API 설계(Q11)처럼 구조와 도구가 주제인 Quest도 느낌 대신 검증 결과를 남깁니다 — 격리는 바깥 커넥션으로 증명하고, 계약은 스크립트로 확인합니다.
플립러닝: 수업 전에 혼자 설명하고, 수업에서 맞붙이고, 끝나고 우리 답과 견준다
① 설명 동료에게 그려 보라 · ② 근거 내 숫자로 답하라 · ③ 판단 조건이 바뀌면 답이 바뀌는가. 답은 1 용어 → 2 원리 → 3 근거(내 측정값) → 4 판단(답을 바꾸는 조건)으로 스스로 채점합니다. 우리와 같은 답이어도 이유를 못 대면 1수준이고, 다른 답이어도 조건을 댔다면 4수준입니다.
질문마다 접힌 우리의 답변과 답이 갈리는 지점이 있습니다. 예: “느린 대시보드에 인덱스를 더할까?” — Q14에서 우리는 안 한다고 답했습니다. 인덱스로 아낀 건 1~3 버퍼뿐이고 병목은 101번의 왕복이었으니까요. 하지만 그 쿼리가 풀스캔이었다면 정답은 인덱스입니다. 같은 데이터인데 통계가 바뀌자 옵티마이저가 다른 계획을 고른 것도 우리가 직접 겪었습니다.
Quest 00~14, FastAPI 단독으로 측정 가능한 백엔드를 직접 만들어 돌립니다. 이 과정의 필수 완주 구간입니다. 각 Quest를 펼치면 배우는 것, 직접 만드는 것, 측정으로 증명하는 것을 볼 수 있습니다.
오라클에 붙어 한 줄을 읽는 것에서 시작해, ORM이 처음 등장합니다.
.env에서def 엔드포인트가 스레드풀에서 도는 이유settings.py · db.py · main.py · smoke_oracledb.py를 빈 파일에서 한 블록씩GET /health(DB 핑) · GET /dept(DEPT 조회)/dept가 보낸 SQL을 오라클 v$sql에서 SQL_ID로 되찾고, 한 번 부를 때마다 executions +1 · buffer_gets +4(INDEX FULL SCAN PK_DEPT)가 쌓이는 것을 확인합니다. 이후 모든 Quest가 쓰는 측정 습관의 시작입니다.세션 생성 비용(v$session)이 바로 커넥션 풀을 쓰는 이유입니다.
connect()를 하면 무엇이 비싼가?DeclarativeBase · Mapped · Session · select()Depends(get_db) — 요청마다 세션을 열고 닫는 의존성 주입.limit()이 오라클 FETCH FIRST로 번역되는 원리(dialect)GET /products(목록·카테고리 필터) · GET /products/{id}(단건·404)query_counterINDEX UNIQUE SCAN3 buffersTABLE ACCESS FULL19 buffers이미 아는 PK 조회 vs 풀스캔의 차이를, ORM이 만든 SQL 위에서 다시 확인합니다.
.limit(3)은 오라클에서 어떤 SQL이 되나?쓰기·대용량·N+1·동시성 — SQLP 지식이 애플리케이션 코드로 내려오는 구간입니다.
commit()/rollback()과 오라클 트랜잭션SELECT … FOR UPDATE와 낙관적 대안(UPDATE … WHERE stock >= qty)POST /orders/naive와 안전한 POST /orders/safe — 딱 한 줄 차이concurrent_test.py — 행 락 대기(v$system_event)까지 측정Idempotency-Key 헤더 + UNIQUE 제약으로 중복 요청 흡수FOR UPDATE주문 5 · 재고 0Idempotency-Key 동시 20회주문 1건READ COMMITTED · 행 락 · TX enqueue 대기 · 데드락을 이제 내 코드가 긋는 트랜잭션 경계에서 만납니다.
OFFSET n ROWS FETCH NEXT m의 비용WHERE id > :after — 깊이와 무관한 비용COUNT(*)의 숨은 비용GET /orders(오프셋)와 GET /orders/keyset(키셋)을 나란히measure_q03.pyTop-N · WINDOW NOSORT STOPKEY · INDEX RANGE SCAN. 페이징 튜닝 지식이 API 설계 결정으로 바뀝니다.
OFFSET 180000은 DB에게 무엇을 시키나?selectinload(IN 리스트) vs joinedload(JOIN)/orders-with-names/naive · /selectin · /joinedmeasure_q04.pyselectinload3 SQL · 왕복 6 · 약 5msjoinedload1 SQL · 왕복 2 · 약 1.8ms추가 쿼리 하나하나는 3 buffers짜리 PK 조회라 “쿼리 하나 튜닝”으로는 안 잡힙니다. 문제는 개수와 왕복입니다.
joinedload를 쓰면 무슨 일이 생기나?processes, DRCP 개념GET /stock/{id} · 재고 조정 APIpool_bench.py세션·논리 I/O·읽기 일관성이 애플리케이션 설계의 손잡이로 올라옵니다.
누가 요청했는지 알고, 요청이 스스로 비용을 말하게 하고, 그 계약을 테스트로 못박습니다.
/me → /admin(403) → 리프레시 회전 → 로그아웃 흐름/me/stateless인증 SQL 0/me/dbcheck요청당 +1Q05의 “캐시 무효화 vs 정합성”과 같은 고민입니다 — 즉시 차단하려면 DB를 봐야 하고, 그만큼 SQL이 늡니다.
{error:{code, message, request_id}}contextvars로 요청별 상태 추적, X-SQL-Count 응답 헤더db.oracle.sql_idv$sql을 바로 집기/* req=<id> */를 새기는 방식의 하드 파싱 비용GET /report/orders와 상관 데모 demo_q07.pyv$sql텍스트 검색 없이 바로요청마다 SQL 텍스트가 달라지면 커서 공유가 깨지고 하드 파싱이 늡니다. 그래서 텍스트는 그대로 두고, SQL_ID를 trace 쪽에 붙여 상관시킵니다.
TestClient, 의존성 오버라이드begin_nested) vs 트런케이트managed=False 모델, adminseed.pynote='' 저장 후 조회None (빈 문자열 = NULL)테스트마다 트랜잭션을 열고 되돌리는 롤백 격리는 SAVEPOINT 지식 그대로입니다.
commit()해도 테스트가 롤백되게 하려면?async의 진실, 요청 흐름 밖으로 일을 빼는 큐, 깨지지 않는 API 계약, 그리고 공격에 대한 방어.
async def와 def가 각각 도는 곳create_pool_async)async def 안의 동기 호출, 그리고 asyncio.to_threadsync_to_async 워커 스레드, async 트랜잭션 미지원/sync · /async · /async_blocking · /async_offload와 같은 일을 하는 Django 동기·async 뷰thread_demo.py, 부하 도구 loadtest.py/async · /sync · Django · to_thread1.06~1.10sasync든 sync든 동시성의 상한은 오라클 세션 수입니다. async의 이점은 처리량보다 적은 스레드와 확장성입니다.
FOR UPDATE SKIP LOCKED로 여러 워커가 경합 없이 분담FETCH FIRST와 FOR UPDATE는 한 문장에 못 쓴다POST /orders(큐잉)와 POST /orders-inline(인라인) 비교worker.py, 분담·멱등 측정 measure_q10.pySKIP LOCKED claim이 잠근 행2→1 (prefetch)행 락 · SKIP LOCKED · status 인덱스가 큐의 재료입니다. 이미 아는 Lock 지식의 응용입니다.
SKIP LOCKED 없이 FOR UPDATE만 쓰면?Deprecation·Sunset 헤더와 OpenAPI deprecated/docs · /redoc · /openapi.json — 문서가 곧 계약/v1/products(offset, 구 필드명, deprecated)와 /v2/products(cursor, 새 필드명)verify_q11.pyDeprecation · Sunset · LinkErrorEnvelopev1 offset → v2 cursor 전환은 Q03 키셋 측정의 근거를 API 계약으로 옮긴 것입니다.
.env 커밋 금지, 로그·응답 마스킹/search/vuln·/search/safe, /orders/{id}/vuln·/safeverify_q12.py' OR '1'='12000건 유출인젝션 방어 = 바인드 변수, 커서 공유와 하드 파싱 회피의 바로 그 도구입니다. 보안과 성능이 같은 도구로 해결됩니다.
안전하게 교체 가능한 형태로 배포하고, 부하 아래에서 진짜 병목을 측정으로 찾습니다.
.dockerignore, thin 모드의 가벼운 이미지host.docker.internal로 호스트 오라클 연결stop_grace_period/healthz/live · /healthz/ready를 갖춘 앱과 Dockerfile · docker-compose.ymlcrash_demo.pydocker stop200 · exit 0docker kill · grace 부족 · 셸 형식 CMD응답 없음 · exit 137커밋되지 않은 트랜잭션은 오라클이 롤백합니다 — “반쯤 처리된 주문”이 남지 않는 근거입니다.
Server-Timing 헤더로 요청마다 pool · db · appv$sql elapsedGET /dashboard(naive / eager)Server-Timing), 측정 measure_q14.py, 부하 도구 loadtest_q14.py“느리면 추측하지 말고 측정하라.” 인덱스는 옵티마이저가 쓰지도 않았고, 풀을 키우자 기다림이 GIL로 옮겨 갔을 뿐입니다. 측정이 가리킨 진짜 병목은 101번의 왕복(N+1)이었습니다.
이 분야에 해당하는 Quest가 없습니다.
SQLP 교재의 합격 → 딥다이브 → Hero 서사를 백엔드로 옮겼습니다. 같은 주문 백엔드를 세 번 관통하며, 매번 한 층 더 깊이 들어갑니다.
SQLP의 “합격”에 해당
측정 가능한 백엔드를 직접 만들어 돌립니다. 이 과정의 필수 완주 구간입니다.
SQLP의 “딥다이브”에 해당
내부 원리와 Django 대조, 운영성·아키텍처, 도메인 탐구로 넓고 깊게 들어갑니다.
SQLP의 “Hero”에 해당
부하·분산·정합성을 측정으로 증명하고, 핀테크 거래 원장 캡스톤으로 마무리합니다.
펼치면 배우는 것 · 잰 숫자 · 끝나면 말할 수 있는 것
Tier 1의 Quest를 Django로 다시 만들어 두 철학을 대조합니다 — 배터리 포함(ORM·admin·migrations) vs 미니멀 + 타입. 같은 오라클, 같은 저울로 잽니다.
get()이 FETCH FIRST 21 ROWS ONLY를 보내는 이유Paginator는 요청마다 COUNT(*)를 얹고 페이지마다 새 커서를 만든다get(pk=1)FETCH FIRST 21 · 3 BuffersQuerySet의 게으름과 캐시를 SQL 개수로 설명하고, get()·exists()·Paginator가 오라클에 실제로 보내는 텍스트와 비용(Buffers·커서 수)을 말할 수 있다.
select_related(JOIN) ≈ joinedload, prefetch_related(IN) ≈ selectinloadprefetch_related의 실패(Django 6.0.6)prefetch_relatedORA-01722같은 화면이 201 → 3 → 1 SQL이 되는 것을 재고, Django에만 있는 두 함정 — identity map이 없다, 오라클 IN 1000개 한도에서 prefetch_related가 깨진다 — 을 설명하고 피할 수 있다.
atomic() 밖의 쓰기는 문장마다 커밋된다atomic()만으로는 오버셀이 남는다obj.stock -= 1; obj.save()는 절대값을 써서 갱신 손실을 만든다 → F()·조건부 update()‘트랜잭션 ≠ 잠금’, ‘save()는 절대값을 쓴다’, ‘autocommit은 실패를 반쯤 남긴다’를 숫자로 설명하고, Django ORM만으로는 큐에서 1행만 잠그기가 안 되는 이유와 우회를 안다.
CONN_MAX_AGE=0 = 요청마다 오라클 세션을 열고 닫는다CONN_MAX_AGE는 스레드에 붙는다 — 스레드를 재사용하는 서버에서만 통한다‘Django는 기본으로 요청마다 오라클 세션을 새로 연다’, ‘CONN_MAX_AGE는 스레드를 재사용하는 서버에서만 통한다’, ‘로그인된 요청 = SQL 2개’를 숫자로 말하고, 세션 저장소를 고를 때 잃는 것을 안다.
TestCase(savepoint 롤백) vs TransactionTestCase(커밋 + 테이블 비우기)makemigrations가 만드는 오라클 DDL 읽기default=는 DB 기본값을 지운다 → 배포 중 옛 코드의 INSERT가 깨진다default= 추가 뒤 옛 코드 INSERTORA-01400‘Django 테스트는 오라클 사용자를 만든다(DBA급 권한)’, ‘TransactionTestCase는 테스트마다 테이블을 비운다(시간 37배, SQL 3.4배)’, ‘default=는 DB 기본값을 지운다’를 숫자로 설명한다.
%s)로는 SQL_ID를 못 구한다 → 드라이버 커서의 .statement로raw() 인젝션, CSRF의 경계, check --deployv$sql드라이버 .statement로 일치raw() 인젝션 · 리터럴 vs 바인드2000 vs 0행check --deploy 경고6 → 2‘Django가 보여 주는 SQL로는 SQL_ID를 못 구한다(%s → :arg0)’, ‘DRF 시리얼라이저 한 줄이 페이지당 SQL 42개를 만든다’, ‘DRF는 익명 POST에 CSRF를 안 본다’를 숫자로 설명한다.
서비스를 멈추지 않고 바꾸고, 서비스 사이까지 관측하고, 코드에 경계를 긋는 값을 잽니다.
ADD COLUMN은 기본값이 있어도 메타데이터만 바꾼다UPDATE 한 방은 100만 행을 잠그고 redo를 쏟는다 → 키 범위로 나눠 커밋ddl_lock_timeout은 실패를 앱 정지로 바꾼다, 잊힌 트랜잭션 하나가 DDL을 무한히 세운다‘ADD COLUMN은 메타데이터만 바꾼다(0.01s)’, ‘백필 UPDATE 한 방은 앱을 7초 멈춘다’, ‘ddl_lock_timeout은 실패를 앱 정지로 바꾼다’, ‘RENAME은 롤링 배포 중 498건의 에러’를 숫자로 말하고, 오라클 마이그레이션 절차를 설계할 수 있다.
traceparent 주입·추출로 서비스 둘을 한 줄기 trace로module·action·client_identifier — 드라이버가 다음 호출에 실어 보낸다v$session에서 trace를, trace에서 세션을 서로 찾기요청 하나를 서비스 → SQL_ID → v$sql.module/action까지, 실행 중인 세션을 v$session.client_identifier = trace_id로 찾을 수 있고, 그 비용이 왕복 0·span 18µs임을 안다.
‘모듈로 나눴다고 SQL이 줄지 않는다 — 건별 API는 101개(2,000건이면 4,001개)’, ‘일괄 API로 3~5개’, ‘경계를 무시한 JOIN은 1개지만 그 대가는 결합’을 숫자로 말하고, 경계를 어디에 그을지 판단한다.
데이터셋 위에서 실전 도메인을 read-only 분석과 작은 PoC로 탐구하고, 결론을 분석노트로 남깁니다.
여러 행에 걸친 규칙을 어디서 지킬지(트랜잭션·감사·스냅샷)를 데이터로 말하고, 상태 전이를 조건부 UPDATE로 0 이중 전이로 만든다.
집계가 필요한 추천은 미리 계산해 PK 점 조회로 서빙하고, 신선도를 빌드 주기로 관리한다는 결론을 Buffers·지연·빌드 시간으로 말할 수 있다.
bulk_create의 UNION ALL 한 문장은 배치가 클수록 초선형으로 느려진다배치는 왕복과 커밋의 문제라는 것을 행/초로 말하고, 모델 교체를 읽기가 깨지지 않게 설계할 수 있다.
졸업 정점 · 3B 핀테크 캡스톤
DB 안을 진단하고, 동시에 많이 쓸 때의 경합을 재고, 트랜잭션 바깥의 정합성을 장애 주입으로 시험합니다.
INACTIVE) — 트레이스와 샘플링 중 무엇으로 볼지‘tkprof가 0.0ms라고 한 SQL은 실제로 1회 37µs였다’, ‘락을 쥔 범인은 DB에서 놀고 있다’를 숫자로 말하고, 운영 중 느려짐을 트레이스와 샘플링 중 무엇으로 볼지 고를 수 있다.
log file sync)dc_sequences, 캐시는 뮤텍스log file sync 비중~90%‘1행마다 커밋하면 키 전략이 아니라 커밋이 병목’, ‘핫 로우는 세션을 늘려도 1,600/s에서 멈추고, 커밋을 같은 왕복에 실으면 4,300/s’를 숫자로 말할 수 있다.
‘펜싱 토큰 없이 리스만 쓰면 멈춤 9번에 갱신 22개를 잃는다’, ‘이중 쓰기는 어느 순서로 해도 틀린다(유실 59 / 유령 76)’, ‘아웃박스의 중복 20을 멱등 소비자가 0으로 만든다’를 숫자로 말할 수 있다.
F1~F5 — 돈의 타입부터 부하 아래 증명까지, 하나의 결제 서비스를 쌓아 올립니다.
복식부기 원장으로 돈이 오가는 백엔드를 끝까지 만들고, 정합성·동시성·멱등을 측정으로 증명합니다. 실 결제망·실 금융 데이터 없이 합성 원장과 카드 승인 시뮬레이터를 씁니다. 전용 사용자 fin_owner·fin_app을 만들어, 앱 계정은 원장을 고치지도 지우지도 못하게 시작합니다.
‘python-oracledb는 NUMBER를 float로 준다(90조 원대에서 1전이 바뀐다)’, ‘앱 계정의 UPDATE·DELETE는 ORA-41900’, ‘불균형 분개는 커밋 시점에 막을 수 있지만 처리량이 1,250 → 300/s’를 숫자로 말할 수 있다.
‘데드락 3~4번이 처리량을 860~890 → 7~13/s로 무너뜨렸다’, ‘앱 확인만으로는 100만 원 계정이 −14만~−20만 원이 됐다’, ‘같은 핫 계정 결제가 약 400 → 1,600~1,800/s’를 숫자로 말할 수 있다.
‘키 없이 680번 요청에 중복 280’, ‘있나 보고 → 결제 → 기록은 동시 중복에서 99건 샌다’, ‘키를 결제와 같은 트랜잭션에 먼저 INSERT하면 0’을 숫자로 말할 수 있다.
‘대사 SQL 0.03초에 2만 건, 심은 불일치 4종을 개수까지 정확히’, ‘금액을 줄이고 잔액까지 맞춘 조작을 체인이 지목했다’, ‘체인까지 다시 쓰면 외부 앵커만 잡는다’를 숫자로 말할 수 있다.
‘1만 7천 요청, 590 req/s, 예상 밖 상태 0, 데드락 0’, ‘서버가 잰 p95 11~16ms vs 클라이언트 87~93ms — 꼬리는 서버 밖’, ‘부하에서 드러난 드라이버 멈춤을 재현하고 우회했다’를 숫자로 말할 수 있다.
주 8~10시간 학습 기준
Quest 00~14 통과 + 측정 대상 Quest의 전/후 측정표 + Quest 14에서 병목 1건을 앱·풀·DB 3계층에서 식별하고 개선.
핀테크 캡스톤에서 잔액 정합성·데드락 회피·멱등 보장 중 핵심을 측정으로 입증.
각 Quest의 체크리스트 답변을 산출물로 남기고, Tier별 루브릭으로 스스로 점검합니다.
교재의 모든 수치는 같은 검증 환경에서 실제로 돌려 얻은 출력입니다. 그리고 교재를 처음 보는 학생처럼 README부터 Tier마다 처음부터 따라 하며, 막히는 곳과 문서와 다른 출력을 찾아 고쳤습니다.
gvenzl/oracle-free:23.26.2-slim · cpu_count 2 · redo 10MB × 2.venv·.env 없는 새 사본에서 시작버전은 실습마다 고정해 설치합니다. 결과가 다르면 SQL보다 버전부터 의심하세요.
| Tier | 범위 | 결과 | 찾은 문제 |
|---|---|---|---|
| Tier 1 코어 | Q00~Q14 | 15/15 완주 | BLOCKER 0 · MAJOR 7 · MINOR 18 → 전부 반영 |
| Tier 2 딥다이브 | D1~D6 · O1~O3 · E·R·M | 12/12 완주 | BLOCKER 1 · MAJOR 4 · MINOR 8 → 전부 반영 |
| Tier 3 Hero | H1~H3 · F1~F5 | 8/8 완주 | BLOCKER 0 · MAJOR 2 · MINOR 7 → 전부 반영 |
틀린 문장은 고치고, 숫자는 다시 쟀습니다
따라 해 보니 스택은 어디에도 없었습니다. 미들웨어가 unhandled_exception을 스택과 함께 로그로 남기고, trace 내보내기에 exception event를 싣도록 코드를 고쳐 실제로 남는 것을 확인했습니다.
localhost가 효과를 묻었다Windows에서 curl이 IPv6부터 시도해 연결마다 약 0.2초가 붙었습니다. 큐 6ms vs 인라인 504ms의 교훈이 278ms vs 708ms로 흐려졌습니다 → 모든 HTTP 주소를 127.0.0.1로.
같은 데이터인데 STATUS 히스토그램이 생긴 뒤 옵티마이저가 복합 인덱스를 골랐습니다. 다시 재서 10 → 7 Buffers로 고치고, “계획은 통계에 따라 갈린다”를 본문과 자가점검의 ‘답이 갈리는 지점’에 넣었습니다.
migrate reviews 0001 뒤 시드가 ORA-00904로 실패했습니다. 시드가 DB에 실제로 적용된 마이그레이션 시점의 모델로 넣도록 고치고, 깨끗한 상태에서 처음부터 다시 돌려 확인했습니다.
읽은 분량이 아니라 설명할 수 있는 것으로
Level 1이 “이 코드가 DB에 무엇을 보내는가”를 재는 과정이었다면, Level 2는 “운영 중에 이것이 깨지면 어떻게 알고, 어떻게 되돌리는가”를 직접 깨뜨려 보며 배우는 과정입니다. 금융 서비스를 기준으로 한 10단계 실무 로드맵을 따라갑니다.
각 단계가 Level 1의 어디에서 이어지는지 — 칩을 누르면 그 Quest로
IP는 임대 자원이다. TTL을 미리 낮추고 옮기는 절차, ACME 갱신과 reload, 중간 인증서 누락, 프록시 뒤의 HTTPS.
CORS는 인가가 아니다. 프리플라이트, credentials와 와일드카드, SameSite·CSRF, 세션 vs JWT의 폐기, 개인화 응답의 캐시 헤더.
워커 수는 메모리와 DB 커넥션에서 역산한다. 타임아웃 사다리는 안쪽일수록 짧게, 배포 중 요청 유실 0.
잔고 UPDATE 대신 이중부기와 두 시간축, 실행계획, 잠금 순서, 무중단 마이그레이션 — PostgreSQL로, Oracle과 대조하며.
스냅샷과 증분을 시퀀스 번호로 잇는다. heartbeat, 느린 소비자와 컨플레이션, 지터로 막는 재연결 폭주.
정확히 한 번은 없다. 멱등 키를 제약으로, DLQ 운영, 아웃박스, 거래일을 인자로 받는 재실행 가능한 마감 배치.
올리는 속도가 아니라 되돌리는 속도. 커밋 SHA 태그, 마이그레이션 선행, 1분 롤백, 기능 플래그.
질문 순서를 갖는다. RED·USE, SLO와 에러 버짓, 원인 분석보다 완화 먼저, 포스트모템, 대사.
신뢰 경계를 그린다. SSRF·IDOR, 컬럼 암호화와 블라인드 인덱스, 조회까지 남기는 감사, 망분리라는 설계 제약.
만들지 않을 것을 고른다. 분리 기준, 하위호환 계약, ADR, 용량·비용 산정, 코드리뷰·온콜.
4단계까지는 순서대로, 5단계(실시간)와 6단계(큐)는 나란히 진행해도 됩니다. 7단계부터는 앞에서 만든 서비스가 실제로 배포돼 있어야 의미가 생깁니다.
Level 2가 겨냥하는 차이
| 질문 | 프레임워크 수준 | 실무 수준 |
|---|---|---|
| 배포는 어떻게 하나 | 푸시하면 플랫폼이 빌드해서 올려 준다 | 이미지 태그를 커밋 SHA로 고정하고, 스키마 마이그레이션을 배포보다 먼저 돌리고, readiness를 통과한 인스턴스만 로드밸런서에 넣고, 실패하면 이전 태그로 되돌린다 |
| CORS 에러가 난다 | 모든 오리진을 허용한다 | credentials 요청엔 *를 쓸 수 없으니 허용 목록의 오리진만 반사하고, Vary: Origin으로 CDN이 다른 오리진의 응답을 재사용하지 않게 한다 |
| 응답이 느리다 | 캐시를 붙인다 | 평균이 아니라 p99를 보고, 트레이스로 DB·외부 API·이벤트 루프 대기 중 어느 구간인지 가른 뒤, 쿼리라면 실행계획으로 확인한다 |
완성 기준은 측정치로 — Level 2의 목표입니다
Level 1과 나란한 별도 저장소로 공개돼 있습니다. Level 2(실무 — 웹 백엔드)와 Level 3(운영 — 규모·신뢰성)까지, Level 1의 Tier 1을 끝냈다면 바로 이어 갈 수 있습니다.
준비물을 확인하고, 저장소를 받아 Quest 00 스모크 테스트부터 돌려 봅니다.
gvenzl/oracle-free:23.26.2-slim — docker ps에 ora-hanorder가 Up127.0.0.1:1521/FREEPDB1 · 실습 계정 sqlp (Windows의 localhost는 IPv6를 먼저 시도해 연결마다 늦을 수 있음)sqlp에 SELECT_CATALOG_ROLE — v$sql·실행계획 조회용# 0) 저장소 받기
git clone https://github.com/voidmain443/backend_dev_learning_guide.git
cd backend_dev_learning_guide
# 1) 오라클 컨테이너 확인 — ora-hanorder, 0.0.0.0:1521->1521
docker ps
# 2) 가상환경 + 의존성
python -m venv .venv
source .venv/Scripts/activate # mac/linux: source .venv/bin/activate
pip install -r 0_환경_전제/app/requirements.txt
# 3) Quest 00 스모크 테스트 (프레임워크 없이 순수 드라이버)
cd 0_환경_전제/app
cp .env.example .env
python smoke_oracledb.py # DEPT 4건 + v$sql SQL_ID 상관
# 4) FastAPI 서버
python -m uvicorn main:app --port 8000
# → http://127.0.0.1:8000/health · /dept
명령은 Git Bash 기준입니다. Windows에서 한글 출력이 깨지면 export PYTHONIOENCODING=utf-8(PowerShell은 $env:PYTHONIOENCODING='utf-8')을 먼저 실행하세요. 주소는 localhost 대신 127.0.0.1 — Windows는 IPv6를 먼저 시도해 연결마다 늦어집니다. 접속 정보는 .env에만 두고 커밋하지 않습니다.