LEVEL 1기초 백엔드 · 수업 교재 커리큘럼 안내서

DB를 꿰뚫는
백엔드 엔지니어링

SQL·실행계획·Lock을 이미 아는 사람이, 그 위에 백엔드를 한 층씩 직접 쌓아 올리는 과정입니다. FastAPI와 Django, Oracle 26ai, Docker로 동작하는 주문 백엔드를 만들며, 모든 프레임워크 동작을 “오라클에 어떤 SQL이, 몇 번, 어떤 실행계획으로 나가는가”로 풀어 봅니다. 결론은 전부 직접 잰 숫자로 남깁니다.

35
Quest
T1 15 · T2 12 · T3 8
3
Tier
세 번, 더 깊이
전→후
성능 개선은
숫자로 증명
3회
처음부터 따라 한
학생 검증
LEVEL 1기초 · 원리를 측정으로지금 여기 LEVEL 2실무 · 운영하고 깨뜨리고 복구공개 LEVEL 3운영 · 규모와 신뢰성공개
Order Service의 계층위 = 사용자 · 아래 = DB
  1. HTTP
    클라이언트 요청curl · httpx · 브라우저 /docs
  2. APP
    FastAPI라우팅 · Pydantic 검증 · 인증 · 미들웨어
  3. ORM
    SQLAlchemy 2.0“내가 안 짠 SQL”이 생기는 곳
  4. DRIVER
    python-oracledb (thin)커넥션 풀 = 오라클 세션
  5. DB
    Oracle 26ai Freev$sql · 실행계획 · Buffers
DB에서 출발해 한 층씩 올리고, 위에서 보낸 SQL은 다시 DB에서 확인합니다.
01 · Overview

이 과정은 무엇이 다른가

대부분의 백엔드 강좌는 DB를 블랙박스로 둡니다. 이 과정은 정반대에서 출발합니다. DB를 SQLP 수준으로 아는 사람에게, 그 지식이 백엔드의 어느 지점에서 무기가 되는지를 Quest마다 직접 재서 보여 줍니다.

01

프레임워크 = SQL 생성기 + 커넥션 관리자

ORM과 프레임워크가 하는 일을 “어떤 SQL을 몇 번 보내고, 커넥션을 어떻게 빌려 쓰는가”로 봅니다. 객체 코드를 짤 때도 머릿속에 SQL이 보이게 만드는 것이 목표입니다.

02

측정으로 증명한다

추측은 금지입니다. 성능을 다루는 Quest는 개선 전(naive)과 후의 숫자를 표로 남겨야 통과합니다. 실행계획의 Buffers와 요청당 발행 SQL 수, 두 개의 자를 씁니다.

03

직접 손으로 빌드한다

완성본을 읽는 대신 빈 파일에서 한 블록씩 쌓습니다. 블록마다 어디에 넣는지, 무엇이 켜지는지, 바로 돌려서 무엇을 확인하는지를 따라갑니다.

04

하나의 누적 프로젝트

주문 백엔드(Order Service) 하나가 Quest를 따라 진화합니다. 상품 조회에서 시작해 주문·재고·인증·큐·배포까지, 앞 결과물에 살을 붙여 나갑니다.

★ 이 과정의 정체성

앱 요청 ↔ 오라클 SQL_ID 상관

“이 느린 HTTP 요청이 실행한 그 SQL은 오라클에서 어떤 실행계획과 Buffers였나?” 앱 trace와 오라클 v$sql을 SQL_ID로 잇는 법을 Quest 00에서 처음 맛보고, Quest 07에서 OpenTelemetry로 정식으로 만들며, Quest 14 부하 분석에서 실전에 씁니다.

HTTP 요청→trace · span→앱이 계산한 SQL_ID→v$sql→실행계획 · Buffers

Level 1을 마치면 — 말이 아니라 숫자로

모두 이 교재의 검증 환경에서 잰 값입니다

  1. 201 → 3N+1을 잡는다주문 100건 화면이 보내는 SQL을 왕복 수로 설명하고 3개로 줄인다Q04
  2. 오버셀 0동시 쓰기를 지킨다재고 5에 동시 주문 20건 — 성공 5 · 품절 15 · 최종 재고 0Q02
  3. SQL_ID느린 요청을 DB까지 따라간다요청 → trace → 계산한 SQL_ID → v$sql → 실행계획Q07
  4. 38 → 300+병목을 3계층으로 가른다앱·풀·DB로 나눠 재고, 진짜 병목(요청당 101번 왕복)을 고쳐 처리량 38 → 300+ req/s · p95 약 800 → 100msQ14
  5. 200 · 137내려갈 때도 요청을 지킨다SIGTERM엔 처리 중 요청이 끝까지 200, SIGKILL엔 끊기고 종료 코드 137Q13
  6. 590 req/s돈이 오가는 API를 증명한다복식부기 원장 결제 API — 부하 아래 데드락 0 · 중복 결제 0 · 불변식 위반 0F5

누구를 위한 과정인가

DB는 아는데 서버는 처음인 사람

이미 알고 있다고 가정하는 것 전제

  • 고급 SQL 작성과 실행계획 읽기
  • 인덱스·조인 튜닝의 기본 — 풀스캔이 나쁠 때와 좋을 때
  • 트랜잭션 격리수준·Lock, 오라클 undo 기반 MVCC
  • GATHER_PLAN_STATISTICS로 전/후 Buffers 비교
  • Docker로 띄운 오라클 컨테이너와 실습 데이터셋 운용

이 과정에서 채우는 것 새로 배움

  • 웹 계층 — HTTP·REST·인증·보안
  • 앱↔DB 경계 — ORM이 만드는 SQL, N+1, 커넥션 풀, 세션
  • 비동기 I/O 모델 — 이벤트 루프와 스레드풀
  • 배포·운영 — 컨테이너, 헬스체크, CI, 관측성
  • 도메인 설계 — 이커머스·핀테크 백엔드 (Tier 2·3)

DB가 처음이라면 SQLP(오라클 SQL 튜닝) 과정을 먼저 진행하세요. 이 과정은 오라클 도커와 실습 데이터셋이 이미 갖춰져 있다고 가정합니다.

02 · Infrastructure

실습 인프라와 기술 스택

새 인프라를 크게 늘리지 않습니다. 이미 띄워 둔 Oracle 26ai Free 컨테이너 위에 앱 계층만 올리고, 노트북 한 대에서 개발·측정·배포·CI까지 전 과정을 돌립니다.

과정에서 다루는 인프라 주제

칩을 누르면 해당 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.

테스트와 CI

실 오라클 위의 롤백 격리 테스트, Alembic 마이그레이션, GitHub Actions로 린트·타입·테스트·빌드를 자동 검증합니다.

부하와 용량

p50/p95와 처리량, 풀을 키우면 기다림이 어디로 옮겨 가는지, 요청 시간을 앱·풀·DB 3계층으로 쪼개 재는 법(Server-Timing).

12GB사용자 데이터
2GBRAM (SGA+PGA)
2CPU
제약이 곧 설계 결정

Oracle 26ai Free의 한도 안에서 설계합니다

풀을 키우면 오히려 느려지는 현상(Q05), 상시 브로커 대신 DB 큐를 쓰는 이유(Q10), 데이터셋을 필요한 것만 적재하는 운영 습관까지 — 작은 한도가 현실의 용량 계획을 미리 연습시킵니다.

기술 스택

설치 직전 공식 문서로 버전을 고정합니다

영역선택메모
언어Python 3.12+검증 환경 3.13
웹 프레임워크FastAPI · Starlette · Pydantic v2Tier 1의 주축. 진짜 async I/O의 주체
ORMSQLAlchemy 2.0비동기는 oracle+oracledb_async (2.0.25+)
오라클 드라이버python-oracledb (thin 모드)pip install oracledb · cx_Oracle 대체 · Instant Client 불필요
데이터베이스Oracle 26ai Freegvenzl/oracle-free:23.26.2-slim · PDB FREEPDB1
병행 프레임워크Django 6.0 또는 5.2 LTSQuest 08에서 첫 투입, Tier 2에서 본격 대조
마이그레이션Alembic · Django migrations무중단(expand-contract)은 Tier 2
테스트pytest · httpx (TestClient)단위·통합·E2E·Mock, 롤백 격리
컨테이너Docker · docker compose앱 컨테이너 → 호스트 오라클
CIGitHub Actionsruff → mypy → pytest → build

검증 환경 패키지: fastapi 0.138 · sqlalchemy 2.0.51 · oracledb 4.0.1 · pydantic 2.13 · uvicorn 0.49

실습 데이터셋

누적 프로젝트의 기준 스키마는 데이터셋 A 하나입니다

데이터셋내용쓰임
A · sqlpPRODUCTS 2천 · CUSTOMERS 2만 · ORDERS 20만Tier 1 전 구간, 주문 백엔드의 기준(write) 스키마
B · sqlpT_ORD_BIG 100만 건, 인덱스 없음대용량·풀스캔 비용 체감 (선택)
C · D · E튜닝 실습 · 유통 바스켓 · 이커머스 30테이블Tier 2 도메인 탐구용 read-only 데이터
03 · How to learn

한 Quest를 공부하는 방법

각 Quest 폴더는 문서 5종과 실행 가능한 app/으로 이루어집니다. 아래 순서대로 읽고, 만들고, 돌리고, 재고, 확장합니다.

  1. ① 먼저 통독 가이드.md

    목표·핵심 개념·체크리스트·함정·심화 화두. 이 Quest가 왜 필요한지부터.

  2. ② ★ 메인 경로 빌드_한줄씩.md

    빈 파일에서 한 블록씩 쌓습니다. 블록마다 바로 실행해서 효과를 확인합니다.

  3. ③ 돌려서 대조 실습.md

    실행 명령과 검증 환경에서 나온 실제 출력. 내 결과와 비교합니다.

  4. ④ ★ 숫자로 증명 측정_전후.md

    전(naive)→후(개선) 측정표. 발행 SQL 수·Buffers·p95를 남깁니다.

  5. ⑤ 스스로 확장 심화_연습.md

    🟢 기본 · 🟡 응용 · 🔴 심화 난이도별 도전 과제. 답도 측정으로 증명합니다.

app/그 Quest 시점의 동작하는 참조 구현(스냅샷). 베끼지 말고, 막혔을 때 대조용으로 씁니다.

“한 줄씩 빌드”는 이렇게 생겼습니다

Quest 02 · 오버셀을 막는 한 줄

02_쓰기_트랜잭션 / 빌드_한줄씩.md · 파트 5
[어디에]

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   → 정상

모든 블록은 세 단계

어디에 넣는지(파일과 위치) → 그 블록이 무엇을 켜는지(한 단락) → 바로 돌려서 효과를 확인. 한 블록 쓰고, 돌려 보고, “왜 이 모양인지” 말할 수 있을 때 다음 블록으로 넘어갑니다.

가이드.md의 구성

원본 WebDevCurriculum의 Quest 구조를 계승했습니다.

🎯 목표Topics✅ 체크리스트🧩 과제🧠 멘탈 모델⚠️ 함정 박스🔬 심화 화두🔗 다음 Quest 연결자가진단

소크라테스식 체크리스트

각 Quest의 질문에 왜 → 어떻게 → 장단점·대안 순서로, 내 언어로 답합니다. 답변은 산출물로 남겨 면접 답변처럼 자가 점검에 씁니다.

측정은 두 레벨로

성능을 다루는 Quest에서 필수

DB LEVEL · 논리 I/O

실행계획과 Buffers

쿼리를 실제로 실행한 뒤 실행계획을 떠서 Buffers, A-Rows 대 E-Rows, 실제 계획을 봅니다.

SELECT /*+ GATHER_PLAN_STATISTICS */ …;
SELECT * FROM TABLE(
  DBMS_XPLAN.DISPLAY_CURSOR(
    format => 'ALLSTATS LAST'));
APP LEVEL · 왕복

요청당 발행 SQL 수

SQLAlchemy 이벤트로 한 요청이 DB에 몇 번 왕복하는지 셉니다. N+1은 이 숫자로만 잡힙니다.

@event.listens_for(
    engine, "before_cursor_execute")
def _count(*args):
    counter["n"] += 1
TIER 3 · 고도화

부하와 정합성

p95 지연, 앱 trace와 SQL_ID의 상관, 동시성 충돌률. 핀테크 캡스톤에서는 잔액 정합성과 데드락률까지 측정합니다.

테스트(Q08)·API 설계(Q11)처럼 구조와 도구가 주제인 Quest도 느낌 대신 검증 결과를 남깁니다 — 격리는 바깥 커넥션으로 증명하고, 계약은 스크립트로 확인합니다.

자가점검 — 모범답안이 없는 질문

플립러닝: 수업 전에 혼자 설명하고, 수업에서 맞붙이고, 끝나고 우리 답과 견준다

Quest마다 세 질문, 네 수준

① 설명 동료에게 그려 보라 · ② 근거 내 숫자로 답하라 · ③ 판단 조건이 바뀌면 답이 바뀌는가. 답은 1 용어 → 2 원리 → 3 근거(내 측정값) → 4 판단(답을 바꾸는 조건)으로 스스로 채점합니다. 우리와 같은 답이어도 이유를 못 대면 1수준이고, 다른 답이어도 조건을 댔다면 4수준입니다.

“우리의 답변”은 정답이 아니다

질문마다 접힌 우리의 답변과 답이 갈리는 지점이 있습니다. 예: “느린 대시보드에 인덱스를 더할까?” — Q14에서 우리는 안 한다고 답했습니다. 인덱스로 아낀 건 1~3 버퍼뿐이고 병목은 101번의 왕복이었으니까요. 하지만 그 쿼리가 풀스캔이었다면 정답은 인덱스입니다. 같은 데이터인데 통계가 바뀌자 옵티마이저가 다른 계획을 고른 것도 우리가 직접 겪었습니다.

NEW · 3D 해부도

보이지 않는 설계를 쌓고, 올리고, 이어 보기

교재의 실측 숫자로 움직이는 3D 장면 여섯 개입니다. 요청 한 번이 여덟 층을 지나는 길(N+1 201 SQL → 3), 커넥션 풀의 대기, 잠그는 순서가 만드는 데드락, 해시로 이어지는 원장, 멱등 키와 아웃박스를 직접 눌러 보며 확인합니다.

DBBuffers · 잠금 POOL세션 = 커넥션 APPORM · SQL HTTP요청 · 응답
04 · Curriculum · Tier 1

Tier 1 코어 — 동작하는 백엔드 교재 공개

Quest 00~14, FastAPI 단독으로 측정 가능한 백엔드를 직접 만들어 돌립니다. 이 과정의 필수 완주 구간입니다. 각 Quest를 펼치면 배우는 것, 직접 만드는 것, 측정으로 증명하는 것을 볼 수 있습니다.

1단계 연결의 뼈대 2단계 앱↔DB 경계 3단계 믿을 수 있는 서비스 4단계 동시성·계약·보안 5단계 운영과 종합
S1

연결의 뼈대

오라클에 붙어 한 줄을 읽는 것에서 시작해, ORM이 처음 등장합니다.

00 환경과 전제 점검Environment & Prerequisites 내가 만든 백엔드가 실제 오라클의 한 줄을 읽어 HTTP로 응답하는 것을 눈으로 확인합니다. 인프라·운영데이터·DB SQL_ID첫 상관 추적

배우는 것

  • python-oracledb thin 모드 — Instant Client 없이 오라클 접속
  • 커넥션 풀로 매 요청 connect 비용 없애기
  • 12-factor 설정 — 접속 정보는 코드가 아니라 .env에서
  • FastAPI 최소 앱과 lifespan — 시작 시 풀 생성, 종료 시 정리
  • 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가 쓰는 측정 습관의 시작입니다.

SQLP 접합

세션 생성 비용(v$session)이 바로 커넥션 풀을 쓰는 이유입니다.

생각해 볼 질문

  • 매 요청 connect()를 하면 무엇이 비싼가?
  • thin과 thick 모드는 무엇이 다르고, thick은 언제 필요한가?
다음 → Q01 · 접속·풀·설정은 이후 모든 Quest가 그대로 재사용합니다 교재 폴더 열기 ↗
01 첫 REST 리소스 — ReadFirst REST Resource 상품 2천 건을 SQLAlchemy ORM으로 노출하고, ORM이 대신 만들어 보내는 SQL을 끝까지 추적합니다. 백엔드데이터·DB 3 vs 19buffers · PK vs 풀스캔

배우는 것

  • SQLAlchemy 2.0 — DeclarativeBase · Mapped · Session · select()
  • ORM 모델(DB 표현)과 Pydantic 스키마(API 계약)의 레이어 분리
  • Depends(get_db) — 요청마다 세션을 열고 닫는 의존성 주입
  • .limit()이 오라클 FETCH FIRST로 번역되는 원리(dialect)
  • 세션 1차 캐시(identity map)가 측정을 헷갈리게 하는 지점

직접 만드는 것

  • GET /products(목록·카테고리 필터) · GET /products/{id}(단건·404)
  • 요청당 발행 SQL을 세는 query_counter

측정으로 증명

PK 조회 · INDEX UNIQUE SCAN3 buffers
비인덱스 검색 · TABLE ACCESS FULL19 buffers

SQLP 접합

이미 아는 PK 조회 vs 풀스캔의 차이를, ORM이 만든 SQL 위에서 다시 확인합니다.

생각해 볼 질문

  • ORM 모델과 응답 스키마를 왜 나누나? 합치면 무슨 문제가?
  • .limit(3)은 오라클에서 어떤 SQL이 되나?
다음 → Q02 · Product 모델·세션·query_counter를 그대로 확장합니다 교재 폴더 열기 ↗
S2

앱↔DB 경계

쓰기·대용량·N+1·동시성 — SQLP 지식이 애플리케이션 코드로 내려오는 구간입니다.

02 쓰기·트랜잭션·멱등성Write · Transaction · Idempotency 주문 생성과 재고 차감을 한 트랜잭션으로 묶고, 동시 요청의 오버셀과 중복 요청을 막습니다. 백엔드데이터·DB −2 → 0최종 재고 · 동시 20요청

배우는 것

  • 트랜잭션 경계 — ORM commit()/rollback()과 오라클 트랜잭션
  • check-then-act 경쟁 상태와 갱신 손실(lost update)
  • 비관적 락 SELECT … FOR UPDATE와 낙관적 대안(UPDATE … WHERE stock >= qty)
  • 멱등 키 — UNIQUE 제약과 위반 처리로 “정확히 한 번”

직접 만드는 것

  • 무방비 POST /orders/naive와 안전한 POST /orders/safe — 딱 한 줄 차이
  • 재고 5개에 동시 20요청을 쏘는 concurrent_test.py — 행 락 대기(v$system_event)까지 측정
  • Idempotency-Key 헤더 + UNIQUE 제약으로 중복 요청 흡수

측정으로 증명

naive · 락 없음주문 7 · 재고 −2
safe · FOR UPDATE주문 5 · 재고 0
정확성의 비용 · 행 락 대기190회 · 9.9s
같은 Idempotency-Key 동시 20회주문 1건

SQLP 접합

READ COMMITTED · 행 락 · TX enqueue 대기 · 데드락을 이제 내 코드가 긋는 트랜잭션 경계에서 만납니다.

생각해 볼 질문

  • 락 없이도 UPDATE는 행을 잠그는데, 왜 오버셀이 날까?
  • 같은 멱등 키를 가진 요청 두 개가 동시에 오면?
다음 → Q03 · 쓰기 테이블과 멱등 패턴은 Tier 3 핀테크 원장의 뿌리가 됩니다 교재 폴더 열기 ↗
03 페이지네이션·정렬·필터Pagination at Scale 주문 20만 건을 페이지로 나누고, 깊은 페이지일수록 비싸지는 OFFSET 방식을 키셋(커서) 방식으로 고칩니다. 백엔드데이터·DB 1417 → 5buffers · 283배

배우는 것

  • 오프셋 페이지네이션 OFFSET n ROWS FETCH NEXT m의 비용
  • 키셋 페이지네이션 WHERE id > :after — 깊이와 무관한 비용
  • 커서 키가 유일·불변이어야 하는 이유, 오프셋이 “흔들리는” 문제
  • “총 N페이지”를 위한 COUNT(*)의 숨은 비용
  • 필터 선택도와 인덱스 — 무인덱스 STATUS vs 인덱스 CUST_ID

직접 만드는 것

  • GET /orders(오프셋)와 GET /orders/keyset(키셋)을 나란히
  • 실행계획 비교 스크립트 measure_q03.py
  • (선택) 100만 건 무인덱스 테이블로 풀스캔 비용 체감

측정으로 증명

깊은 오프셋 · page 90011417 buffers
키셋 · after_id5 buffers
wall-clock — 작아 보이는 함정4ms→25ms
100만 건 무인덱스 · 깊은 OFFSET5,826 buffers · 정렬 70MB

SQLP 접합

Top-N · WINDOW NOSORT STOPKEY · INDEX RANGE SCAN. 페이징 튜닝 지식이 API 설계 결정으로 바뀝니다.

생각해 볼 질문

  • OFFSET 180000은 DB에게 무엇을 시키나?
  • 키셋은 왜 “5페이지로 점프”를 못 하나? 어떤 UI에 어울리나?
다음 → Q04 · 이 목록 응답이 N+1의 무대가 됩니다 교재 폴더 열기 ↗
04 N+1과 ORM의 진실The N+1 Problem 주문 목록에 고객명·상품명을 붙이는 순간 ORM이 몰래 보내는 수백 개의 쿼리를 잡아 1~3개로 줄입니다. 백엔드데이터·DB 201 → 3 → 1요청당 발행 SQL

배우는 것

  • 관계(relationship) 매핑과 lazy 로딩 — “점(.) 하나가 쿼리 하나”
  • 즉시 로딩 selectinload(IN 리스트) vs joinedload(JOIN)
  • identity map이 N+1을 줄이기도, 숨기기도 하는 이유
  • 응답 직렬화에서 조용히 터지는 N+1
  • 심화: GraphQL DataLoader가 흩어진 조회를 배치하는 원리

직접 만드는 것

  • 같은 결과를 내는 세 엔드포인트 /orders-with-names/naive · /selectin · /joined
  • 세 전략의 SQL 수와 시간을 비교하는 measure_q04.py

측정으로 증명

lazy 로딩 (N+1)201 SQL · 왕복 202 · 약 117ms
selectinload3 SQL · 왕복 6 · 약 5ms
joinedload1 SQL · 왕복 2 · 약 1.8ms

SQLP 접합

추가 쿼리 하나하나는 3 buffers짜리 PK 조회라 “쿼리 하나 튜닝”으로는 안 잡힙니다. 문제는 개수와 왕복입니다.

생각해 볼 질문

  • N+1의 “1”과 “N”은 코드의 어느 줄이 만드나?
  • 일대다 관계에 joinedload를 쓰면 무슨 일이 생기나?
다음 → Q05 · 이 쿼리들이 커넥션 풀 위에서 어떻게 도는지로 넘어갑니다 교재 폴더 열기 ↗
05 커넥션 풀·캐시Connection Pool & Cache 동시 요청에서 풀 크기가 처리량을 어떻게 가르는지 재고, 캐시로 SQL을 0으로 만드는 대가(불일치)를 직접 봅니다. 인프라·운영데이터·DB ×6.7처리량 · 풀 1 → 8

배우는 것

  • 풀 크기 ↔ 오라클 세션·processes, DRCP 개념
  • 2 CPU 한도에서 풀을 키우면 처리량은 평탄해지고 쿼리 하나가 느려지는 이유
  • 큰 풀을 게으르게 만들 때의 커넥션 폭풍과 워밍업
  • cache-aside 패턴, TTL, 적중과 미스
  • 쓰기 후 무효화와 불일치 윈도우(stale window)
  • 인프로세스 캐시의 한계 → 외부 캐시(Redis)

직접 만드는 것

  • TTL 캐시와 cache-aside 서비스 계층 · GET /stock/{id} · 재고 조정 API
  • 풀 크기별 처리량 벤치마크 pool_bench.py

측정으로 증명

풀 1 → 8 → 32266→1,789→1,815 q/s
p95 · 풀 1 → 8693→79ms
풀 32 · 커넥션을 미리 안 만들면−47% 처리량
캐시 적중SQL 0
무효화를 빠뜨리면stale 읽기

SQLP 접합

세션·논리 I/O·읽기 일관성이 애플리케이션 설계의 손잡이로 올라옵니다.

생각해 볼 질문

  • 풀을 무한히 키우면 왜 처리량은 그대로인데 p50은 느려지나?
  • uvicorn 워커가 4개면 캐시는 어디에 사나?
다음 → Q06 · 매 요청 인증이 추가하는 “숨은 사용자 조회”를 측정합니다 교재 폴더 열기 ↗
S3

믿을 수 있는 서비스

누가 요청했는지 알고, 요청이 스스로 비용을 말하게 하고, 그 계약을 테스트로 못박습니다.

06 인증·인가Authentication & Authorization bcrypt 로그인, 액세스·리프레시 토큰, 역할 기반 접근 제어를 만들고, 인증 방식이 요청당 SQL을 어떻게 바꾸는지 측정합니다. 백엔드보안 +0 vs +1인증 SQL / 요청

배우는 것

  • 비밀번호 해싱 — bcrypt의 솔트와 “일부러 느린” 코스트
  • JWT의 서명·클레임·만료, 그리고 “서명됐을 뿐 암호화는 아니다”
  • 무상태 검증 vs DB 확인 인증의 트레이드오프
  • 액세스(짧게)·리프레시(길게) 분리, 회전과 재사용 탐지
  • 401과 403, 역할 기반 인가(RBAC)

직접 만드는 것

  • 로그인 → /me → /admin(403) → 리프레시 회전 → 로그아웃 흐름
  • 리프레시 토큰을 DB에 기록하는 토큰 테이블 · 로그아웃용 블랙리스트 테이블

측정으로 증명

무상태 검증 /me/stateless인증 SQL 0
DB 확인 /me/dbcheck요청당 +1
로그아웃 후 같은 토큰무상태 200 · DB확인 401
회전된 리프레시 재사용401 · 그 사용자 전체 폐기

SQLP 접합

Q05의 “캐시 무효화 vs 정합성”과 같은 고민입니다 — 즉시 차단하려면 DB를 봐야 하고, 그만큼 SQL이 늡니다.

생각해 볼 질문

  • JWT는 왜 즉시 로그아웃·차단이 어려운가?
  • 리프레시 회전은 어떤 공격을 막나?
다음 → Q07 · 요청당 총 SQL을 로깅과 관측으로 끌어올립니다 교재 폴더 열기 ↗
07 검증·에러·관측성Validation · Errors · Observability 입력 검증, 일관된 에러, 구조화 로그를 깔고 — 각 HTTP 요청이 실행한 SQL을 오라클 v$sql에서 SQL_ID로 되찾습니다. 백엔드인프라·운영데이터·DB 요청 → SQL_ID★ 킬러 기능

배우는 것

  • Pydantic 제약으로 문 앞에서 422 검증
  • 예외 핸들러로 통일한 에러 봉투 {error:{code, message, request_id}}
  • JSON 구조화 로그 — request_id · sql_count · dur_ms · slow
  • contextvars로 요청별 상태 추적, X-SQL-Count 응답 헤더
  • OpenTelemetry trace — 요청 span 아래 SQL span, 그 span에 db.oracle.sql_id
  • 앱이 SQL을 보내기 전에 SQL_ID를 계산(MD5 → base32)해 v$sql을 바로 집기
  • 대조: SQL에 /* req=<id> */를 새기는 방식의 하드 파싱 비용

직접 만드는 것

  • 요청 ID 미들웨어와 SQL 카운트용 DB 훅
  • 일부러 무거운 GET /report/orders와 상관 데모 demo_q07.py

측정으로 증명

trace → SQL_ID → v$sql텍스트 검색 없이 바로
그 SQL의 비용1,206 buffers · FULL 200K행
요청 50개 · 주석 각인 vs trace하드 파싱 50→0

SQLP 접합

요청마다 SQL 텍스트가 달라지면 커서 공유가 깨지고 하드 파싱이 늡니다. 그래서 텍스트는 그대로 두고, SQL_ID를 trace 쪽에 붙여 상관시킵니다.

생각해 볼 질문

  • request_id를 SQL에 새기면 무엇이 가능해지고, 대가는 무엇인가? trace는 그 대가를 어떻게 피하나?
  • 평문 로그 대신 JSON 로그가 검색·집계에 유리한 이유는?
다음 → Q08 · 검증·에러·SQL 수라는 계약을 테스트로 못박습니다 교재 폴더 열기 ↗
08 테스트·마이그레이션 + DjangoTesting & Migrations 지금까지의 계약을 자동화 테스트로 못박고, 스키마 변경은 Alembic으로 안전하게 — 그리고 Django를 처음 들입니다. 백엔드인프라·운영 (100, 0)격리 증명 · 바깥에서 본 값

배우는 것

  • 테스트 피라미드 — 단위 · 통합 · E2E · Mock
  • pytest와 httpx TestClient, 의존성 오버라이드
  • 롤백 격리(SAVEPOINT · begin_nested) vs 트런케이트
  • Alembic 수동 리비전과 upgrade/downgrade — 공유 스키마에서 autogenerate 금지
  • 오라클 함정 — 빈 문자열 = NULL
  • Django 첫 투입 — 오라클 백엔드, managed=False 모델, admin

직접 만드는 것

  • 실 오라클에서 도는 단위·통합·E2E·Mock 테스트 12개와 멱등 시드 seed.py
  • 컬럼을 추가하고 되돌리는 마이그레이션
  • 같은 오라클을 읽는 최소 Django 프로젝트

측정으로 증명

롤백 격리 픽스처 · 바깥 커넥션이 본 (재고, 주문)(100, 0)
격리 없는 세션으로 같은 일(6, 1) 누수
오라클 note='' 저장 후 조회None (빈 문자열 = NULL)

SQLP 접합

테스트마다 트랜잭션을 열고 되돌리는 롤백 격리는 SAVEPOINT 지식 그대로입니다.

생각해 볼 질문

  • 서비스 코드가 commit()해도 테스트가 롤백되게 하려면?
  • FastAPI와 Django는 같은 오라클을 두고 무엇이 다른가?
다음 → Q09 · Django(스레드풀)와 FastAPI(진짜 async)를 나란히 비교합니다 교재 폴더 열기 ↗
S4

동시성·계약·보안

async의 진실, 요청 흐름 밖으로 일을 빼는 큐, 깨지지 않는 API 계약, 그리고 공격에 대한 방어.

09 동기 vs 진짜 비동기Sync vs True Async 같은 0.1초 DB 대기를 동기·진짜 비동기·async 안의 블로킹으로 돌려, async가 언제 이득인지 측정합니다. 백엔드인프라·운영 10.2s vs 1.1sasync 안의 블로킹 · 동시 100

배우는 것

  • 이벤트 루프 vs 스레드풀 — async def와 def가 각각 도는 곳
  • 진짜 async DB — python-oracledb async(create_pool_async)
  • 안티패턴: async def 안의 동기 호출, 그리고 asyncio.to_thread
  • Django async 뷰의 ORM = sync_to_async 워커 스레드, async 트랜잭션 미지원
  • 동시성은 결국 커넥션 풀(=세션)에 묶인다

직접 만드는 것

  • /sync · /async · /async_blocking · /async_offload와 같은 일을 하는 Django 동기·async 뷰
  • 실행 스레드 이름으로 구조를 확인하는 thread_demo.py, 부하 도구 loadtest.py

측정으로 증명 · 동시 100요청 · 풀 10

async 안의 블로킹 · 세션 최대 1개10.21s · 직렬화
/async · /sync · Django · to_thread1.06~1.10s
같은 일에 쓴 스레드async 1 · sync 40 · Django 100

SQLP 접합

async든 sync든 동시성의 상한은 오라클 세션 수입니다. async의 이점은 처리량보다 적은 스레드와 확장성입니다.

생각해 볼 질문

  • async 엔드포인트에서 동기 DB를 부르면 왜 오히려 느려지나?
  • Django async 뷰의 ORM 쿼리는 진짜 async인가?
다음 → Q10 · 무거운 일을 요청 흐름에서 떼어 냅니다 교재 폴더 열기 ↗
10 배경작업·큐Background Jobs & Queue 무거운 후처리를 큐에 넣고 202로 곧장 응답합니다. 브로커 없이 DB 잡 테이블과 SKIP LOCKED로 견고한 큐를 만듭니다. 백엔드인프라·운영데이터·DB 504 → 6ms응답 시간

배우는 것

  • 생산자 · 잡 테이블 · 소비자 패턴, 202 Accepted
  • FOR UPDATE SKIP LOCKED로 여러 워커가 경합 없이 분담
  • at-least-once 배달과 소비자 멱등(job_id 키)
  • DB 큐 vs Redis·Kafka — 2GB RAM 한도에서의 선택
  • 오라클 함정: FETCH FIRST와 FOR UPDATE는 한 문장에 못 쓴다

직접 만드는 것

  • POST /orders(큐잉)와 POST /orders-inline(인라인) 비교
  • 워커 worker.py, 분담·멱등 측정 measure_q10.py

측정으로 증명

인라인 처리약 504ms
큐잉 후 202약 6ms
워커 1 → 2 · 6잡1.85→0.95s · 3:3 분담
SKIP LOCKED claim이 잠근 행2→1 (prefetch)

SQLP 접합

행 락 · SKIP LOCKED · status 인덱스가 큐의 재료입니다. 이미 아는 Lock 지식의 응용입니다.

생각해 볼 질문

  • 워커가 처리 후 DONE 표시 전에 죽으면 어떻게 되나?
  • SKIP LOCKED 없이 FOR UPDATE만 쓰면?
다음 → Q11 · 202를 포함한 엔드포인트들의 API 규약을 정리합니다 교재 폴더 열기 ↗
11 API 설계·버저닝·문서화API Design & Versioning 엔드포인트를 일관된 규약으로 정리하고, v1·v2를 공존시켜 깨는 변경을 다루며, OpenAPI 문서를 자동 생성합니다. 백엔드 v1 ↔ v2공존 · Deprecation·Sunset

배우는 것

  • 페이지 envelope · 에러 봉투 · 상태코드 · 네이밍 규약
  • 버저닝 위치 — URL 경로 vs 헤더 vs 미디어타입
  • 깨는 변경(개명·삭제·타입 변경) vs 안전한 변경(추가)
  • Deprecation·Sunset 헤더와 OpenAPI deprecated
  • /docs · /redoc · /openapi.json — 문서가 곧 계약

직접 만드는 것

  • /v1/products(offset, 구 필드명, deprecated)와 /v2/products(cursor, 새 필드명)
  • 계약 검증 스크립트 verify_q11.py

계약 검증

v1 응답 헤더Deprecation · Sunset · Link
422 · 404 · 405 · 500같은 에러 봉투 + request_id
OpenAPI의 422 스키마실제와 같은 ErrorEnvelope

SQLP 접합

v1 offset → v2 cursor 전환은 Q03 키셋 측정의 근거를 API 계약으로 옮긴 것입니다.

생각해 볼 질문

  • 필드 하나의 이름을 바꾸는 것이 왜 “깨는 변경”인가?
  • 구버전은 어떻게 예고하고 끄나?
다음 → Q12 · 이 API를 보안 관점에서 단단하게 만듭니다 교재 폴더 열기 ↗
12 보안 하드닝Security Hardening SQL 인젝션·IDOR·레이트리밋을 취약 버전과 방어 버전으로 나란히 만들어, 실제로 터뜨려 보고 막습니다. 보안데이터·DB 2000 → 0인젝션 유출 건수

배우는 것

  • SQL 인젝션 — 문자열 조립 vs 바인드 변수
  • 인가 경계(BOLA/IDOR) — 객체 수준 소유권 검사
  • 레이트리밋 알고리즘 — 고정 윈도우 · 슬라이딩 · 토큰 버킷, 429
  • 비밀관리 — 환경변수, .env 커밋 금지, 로그·응답 마스킹
  • HTTPS/TLS와 종단 위치 — 앱 vs 리버스 프록시

직접 만드는 것

  • 취약/안전 쌍 엔드포인트 — /search/vuln·/search/safe, /orders/{id}/vuln·/safe
  • 공격을 재현하는 시연 스크립트 verify_q12.py

측정으로 증명

문자열 조립 + ' OR '1'='12000건 유출
바인드 변수0건
상품명 검색 50번 · 리터럴 vs 바인드하드 파싱 50→0
남의 주문 조회 · 폭주 요청403 · 429

SQLP 접합

인젝션 방어 = 바인드 변수, 커서 공유와 하드 파싱 회피의 바로 그 도구입니다. 보안과 성능이 같은 도구로 해결됩니다.

생각해 볼 질문

  • 바인드 변수가 보안과 성능에 동시에 좋은 이유는?
  • 로그인만 했으면 남의 주문을 봐도 되는가?
다음 → Q13 · 비밀 주입과 TLS 종단은 배포로 이어집니다 교재 폴더 열기 ↗
S5

운영과 종합

안전하게 교체 가능한 형태로 배포하고, 부하 아래에서 진짜 병목을 측정으로 찾습니다.

13 컨테이너·배포·CI·헬스체크Containers · Deploy · CI 앱을 이미지로 묶어 호스트 오라클에 붙이고, 헬스체크·graceful shutdown·CI를 갖춰 안전하게 교체 가능한 형태로 만듭니다. 인프라·운영 200 vs 137graceful vs SIGKILL

배우는 것

  • Dockerfile 레이어 캐시(의존성 먼저), .dockerignore, thin 모드의 가벼운 이미지
  • docker compose와 host.docker.internal로 호스트 오라클 연결
  • liveness vs readiness — DB가 안 닿으면 503으로 트래픽 차단
  • graceful shutdown — SIGTERM → 신규 차단 → 처리 중 요청 드레인 → 풀 정리, exec 형식 CMD(PID 1)
  • grace 정렬 — uvicorn 드레인 상한 < 컨테이너 stop_grace_period
  • CI 게이트 순서 — ruff → mypy → pytest → build (GitHub Actions)

직접 만드는 것

  • /healthz/live · /healthz/ready를 갖춘 앱과 Dockerfile · docker-compose.yml
  • CI 워크플로, 미커밋 트랜잭션 실험 crash_demo.py

측정으로 증명

처리 중 요청 + docker stop200 · exit 0
docker kill · grace 부족 · 셸 형식 CMD응답 없음 · exit 137
코드 1줄 바꾼 뒤 재빌드 (레이어 순서)10.7s→1.4s
DB 불통ready 503 · live 200

SQLP 접합

커밋되지 않은 트랜잭션은 오라클이 롤백합니다 — “반쯤 처리된 주문”이 남지 않는 근거입니다.

생각해 볼 질문

  • liveness와 readiness가 실패하면 오케스트레이터는 각각 무엇을 하나?
  • CI 게이트를 왜 lint → type → test → build 순서로 두나?
다음 → Q14 · 배포된 앱에 부하를 걸어 3계층을 종합 측정합니다 교재 폴더 열기 ↗
14 부하·튜닝 캡스톤Load & Tuning Capstone 일부러 느린 대시보드에 부하를 걸어 요청 시간을 앱·풀·DB 3계층으로 쪼개고, 증상이 아닌 원인부터 고쳐 p95·처리량을 추적합니다. 인프라·운영데이터·DB ×8~10처리량 · N+1 수정

배우는 것

  • 3계층 병목 분해 — Server-Timing 헤더로 요청마다 pool · db · app
  • p50 vs p95, 처리량, 그리고 “가장 큰 대기 = 원인”이 틀리는 이유
  • 측정 도구 총동원 — query_counter · v$mystat · prev_sql_id · 풀 · v$sql elapsed
  • “느리다 → 인덱스·풀 키우기”라는 직관이 틀리는 이유, 포화 지점(GIL) 찾기
  • 심화: 장애 주입, 타임아웃·재시도·서킷브레이커, 백프레셔

직접 만드는 것

  • 일부러 느린 GET /dashboard(naive / eager)
  • 계측 미들웨어(Server-Timing), 측정 measure_q14.py, 부하 도구 loadtest_q14.py

측정으로 증명

앱 · DB 왕복 (지배적)102→6
DB · 인덱스 추가 (아낀 건 1~3 버퍼)10→7~8 buffers
SQL 1개 · 오라클 안 vs 앱이 기다린 시간12µs vs 553µs
부하 · 처리량 / p95 (풀 8)38→300+ req/s · 약 800→100ms
풀 2 → 8 (naive)처리량 1.5배 · 파이썬 1.08코어 포화

SQLP 접합

“느리면 추측하지 말고 측정하라.” 인덱스는 옵티마이저가 쓰지도 않았고, 풀을 키우자 기다림이 GIL로 옮겨 갔을 뿐입니다. 측정이 가리킨 진짜 병목은 101번의 왕복(N+1)이었습니다.

생각해 볼 질문

  • 평균이 아니라 p95를 봐야 하는 이유는?
  • 풀 대기가 가장 큰데, 왜 풀이 원인이 아닌가?
다음 → Tier 1 졸업 · Tier 2 딥다이브 / Tier 3 핀테크 캡스톤으로 교재 폴더 열기 ↗

이 분야에 해당하는 Quest가 없습니다.

05 · Roadmap

세 번, 더 깊이 — 3-Tier 로드맵

SQLP 교재의 합격 → 딥다이브 → Hero 서사를 백엔드로 옮겼습니다. 같은 주문 백엔드를 세 번 관통하며, 매번 한 층 더 깊이 들어갑니다.

TIER 1교재 공개

코어 — 동작하는 백엔드

SQLP의 “합격”에 해당

측정 가능한 백엔드를 직접 만들어 돌립니다. 이 과정의 필수 완주 구간입니다.

범위
Quest 00~14
프레임워크
FastAPI 단독
산출물
가이드 · 빌드 · 실습 · 측정
TIER 2교재 공개

딥다이브 — 원리·대조·운영

SQLP의 “딥다이브”에 해당

내부 원리와 Django 대조, 운영성·아키텍처, 도메인 탐구로 넓고 깊게 들어갑니다.

범위
2A · 2B · 2C
프레임워크
FastAPI + Django 병행
산출물
평행 실습 · 분석노트
TIER 3교재 공개

Hero — 고부하·분산·정합성

SQLP의 “Hero”에 해당

부하·분산·정합성을 측정으로 증명하고, 핀테크 거래 원장 캡스톤으로 마무리합니다.

범위
3A · 3B 캡스톤
프레임워크
도메인별 적합 선택
산출물
p95 · SQL_ID 상관 · 동시성 증명
나선형 반복Tier 1의 각 Quest는 Tier 2에서 “원리 평행 실습”으로, Tier 3에서 “고도화 측정”으로 다시 등장합니다.
Django의 투입 시점Tier 1은 FastAPI 단독. Django는 Q08(admin·마이그레이션)과 Q09(동기 vs 비동기)에서 처음 등장합니다.
졸업 기준선Tier 1 완주는 필수, Tier 2는 권장, Tier 3 핀테크 캡스톤이 최종 정점입니다.

TIER 2딥다이브 — 12 Quest

펼치면 배우는 것 · 잰 숫자 · 끝나면 말할 수 있는 것

2A

Django 평행 실습

Tier 1의 Quest를 Django로 다시 만들어 두 철학을 대조합니다 — 배터리 포함(ORM·admin·migrations) vs 미니멀 + 타입. 같은 오라클, 같은 저울로 잽니다.

D1 첫 리소스와 QuerySetFirst Resource & QuerySet 같은 PRODUCTS·ORDERS를 Django ORM으로 읽고, QuerySet이 언제·어떤 SQL을 보내는지 오라클 쪽에서 확인합니다. T1 Q00·Q01·Q03 450Paginator COUNT(*) · Buffers

배우는 것

  • QuerySet은 게으르고(평가 전 SQL 0) 결과를 캐시한다 — 같은 코드가 어떤 줄에선 SQL을 보내고 어떤 줄에선 안 보낸다
  • get()이 FETCH FIRST 21 ROWS ONLY를 보내는 이유
  • 배터리 포함의 값 — Paginator는 요청마다 COUNT(*)를 얹고 페이지마다 새 커서를 만든다
  • T1 Q03의 키셋이 Django에서도 답이지만, 기본으로 주지는 않는다

Tier 1에서 이어지는 것

측정으로 증명

get(pk=1)FETCH FIRST 21 · 3 Buffers
Paginator 1페이지 (COUNT + 페이지)450 + 5 = 455
Paginator 깊은 페이지 (18만 행 건너뜀)450 + 1,417 = 1,867

끝나면 말할 수 있는 것

QuerySet의 게으름과 캐시를 SQL 개수로 설명하고, get()·exists()·Paginator가 오라클에 실제로 보내는 텍스트와 비용(Buffers·커서 수)을 말할 수 있다.

다음 → D2 · N+1 — select_related·prefetch_related 교재 폴더 열기 ↗
D2 N+1 — select_related·prefetch_relatedN+1 in Django 주문 응답에 고객명·상품명을 붙이는 순간 Django가 몰래 보내는 SQL을 잡고, T1 Q04와 같은 저울에 올립니다. T1 Q04·Q14 201 → 3 → 1주문 100건 · 발행 SQL

배우는 것

  • select_related(JOIN) ≈ joinedload, prefetch_related(IN) ≈ selectinload
  • SQLAlchemy와 숫자가 같은 이유 — 오라클 입장에선 같은 일
  • Django에는 identity map이 없다 — 같은 고객을 주문 수만큼 다시 조회
  • 오라클 IN 1,000개 한도를 넘는 prefetch_related의 실패(Django 6.0.6)

Tier 1에서 이어지는 것

측정으로 증명

naive / prefetch / select · SQL201 / 3 / 1
DB 왕복202 / 6 / 2
같은 고객 주문 10건 · Django vs SQLAlchemy11 vs 2 SQL
관련 키 1,001개 이상 prefetch_relatedORA-01722

끝나면 말할 수 있는 것

같은 화면이 201 → 3 → 1 SQL이 되는 것을 재고, Django에만 있는 두 함정 — identity map이 없다, 오라클 IN 1000개 한도에서 prefetch_related가 깨진다 — 을 설명하고 피할 수 있다.

다음 → D3 · 쓰기·트랜잭션·큐 교재 폴더 열기 ↗
D3 쓰기·트랜잭션·큐Writes, Transactions & Queue T1 Q02의 ‘재고 5에 동시 주문 20건’을 Django로 다시 하고, 기본값 autocommit이 정합성에 무엇을 하는지 잽니다. T1 Q02·Q10 0.09s vs 1.06s조건부 UPDATE vs FOR UPDATE

배우는 것

  • Django의 기본은 autocommit — atomic() 밖의 쓰기는 문장마다 커밋된다
  • 트랜잭션 ≠ 잠금 — atomic()만으로는 오버셀이 남는다
  • obj.stock -= 1; obj.save()는 절대값을 써서 갱신 손실을 만든다 → F()·조건부 update()
  • 큐에서 ORM만으로는 1행만 잠글 수 없다 → 드라이버 커서로 우회, 관리 명령 워커

Tier 1에서 이어지는 것

측정으로 증명

autocommit 중간 실패재고만 100 → 99, 주문 없음
FOR UPDATE · 정확 (락 대기 190회)1.06s
조건부 UPDATE · 정확0.09s
워커 2개 · 잡 6건 분담3 : 3

끝나면 말할 수 있는 것

‘트랜잭션 ≠ 잠금’, ‘save()는 절대값을 쓴다’, ‘autocommit은 실패를 반쯤 남긴다’를 숫자로 설명하고, Django ORM만으로는 큐에서 1행만 잠그기가 안 되는 이유와 우회를 안다.

다음 → D4 · 커넥션·인증·세션 교재 폴더 열기 ↗
D4 커넥션·인증·세션Connections, Auth & Sessions Django의 커넥션 수명(풀 없음·CONN_MAX_AGE·풀)을 서버 종류별로 재고, 로그인된 요청마다 숨어 나가는 SQL을 세션 저장소별로 잽니다. T1 Q05·Q06 +35ms기본값 · 요청마다 logon

배우는 것

  • 기본값 CONN_MAX_AGE=0 = 요청마다 오라클 세션을 열고 닫는다
  • CONN_MAX_AGE는 스레드에 붙는다 — 스레드를 재사용하는 서버에서만 통한다
  • Django 5.2+의 풀은 서버를 가리지 않는다
  • 세션 저장소(db · cached_db · signed_cookies)와 로그아웃 즉시 무효화의 맞교환

Tier 1에서 이어지는 것

측정으로 증명

요청당 logon 비용15 → 50ms
풀 없이 동시 8 · 200건 중 거절42~126
runserver + CONN_MAX_AGE=60 · 세션 최대36~37
로그인된 요청 (db 세션)SQL 2

끝나면 말할 수 있는 것

‘Django는 기본으로 요청마다 오라클 세션을 새로 연다’, ‘CONN_MAX_AGE는 스레드를 재사용하는 서버에서만 통한다’, ‘로그인된 요청 = SQL 2개’를 숫자로 말하고, 세션 저장소를 고를 때 잃는 것을 안다.

다음 → D5 · 테스트·마이그레이션 교재 폴더 열기 ↗
D5 테스트·마이그레이션Tests & Migrations Django 테스트 러너가 오라클에 테스트 DB(= 사용자 + 테이블스페이스)를 만드는 비용을 재고, 배포 중에 깨지는 마이그레이션을 재현합니다. T1 Q08 시간 37배TransactionTestCase vs TestCase

배우는 것

  • 오라클엔 ‘DB 하나 더’가 없다 — Django는 테스트 사용자와 테이블스페이스 둘을 만든다(DBA급 권한 → 전용 사용자)
  • TestCase(savepoint 롤백) vs TransactionTestCase(커밋 + 테이블 비우기)
  • makemigrations가 만드는 오라클 DDL 읽기
  • default=는 DB 기본값을 지운다 → 배포 중 옛 코드의 INSERT가 깨진다

Tier 1에서 이어지는 것

측정으로 증명

테스트 DB 준비 (사용자 + 테이블스페이스)1.2s
TestCase · 11개0.13s · 170 SQL
TransactionTestCase · 같은 11개4.9s · 578 SQL
default= 추가 뒤 옛 코드 INSERTORA-01400

끝나면 말할 수 있는 것

‘Django 테스트는 오라클 사용자를 만든다(DBA급 권한)’, ‘TransactionTestCase는 테스트마다 테이블을 비운다(시간 37배, SQL 3.4배)’, ‘default=는 DB 기본값을 지운다’를 숫자로 설명한다.

다음 → D6 · 관측성·API·보안 교재 폴더 열기 ↗
D6 관측성·API·보안Observability, API & Security 요청마다 실행한 SQL의 SQL_ID를 남기는 미들웨어를 만들고, DRF로 T1의 API 계약(검증·버저닝·페이지네이션·스로틀링)을 다시 세웁니다. T1 Q07·Q11·Q12 42 → 1DRF 페이지당 SQL

배우는 것

  • Django가 보여 주는 SQL(%s)로는 SQL_ID를 못 구한다 → 드라이버 커서의 .statement로
  • DRF 시리얼라이저 한 줄이 만드는 N+1
  • 400 봉투 · 폐기 헤더 · 429 — T1의 계약을 DRF로
  • raw() 인젝션, CSRF의 경계, check --deploy

Tier 1에서 이어지는 것

측정으로 증명

SQL_ID ↔ v$sql드라이버 .statement로 일치
시리얼라이저 N+1 → select_related + 커서42→1
raw() 인젝션 · 리터럴 vs 바인드2000 vs 0행
check --deploy 경고6 → 2

끝나면 말할 수 있는 것

‘Django가 보여 주는 SQL로는 SQL_ID를 못 구한다(%s → :arg0)’, ‘DRF 시리얼라이저 한 줄이 페이지당 SQL 42개를 만든다’, ‘DRF는 익명 POST에 CSRF를 안 본다’를 숫자로 설명한다.

다음 → O1 · 무중단 마이그레이션 교재 폴더 열기 ↗
2B

운영·아키텍처 심화

서비스를 멈추지 않고 바꾸고, 서비스 사이까지 관측하고, 코드에 경계를 긋는 값을 잽니다.

O1 무중단 마이그레이션Zero-downtime Migration 100만 행 테이블에 앱이 쓰는 동안 DDL을 걸어, 어떤 DDL이 앱을 멈추는지 쓰기 지연으로 잽니다. T1 Q08·Q13 498 → 0RENAME vs expand-contract · 에러

배우는 것

  • 오라클 26ai의 ADD COLUMN은 기본값이 있어도 메타데이터만 바꾼다
  • 백필 UPDATE 한 방은 100만 행을 잠그고 redo를 쏟는다 → 키 범위로 나눠 커밋
  • ddl_lock_timeout은 실패를 앱 정지로 바꾼다, 잊힌 트랜잭션 하나가 DDL을 무한히 세운다
  • 롤링 배포 중의 RENAME 대신 expand → migrate → contract

Tier 1에서 이어지는 것

측정으로 증명

ADD COLUMN · 100만 행0.01s
백필 UPDATE 한 방 · 앱 최대 지연약 7s
백필 redo264MB
롤링 배포 중 RENAME vs expand-contract · 에러498→0

끝나면 말할 수 있는 것

‘ADD COLUMN은 메타데이터만 바꾼다(0.01s)’, ‘백필 UPDATE 한 방은 앱을 7초 멈춘다’, ‘ddl_lock_timeout은 실패를 앱 정지로 바꾼다’, ‘RENAME은 롤링 배포 중 498건의 에러’를 숫자로 말하고, 오라클 마이그레이션 절차를 설계할 수 있다.

다음 → O2 · 관측성 심화 교재 폴더 열기 ↗
O2 관측성 심화Deep Observability 두 서비스를 W3C traceparent로 한 trace에 잇고, 오라클 세션에 요청의 이름표를 붙이고, RED 메트릭의 히스토그램 p95가 실측과 얼마나 맞는지 잽니다. T1 Q07·Q14 +0 왕복세션 이름표 비용

배우는 것

  • traceparent 주입·추출로 서비스 둘을 한 줄기 trace로
  • 오라클 module·action·client_identifier — 드라이버가 다음 호출에 실어 보낸다
  • v$session에서 trace를, trace에서 세션을 서로 찾기
  • RED 메트릭과 Prometheus 히스토그램 — p95는 버킷 해상도만큼만 정확하다

Tier 1에서 이어지는 것

측정으로 증명

이름표를 매 요청 바꿈 · SQL*Net 왕복200 (+0)
p95 · 클라이언트 실측 vs 히스토그램 추정17.6 vs 24.1ms
span 1개 기록 비용18µs

끝나면 말할 수 있는 것

요청 하나를 서비스 → SQL_ID → v$sql.module/action까지, 실행 중인 세션을 v$session.client_identifier = trace_id로 찾을 수 있고, 그 비용이 왕복 0·span 18µs임을 안다.

다음 → O3 · 아키텍처 교재 폴더 열기 ↗
O3 아키텍처Layered → Hexagonal → Modular Monolith 같은 주문 기능을 레이어드 → 헥사고날 → 모듈러 모놀리스로 나눠 짜고, 경계가 SQL을 몇 개 만드는지 잽니다. T1 Q04·Q08 4,001 → 5경계 너머 SQL · 2,000건

배우는 것

  • 포트·어댑터 — 테스트에선 인메모리, 운영에선 오라클을 같은 테스트로
  • 모듈은 각자 테이블을 소유하고 남의 모듈은 공개 API로만 만난다
  • 경계 너머 N+1 — 건별 API는 SQL을 숨긴다 → 일괄 API
  • import 경계 검사기로 경계를 기계가 지키게

Tier 1에서 이어지는 것

측정으로 증명

모듈 경계 + 건별 API · 100건 / 2,000건101 / 4,001 SQL
일괄 API3 / 5 SQL
경계를 무시한 JOIN1 SQL (대가는 결합)

끝나면 말할 수 있는 것

‘모듈로 나눴다고 SQL이 줄지 않는다 — 건별 API는 101개(2,000건이면 4,001개)’, ‘일괄 API로 3~5개’, ‘경계를 무시한 JOIN은 1개지만 그 대가는 결합’을 숫자로 말하고, 경계를 어디에 그을지 판단한다.

다음 → E · 이커머스 탐구 교재 폴더 열기 ↗
2C

도메인 탐구

데이터셋 위에서 실전 도메인을 read-only 분석과 작은 PoC로 탐구하고, 결론을 분석노트로 남깁니다.

E 이커머스 — 불변식·상태머신·가격 이력E-commerce 주문 데이터에 도메인 불변식을 SQL로 대 보고, 동시에 오는 ‘완료’와 ‘취소’를 재고, ‘그 시점의 가격’을 조회하는 비용을 잽니다. T1 Q02 199/200 → 0동시 상태 전이 · 이중 기록

배우는 것

  • PK·CHECK는 한 행만 지킨다 — 여러 행 불변식은 쓰는 트랜잭션이나 주기 감사가 지킨다
  • 검증 쿼리도 틀린다 — 날짜와 일시를 그대로 비교하면 100% 위반으로 보인다
  • 상태 전이는 현재 상태를 조건으로 건 UPDATE 한 문장으로
  • 가격 이력(SCD2)의 키를 (상품, 끝 시각)으로 — 점 조회가 STOPKEY 한 행

Tier 1에서 이어지는 것

측정으로 증명

주문 금액 ≠ 라인 합99,982건
동시 완료·취소 · 읽고 쓰기 vs 조건부 UPDATE199/200→0
가격 점 조회 · BETWEEN vs STOPKEY52 → 3 Buffers

끝나면 말할 수 있는 것

여러 행에 걸친 규칙을 어디서 지킬지(트랜잭션·감사·스냅샷)를 데이터로 말하고, 상태 전이를 조건부 UPDATE로 0 이중 전이로 만든다.

다음 → R · 추천 서빙 교재 폴더 열기 ↗
R 추천 서빙 — 요청마다 vs 미리 계산Recommendation Serving ‘함께 산 상품’을 요청마다 계산할지 배치로 미리 계산할지를 계산 비용(Buffers·지연)과 신선도로 잽니다. T1 Q05·Q10 1,388 → 5한 요청 · Buffers

배우는 것

  • 추천의 백엔드 문제는 모델이 아니라 서빙 — 집계를 언제 하나
  • 인덱스가 반만 주는 이유 — 짝을 찾는 쪽은 여전히 전체를 읽는다
  • 미리 계산은 ‘빌드 주기만큼 낡은 답’을 판다 — 빌드가 싸면 신선도를 거의 공짜로

Tier 1에서 이어지는 것

측정으로 증명

요청마다 집계 · p501,388 Buffers · 6.72ms
요청마다 + 인덱스704 Buffers · 5.28ms
미리 계산한 표에서 서빙5 Buffers · 1.01ms
배치 빌드0.07s

끝나면 말할 수 있는 것

집계가 필요한 추천은 미리 계산해 PK 점 조회로 서빙하고, 신선도를 빌드 주기로 관리한다는 결론을 Buffers·지연·빌드 시간으로 말할 수 있다.

다음 → M · MLOps 백엔드 교재 폴더 열기 ↗
M MLOps 백엔드 — 피처·배치 스코어링·모델 교체MLOps Backend 피처를 어디서 만들지, 점수 9만 행을 어떤 경로로 쓸지, 서빙 중인 점수를 새 모델로 어떻게 바꿀지를 왕복과 행/초로 잽니다. T1 Q04 295 → 43만+행/초 · 행마다 커밋 vs executemany

배우는 것

  • 집계는 데이터가 있는 곳에서 — 가져와야 한다면 fetch 크기(arraysize)부터
  • 배치 쓰기의 첫 규칙: 커밋을 묶고, 문장을 배열 바인드로
  • Django bulk_create의 UNION ALL 한 문장은 배치가 클수록 초선형으로 느려진다
  • 서빙 데이터는 버전으로 쓰고 포인터로 바꾼다

Tier 1에서 이어지는 것

측정으로 증명

피처 · DB 안 vs 파이썬(arraysize 100)33 vs 523ms
점수 쓰기 · 행마다 커밋 vs executemany295~352 vs 43만~153만 행/s
bulk_create 배치 5,000 vs 500행2.7천 vs 13만 행/s
모델 교체 중 깨진 읽기 · 제자리 vs 포인터17~23%→0

끝나면 말할 수 있는 것

배치는 왕복과 커밋의 문제라는 것을 행/초로 말하고, 모델 교체를 읽기가 깨지지 않게 설계할 수 있다.

다음 → H1 · Tier 3 · DB 내부 진단 교재 폴더 열기 ↗

TIER 3Hero — 8 Quest

졸업 정점 · 3B 핀테크 캡스톤

3A

고부하·분산

DB 안을 진단하고, 동시에 많이 쓸 때의 경합을 재고, 트랜잭션 바깥의 정합성을 장애 주입으로 시험합니다.

H1 DB 내부 진단Inside the Database 요청 하나는 10046 트레이스로 오라클 안까지 쪼개고, 부하 전체는 0.1초마다 v$session을 샘플링해 대기 이벤트별로 나눕니다. T1 Q14 89.5 = 34.4 + 51.0ms · 요청 하나의 분해

배우는 것

  • 앱이 잰 ‘DB 시간’ = 오라클이 일한 시간 + 오라클이 앱을 기다린 시간(왕복)
  • tkprof의 반올림 함정 — 0.0ms는 0이 아니다
  • 샘플링(ASH-lite)으로 본 DB 시간을 오라클 ASH·시간 모델과 맞춰 보기
  • 락을 쥔 범인은 DB에서 놀고 있다(INACTIVE) — 트레이스와 샘플링 중 무엇으로 볼지

Tier 1에서 이어지는 것

측정으로 증명

앱 89.5ms = 오라클 + 앱을 기다림 (105왕복)34.4 + 51.0ms
tkprof 0.0ms였던 SQL · 실제 1회37µs
샘플링 AAS = ASH = 시간 모델2.0

끝나면 말할 수 있는 것

‘tkprof가 0.0ms라고 한 SQL은 실제로 1회 37µs였다’, ‘락을 쥔 범인은 DB에서 놀고 있다’를 숫자로 말하고, 운영 중 느려짐을 트레이스와 샘플링 중 무엇으로 볼지 고를 수 있다.

다음 → H2 · 고경합 교재 폴더 열기 ↗
H2 고경합 — 키 만들기와 핫 로우High Contention 8세션 동시 INSERT를 키 만들기 8가지 × 커밋 방식 2가지로, 한 행 카운터를 세션 1·4·8·16으로 돌려 처리량과 대기를 잽니다. T1 Q02 1.6천 → 4.3천/s핫 로우 · 같은 왕복 커밋

배우는 것

  • 1행마다 커밋하면 병목은 키 전략이 아니라 커밋(log file sync)
  • 배치에선 시퀀스 자체가 병목 — NOCACHE는 dc_sequences, 캐시는 뮤텍스
  • 키를 흩뿌린 대가 — 오른쪽 블록 경합은 사라지지만 redo·분할·범위 조회를 잃는다
  • 핫 로우는 세션을 늘려도 멈춘다 → 커밋을 같은 왕복에

Tier 1에서 이어지는 것

측정으로 증명

1행 커밋 · log file sync 비중~90%
캐시 시퀀스 · 뮤텍스가 DB time에서45%
핫 로우 · 세션을 늘려도1,600/s에서 정지
커밋을 같은 왕복에 실으면4,300/s

끝나면 말할 수 있는 것

‘1행마다 커밋하면 키 전략이 아니라 커밋이 병목’, ‘핫 로우는 세션을 늘려도 1,600/s에서 멈추고, 커밋을 같은 왕복에 실으면 4,300/s’를 숫자로 말할 수 있다.

다음 → H3 · 분산 정합성 교재 폴더 열기 ↗
H3 분산 정합성 — 리스·펜싱·아웃박스Distributed Consistency 하나의 DB 트랜잭션이 감싸 주지 못하는 두 가지(단일 실행 잡, DB + 브로커 쓰기)를 장애 주입으로 잽니다. T1 Q10 22 → 0잃어버린 갱신 · 펜싱 토큰

배우는 것

  • 리스(TTL 락)는 시간을 믿는다 — 멈춘 소유자에게서도 락을 빼앗는다
  • 펜싱 토큰으로 늦게 깨어난 소유자의 쓰기를 거절
  • 이중 쓰기는 어느 순서로 해도 틀린다 → 트랜잭셔널 아웃박스
  • 아웃박스는 유실·유령 대신 중복을 낸다 → 멱등 소비자

Tier 1에서 이어지는 것

측정으로 증명

리스만 · 멈춤 9번에 잃어버린 갱신22→0 (펜싱)
DB 행 락 · 죽은 소유자 정리3~13ms
이중 쓰기 · 유실 / 유령59 / 76
아웃박스 · 유실 / 유령 / 중복 → 멱등 소비자0 / 0 / 20 →0

끝나면 말할 수 있는 것

‘펜싱 토큰 없이 리스만 쓰면 멈춤 9번에 갱신 22개를 잃는다’, ‘이중 쓰기는 어느 순서로 해도 틀린다(유실 59 / 유령 76)’, ‘아웃박스의 중복 20을 멱등 소비자가 0으로 만든다’를 숫자로 말할 수 있다.

다음 → F1 · 3B 핀테크 캡스톤 교재 폴더 열기 ↗
3B

핀테크 캡스톤

F1~F5 — 돈의 타입부터 부하 아래 증명까지, 하나의 결제 서비스를 쌓아 올립니다.

3B · ★ CAPSTONE

핀테크·카드·거래 원장

복식부기 원장으로 돈이 오가는 백엔드를 끝까지 만들고, 정합성·동시성·멱등을 측정으로 증명합니다. 실 결제망·실 금융 데이터 없이 합성 원장과 카드 승인 시뮬레이터를 씁니다. 전용 사용자 fin_owner·fin_app을 만들어, 앱 계정은 원장을 고치지도 지우지도 못하게 시작합니다.

캡스톤이 증명할 것

  • 1만 7천 요청 × 3회, 원장 불변식 위반 0
  • 데드락 0 (잠금 순서 고정)
  • 재시도·중복 요청에도 중복 결제 0
entryaccountamount
T-1001A−10,000
T-1001B+10,000
Σ amount0 ✓
F1 원장과 금액Ledger & Money 금액 타입의 함정을 재고, 복식부기 원장(저널마다 Σ=0)을 만들고, 앱 계정이 원장을 고치지도 지우지도 못하게 권한으로 막습니다. T1 Q02·Q12 Σ = 0저널마다 · 커밋 시점 검사

배우는 것

  • float·Decimal·NUMBER — 합계·반올림·드라이버 경계
  • 복식부기: 계정·저널·포스팅, 잔액의 합은 늘 0
  • append-only를 앱 코드가 아니라 권한으로
  • Σ=0을 앱·감사·커밋 시점 검사 중 어디서 지킬지 — 비용과 함께

Tier 1에서 이어지는 것

측정으로 증명

드라이버 기본 fetch · 90조 원대float · 1전 오차
앱 계정의 UPDATE·DELETEORA-41900
커밋 시점 Σ=0 검사 · 8세션 처리량1,250 → 300/s
잔액 · 캐시 vs 포스팅 합3 vs 1,089 블록

끝나면 말할 수 있는 것

‘python-oracledb는 NUMBER를 float로 준다(90조 원대에서 1전이 바뀐다)’, ‘앱 계정의 UPDATE·DELETE는 ORA-41900’, ‘불균형 분개는 커밋 시점에 막을 수 있지만 처리량이 1,250 → 300/s’를 숫자로 말할 수 있다.

다음 → F2 · 이체와 데드락 교재 폴더 열기 ↗
F2 이체와 데드락Transfers & Deadlocks 두 계정을 잠그는 이체에서 데드락·마이너스 잔액·핫 계정을 재고, 앱 계정을 PL/SQL API만 쓰게 해 권한 구멍을 닫습니다. T1 Q02 데드락 → 0계정 번호 순 잠금

배우는 것

  • 잠그는 순서가 다르면 데드락 — 비용은 그 한 건이 아니라 줄 선 세션 전부
  • 모든 트랜잭션이 같은 전순서로 잠그면 순환이 생길 수 없다
  • 동시 출금의 마이너스 잔액을 막는 세 가지
  • 핫 계정 — 잠그는 위치와 왕복만 바꿔 락 보유 시간을 줄이는 사다리

Tier 1에서 이어지는 것

측정으로 증명

데드락 3~4번 · 처리량860~890 → 7~13/s
계정 번호 순 잠금 · 데드락0
앱 확인만 · 100만 원 계정 잔액−14만~−20만
핫 계정 결제 사다리약 400 → 1,600~1,800/s

끝나면 말할 수 있는 것

‘데드락 3~4번이 처리량을 860~890 → 7~13/s로 무너뜨렸다’, ‘앱 확인만으로는 100만 원 계정이 −14만~−20만 원이 됐다’, ‘같은 핫 계정 결제가 약 400 → 1,600~1,800/s’를 숫자로 말할 수 있다.

다음 → F3 · 멱등·한도·승인 교재 폴더 열기 ↗
F3 멱등·한도·승인Idempotency, Limits & Holds 같은 요청이 두 번 오고, 한도를 동시에 넘고, 상태가 경쟁하는 세 상황을 재현하고 ‘중복 결제 0 · 한도 초과 0 · 이중 전이 0’을 증명합니다. T1 Q02·Q10 280 → 99 → 0중복 결제

배우는 것

  • 멱등 키(Idempotency-Key) — 두 번째 요청에는 처음의 결과를
  • ‘있나 보고 → 결제 → 기록’은 동시 중복에서 샌다 → 키를 결제와 같은 트랜잭션에 먼저 INSERT
  • 같은 키로 금액을 바꾼 재전송은 422
  • 한도와 상태 전이는 읽고 확인하지 말고 조건부 UPDATE로

Tier 1에서 이어지는 것

측정으로 증명

키 없이 680번 요청 · 중복 결제280
조회 후 결제 · 동시 중복99
키를 먼저 INSERT0
한도 · 읽고 확인 vs 조건부 UPDATE50/50 초과 → 0
승인 보류 이중 전이99~100 → 0

끝나면 말할 수 있는 것

‘키 없이 680번 요청에 중복 280’, ‘있나 보고 → 결제 → 기록은 동시 중복에서 99건 샌다’, ‘키를 결제와 같은 트랜잭션에 먼저 INSERT하면 0’을 숫자로 말할 수 있다.

다음 → F4 · 정산·대사·감사 교재 폴더 열기 ↗
F4 정산·대사·감사Settlement, Reconciliation & Audit 정산 파일과 원장을 SQL 한 번으로 맞춰 심어 둔 불일치를 정확히 찾고, 해시 체인이 무엇을 잡고 무엇을 못 잡는지 잽니다. T1 Q10 0.03초대사 2만 건 · 불일치 4종

배우는 것

  • 대사의 목표는 ‘다 맞았다’가 아니라 어긋남을 종류별로 정확히 세는 것
  • 불변식을 깨지 않는 ‘일관된 조작’을 해시 체인이 잡는다
  • 체인까지 다시 쓰면 외부 앵커만 잡는다
  • 블록체인 테이블 vs 앱 해시 체인, 빈틈없는 번호의 값

Tier 1에서 이어지는 것

측정으로 증명

대사 SQL · 2만 건0.03초
심은 불일치 (누락·추가·금액·중복)37 · 23 · 15 · 9 정확히
처리량 감소 · 블록체인 테이블 vs 앱 체인−5~10% vs −75%
빈틈없는 번호(카운터 행) · 시퀀스 대비 처리량39~53% · 빈틈 0

끝나면 말할 수 있는 것

‘대사 SQL 0.03초에 2만 건, 심은 불일치 4종을 개수까지 정확히’, ‘금액을 줄이고 잔액까지 맞춘 조작을 체인이 지목했다’, ‘체인까지 다시 쓰면 외부 앵커만 잡는다’를 숫자로 말할 수 있다.

다음 → F5 · 종합 부하·졸업 교재 폴더 열기 ↗
F5 종합 부하·졸업Load & Graduation F1~F4의 결정을 하나의 FastAPI 결제 API로 묶고, 실패 경로를 섞은 혼합 부하 아래에서 정합성과 지연을 증명해 졸업 리포트를 씁니다. T1 Q14 590 req/s정합성 위반 0 · 3회

배우는 것

  • 돈은 PL/SQL API로만 — 요청 하나가 DB 왕복 하나
  • 오라클 에러를 업무 언어로: 잔액 부족 409, 키 재사용 422
  • 서버가 잰 시간과 클라이언트가 잰 시간이 다를 때 꼬리는 어디에 있나
  • 부하에서만 드러난 드라이버 멈춤을 결정적으로 재현하고 우회

Tier 1에서 이어지는 것

측정으로 증명

요청 / 처리량 (3회)약 1만 7천 / 587~591 req/s
예상 밖 상태 · 데드락 · 중복 결제0 · 0 · 0
p95 · 서버 구간 vs 클라이언트11~16 vs 87~93ms
풀 반납마다ROLLBACK 왕복 +1

끝나면 말할 수 있는 것

‘1만 7천 요청, 590 req/s, 예상 밖 상태 0, 데드락 0’, ‘서버가 잰 p95 11~16ms vs 클라이언트 87~93ms — 꼬리는 서버 밖’, ‘부하에서 드러난 드라이버 멈춤을 재현하고 우회했다’를 숫자로 말할 수 있다.

다음 → 졸업 · 졸업 리포트를 내 숫자로 교재 폴더 열기 ↗

학습 분량과 완료 기준

주 8~10시간 학습 기준

  1. 0.5~1일환경 구축 + Quest 00오라클 연결, 전제 점검
  2. 약 3~3.5주Tier 1 코어 (Quest 01~14)14개 Quest 빌드와 전/후 측정
  3. 약 2~3주Tier 2 딥다이브Django 평행·운영 심화·도메인 탐구 (범위 선택)
  4. 약 2~3주Tier 3 Hero + 핀테크 캡스톤고도화 측정과 원장 캡스톤
  5. 다음Level 2 실무 백엔드공개 — 10단계 + 산출물 3종
  6. 그 다음Level 3 운영·규모·신뢰성공개 — 로컬 Kubernetes로 7회차

필수Tier 1 졸업

Quest 00~14 통과 + 측정 대상 Quest의 전/후 측정표 + Quest 14에서 병목 1건을 앱·풀·DB 3계층에서 식별하고 개선.

정점캡스톤 졸업

핀테크 캡스톤에서 잔액 정합성·데드락 회피·멱등 보장 중 핵심을 측정으로 입증.

상시셀프 평가

각 Quest의 체크리스트 답변을 산출물로 남기고, Tier별 루브릭으로 스스로 점검합니다.

06 · Verification

이 교재의 숫자는 어디서 왔나

교재의 모든 수치는 같은 검증 환경에서 실제로 돌려 얻은 출력입니다. 그리고 교재를 처음 보는 학생처럼 README부터 Tier마다 처음부터 따라 하며, 막히는 곳과 문서와 다른 출력을 찾아 고쳤습니다.

실측모든 숫자는 실행 출력 — 지어낸 값 없음
35/35Quest를 학생 입장에서 처음부터 완주
47건따라 하며 찾은 문제
BLOCKER 1 · MAJOR 13 · MINOR 33
전부반영 — 바꾼 명령은 다시 돌려 출력을 받음

검증 환경

DB
Oracle AI Database 26ai Free 23.26.2 gvenzl/oracle-free:23.26.2-slim · cpu_count 2 · redo 10MB × 2
언어·드라이버
Python 3.13 · python-oracledb 4.0.1 (thin)
프레임워크
FastAPI 0.138 · SQLAlchemy 2.0.51 · Django 6.0.6
학생 검증
Windows 11 · Git Bash · Docker Desktop, .venv·.env 없는 새 사본에서 시작

버전은 실습마다 고정해 설치합니다. 결과가 다르면 SQL보다 버전부터 의심하세요.

Tier범위결과찾은 문제
Tier 1 코어Q00~Q1415/15 완주BLOCKER 0 · MAJOR 7 · MINOR 18 → 전부 반영
Tier 2 딥다이브D1~D6 · O1~O3 · E·R·M12/12 완주BLOCKER 1 · MAJOR 4 · MINOR 8 → 전부 반영
Tier 3 HeroH1~H3 · F1~F58/8 완주BLOCKER 0 · MAJOR 2 · MINOR 7 → 전부 반영

검증이 바꾼 것

틀린 문장은 고치고, 숫자는 다시 쟀습니다

Q07 · 주장 정정

“500의 스택은 로그와 trace에 남는다” — 아니었다

따라 해 보니 스택은 어디에도 없었습니다. 미들웨어가 unhandled_exception을 스택과 함께 로그로 남기고, trace 내보내기에 exception event를 싣도록 코드를 고쳐 실제로 남는 것을 확인했습니다.

Q03 · Q10 · 측정 위생

localhost가 효과를 묻었다

Windows에서 curl이 IPv6부터 시도해 연결마다 약 0.2초가 붙었습니다. 큐 6ms vs 인라인 504ms의 교훈이 278ms vs 708ms로 흐려졌습니다 → 모든 HTTP 주소를 127.0.0.1로.

Q14 · 다시 잼

통계가 바뀌자 계획이 바뀌었다

같은 데이터인데 STATUS 히스토그램이 생긴 뒤 옵티마이저가 복합 인덱스를 골랐습니다. 다시 재서 10 → 7 Buffers로 고치고, “계획은 통계에 따라 갈린다”를 본문과 자가점검의 ‘답이 갈리는 지점’에 넣었습니다.

D5 · BLOCKER

스냅샷 경로가 시드에서 멈췄다

migrate reviews 0001 뒤 시드가 ORA-00904로 실패했습니다. 시드가 DB에 실제로 적용된 마이그레이션 시점의 모델로 넣도록 고치고, 깨끗한 상태에서 처음부터 다시 돌려 확인했습니다.

배운 것을 스스로 확인하는 도구

읽은 분량이 아니라 설명할 수 있는 것으로

07 · Next level

다음 단계 — Level 2 · Level 3 공개

Level 1이 “이 코드가 DB에 무엇을 보내는가”를 재는 과정이었다면, Level 2는 “운영 중에 이것이 깨지면 어떻게 알고, 어떻게 되돌리는가”를 직접 깨뜨려 보며 배우는 과정입니다. 금융 서비스를 기준으로 한 10단계 실무 로드맵을 따라갑니다.

LEVEL 1 · 기초지금 여기

원리를 측정으로 증명한다

질문
이 코드가 오라클에 어떤 SQL을, 몇 번, 어떤 계획으로 보내나
DB
Oracle 26ai — SQLP 지식을 그대로 무기로
무대
노트북 한 대, 오라클 컨테이너 위의 앱
통과
전→후 측정표와 자가점검
산출물
Quest 35개, 핀테크 캡스톤 졸업 리포트
LEVEL 2 · 실무공개

운영하고, 깨뜨리고, 복구한다

질문
브라우저에서 DB의 한 행까지 가는 경로의 계층마다 무엇이 깨지고, 어떻게 아나
DB
PostgreSQL 중심 + Level 1의 Oracle과 대조
무대
로컬 Docker로 DNS·TLS·프록시·브로커·관측성까지 재현 + 실제 도메인으로 하는 실전 트랙
통과
정상 동작만으로는 부족 — 한 번은 고의로 깨뜨리고 복구한 기록
산출물
실시간 시세·포트폴리오 서비스, 주문·정산 백오피스, 운영 증거 저장소
LEVEL 3 · 운영공개

규모에서 운영한다

질문
이 서비스를 규모에서·분산으로·24/7로 운영하면 어디가 먼저 깨지나
DB
PostgreSQL 복제(primary + replica)
무대
로컬 Kubernetes(kind) — 오케스트레이션·복제·회복탄력성·카오스 + 실전 트랙
통과
장애를 일부러 내고 SLO를 지키는지 측정한 기록
산출물
자가치유·무중단배포·읽기복제·공급망 스캔 등 운영 증거

10단계 지도

각 단계가 Level 1의 어디에서 이어지는지 — 칩을 누르면 그 Quest로

1→2→3→4→5∥6→7→8→9→10
  1. 1 도메인·DNS·TLS

    IP는 임대 자원이다. TTL을 미리 낮추고 옮기는 절차, ACME 갱신과 reload, 중간 인증서 누락, 프록시 뒤의 HTTPS.

    Level 1 ←Q12
  2. 2 HTTP·쿠키·CORS·인증

    CORS는 인가가 아니다. 프리플라이트, credentials와 와일드카드, SameSite·CSRF, 세션 vs JWT의 폐기, 개인화 응답의 캐시 헤더.

    Level 1 ←Q06Q11D4D6
  3. 3 앱 서버·동시성

    워커 수는 메모리와 DB 커넥션에서 역산한다. 타임아웃 사다리는 안쪽일수록 짧게, 배포 중 요청 유실 0.

    Level 1 ←Q05Q09Q13Q14
  4. 4 데이터 계층

    잔고 UPDATE 대신 이중부기와 두 시간축, 실행계획, 잠금 순서, 무중단 마이그레이션 — PostgreSQL로, Oracle과 대조하며.

    Level 1 ←Q01~Q04F1F2O1H2
  5. 5 실시간·웹소켓 6과 병렬

    스냅샷과 증분을 시퀀스 번호로 잇는다. heartbeat, 느린 소비자와 컨플레이션, 지터로 막는 재연결 폭주.

    Level 1에 없던 새 주제
  6. 6 큐·멱등성 5와 병렬

    정확히 한 번은 없다. 멱등 키를 제약으로, DLQ 운영, 아웃박스, 거래일을 인자로 받는 재실행 가능한 마감 배치.

    Level 1 ←Q10F3H3
  7. 7 배포·CI/CD

    올리는 속도가 아니라 되돌리는 속도. 커밋 SHA 태그, 마이그레이션 선행, 1분 롤백, 기능 플래그.

    Level 1 ←Q13O1
  8. 8 관측성·장애대응

    질문 순서를 갖는다. RED·USE, SLO와 에러 버짓, 원인 분석보다 완화 먼저, 포스트모템, 대사.

    Level 1 ←Q07O2H1F4F5
  9. 9 보안·금융 규제

    신뢰 경계를 그린다. SSRF·IDOR, 컬럼 암호화와 블라인드 인덱스, 조회까지 남기는 감사, 망분리라는 설계 제약.

    Level 1 ←Q12D6F4
  10. 10 아키텍처·팀 리드

    만들지 않을 것을 고른다. 분리 기준, 하위호환 계약, ADR, 용량·비용 산정, 코드리뷰·온콜.

    Level 1 ←O3F5

4단계까지는 순서대로, 5단계(실시간)와 6단계(큐)는 나란히 진행해도 됩니다. 7단계부터는 앞에서 만든 서비스가 실제로 배포돼 있어야 의미가 생깁니다.

프레임워크 수준의 답 vs 실무 수준의 답

Level 2가 겨냥하는 차이

질문프레임워크 수준실무 수준
배포는 어떻게 하나푸시하면 플랫폼이 빌드해서 올려 준다이미지 태그를 커밋 SHA로 고정하고, 스키마 마이그레이션을 배포보다 먼저 돌리고, readiness를 통과한 인스턴스만 로드밸런서에 넣고, 실패하면 이전 태그로 되돌린다
CORS 에러가 난다모든 오리진을 허용한다credentials 요청엔 *를 쓸 수 없으니 허용 목록의 오리진만 반사하고, Vary: Origin으로 CDN이 다른 오리진의 응답을 재사용하지 않게 한다
응답이 느리다캐시를 붙인다평균이 아니라 p99를 보고, 트레이스로 DB·외부 API·이벤트 루프 대기 중 어느 구간인지 가른 뒤, 쿼리라면 실행계획으로 확인한다

증명용 산출물 3종

완성 기준은 측정치로 — Level 2의 목표입니다

산출물 1 · 단계 1·3·4·5·8

실시간 시세·포트폴리오 서비스

  • 24시간 연속 운영 기록
  • 강제 단절 뒤 복구 시간
  • 동시 연결 1,000 이상에서 p99 지연
  • 시퀀스 검사로 유실 0 증명
산출물 2 · 단계 2·4·6·9

주문·정산 백오피스

  • 중복 요청 1,000건 주입 뒤 원장 합계 일치
  • 마감 배치 3회 재실행 — 결과 동일
  • 일부러 뺀 건이 대사에서 자동으로 잡힘
산출물 3 · 단계 7·8·10

운영 증거 저장소

  • 롤백 1분 이내 측정 기록
  • 장애 훈련 2회의 포스트모템
  • ADR 5건, SLO·에러 버짓 대시보드
LEVEL 2 · LEVEL 3 · 공개

Level 2 · Level 3 안내서

Level 1과 나란한 별도 저장소로 공개돼 있습니다. Level 2(실무 — 웹 백엔드)와 Level 3(운영 — 규모·신뢰성)까지, Level 1의 Tier 1을 끝냈다면 바로 이어 갈 수 있습니다.

08 · Get started

시작하기

준비물을 확인하고, 저장소를 받아 Quest 00 스모크 테스트부터 돌려 봅니다.

사전 준비

  • Oracle 26ai Free 컨테이너gvenzl/oracle-free:23.26.2-slim — docker ps에 ora-hanorder가 Up
  • 접속 정보127.0.0.1:1521/FREEPDB1 · 실습 계정 sqlp (Windows의 localhost는 IPv6를 먼저 시도해 연결마다 늦을 수 있음)
  • 측정 권한sqlp에 SELECT_CATALOG_ROLE — v$sql·실행계획 조회용
  • 데이터셋 APRODUCTS · CUSTOMERS · ORDERS 적재
  • Python 3.12+검증 환경 3.13
# 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
그다음엔 Quest 00 가이드부터 차례로 따라가세요. Quest 00 열기 ↗

명령은 Git Bash 기준입니다. Windows에서 한글 출력이 깨지면 export PYTHONIOENCODING=utf-8(PowerShell은 $env:PYTHONIOENCODING='utf-8')을 먼저 실행하세요. 주소는 localhost 대신 127.0.0.1 — Windows는 IPv6를 먼저 시도해 연결마다 늦어집니다. 접속 정보는 .env에만 두고 커밋하지 않습니다.