Full-Stack Data Flow
택배 주문부터 배송까지 — 데이터가 흐르는 모든 경로
END-TO-END DATA FLOW — 프론트엔드 관점 요약
코드 한 줄이 사용자 브라우저에 도달하기까지의 전체 여정.
DEPLOYMENT PIPELINE — Vercel이 내부적으로 하는 일
USER REQUEST — localhost:3000이 아닌 실제 서비스에서는
각 계층별 복잡도
인프라
네트워크
프론트엔드
백엔드
데이터베이스
CI/CD 빌드 & 배포 파이프라인
Vercel의 '배포 완료!' 뒤에서 일어나는 일을 분해하면
Vercel에서 git push하면 자동으로 빌드 → 배포가 되잖아? 대기업에서는 이걸 직접 구축해서 쓴다. Jenkins가 npm run build를 실행하고, Docker로 결과물을 포장하고, ArgoCD가 서버에 올린다.
Vercel이 GitHub 연동하면 push할 때마다 자동 빌드 시작하잖아? 같은 원리다. GitHub이 Jenkins에 '새 코드 왔어!' 알림(Webhook)을 보내면 파이프라인이 시작된다.
npm install → npm run build → npm test를 자동으로 돌려주는 CI 서버. 린트, 테스트도 이때 실행. 빌드 실패하면 배포 자체가 안 된다. Vercel의 빌드 로그에서 보이는 그 과정이 바로 이것.
npm run build로 만든 .next 폴더와 Node.js 런타임을 하나의 박스(컨테이너)에 넣는다. 어느 서버에서든 이 박스만 열면 똑같이 실행된다. Vercel이 내부적으로 하는 것도 이거다.
npm publish로 패키지를 npmjs.com에 올리는 것처럼, Docker 이미지를 Registry(Harbor, ECR)에 올린다. 버전 태그를 붙여서 v1.2.3, v1.2.4 식으로 관리한다.
Vercel이 GitHub을 감시해서 새 코드가 올라오면 자동 배포하는 것과 같다. ArgoCD는 Git 저장소의 K8s 설정 파일(YAML)을 감시하고, 변경이 감지되면 자동으로 서버에 새 버전을 올린다.
기존 서버를 끄지 않고 새 버전을 올리는 기술. 새 서버를 먼저 띄우고, 정상 동작 확인(헬스체크) 후 트래픽을 전환하고, 옛 서버를 내린다. 사용자는 배포가 일어났는지도 모른다.
프론트 개발자 관점: Vercel = Jenkins + Docker + K8s + ArgoCD를 한 곳에 숨겨놓은 것. 원리를 알면 Vercel 없이도 직접 배포 파이프라인을 구축할 수 있다.
인프라 & 네트워크 계층
localhost:3000에서는 생략되는, 실제 서비스의 네트워크 경로
next dev로 개발할 때는 브라우저가 바로 내 컴퓨터에 연결된다. 실제 서비스에서는 DNS → CDN → LB → Proxy → Ingress를 거쳐야 서버에 도달한다. 각 계층이 보안, 성능, 가용성을 담당한다.
naver.com을 치면 DNS가 '223.130.195.200'으로 변환해준다. localhost에서는 이 과정이 없다 — 127.0.0.1이 바로 내 컴퓨터니까. 보통 50-200ms 소요. 한 번 조회하면 TTL 동안 캐시된다.
TCP는 SYN → SYN-ACK → ACK 3번 인사로 서로를 확인하는 과정. TLS는 그 위에 데이터를 암호화하는 것. https://의 's'가 TLS다. 없으면 비밀번호가 평문으로 오간다. TLS 1.3은 1-RTT로 빠르게 완료.
JS 번들, CSS, 이미지를 사용자와 물리적으로 가까운 서버에서 제공한다. 서울 사용자가 미국 서버까지 가지 않아도 되게. 캐시 히트 시 10-50ms로 응답. Vercel, Cloudflare, CloudFront가 CDN이다.
서버가 3대인데 1번에만 몰리면 터진다. LB가 '1번 바쁘니까 2번으로!' 하고 분산시킨다. 서버 하나가 죽으면 자동으로 나머지로 보낸다(헬스체크). L7 LB는 URL 경로별로 다른 서버에 보낼 수도 있다.
클라이언트와 백엔드 사이의 중개자. SSL 종료, 요청 압축(gzip), 캐싱, CORS 처리를 담당한다. 백엔드 서버의 존재를 클라이언트로부터 숨긴다. middleware.ts에서 rewrite, redirect 하는 것과 같은 역할.
myapp.com/api/* 요청은 Spring 서버로, 나머지는 Next.js 서버로 보내는 규칙. Next.js의 rewrites()에서 /api/* 를 외부 서버로 프록시하는 것과 같은 역할이지만, 인프라 레벨에서 처리한다.
Pod(서버)는 죽고 다시 태어나면서 IP가 바뀌지만, Service는 고정 DNS(frontend-svc:3000)를 제공한다. 코드에서 fetch('http://backend-svc:8080')로 호출하면 알아서 살아있는 Pod에 연결된다.
localhost:3000 vs 실제 서비스 — 네트워크 경로 비교
개발 환경 (next dev)
└─ Browser → localhost:3000 → 내 컴퓨터의 Next.js (끝!)
프로덕션 환경
├─ Browser → https://myapp.com
├─ DNS → myapp.com → 203.0.113.50
├─ TCP + TLS → 연결 설정 + 암호화
├─ CDN → 정적 자산(JS, CSS) 캐시 히트 시 여기서 끝
├─ L7 Load Balancer → 서버 분산
├─ K8s Ingress → / → frontend, /api/* → backend
└─ Pod → Next.js(3000) 또는 Spring(8080)
프론트 개발자 관점: 개발 환경에서는 이 5단계가 전부 생략된다. 배포하면 DNS → CDN → LB → Proxy → Ingress를 거쳐야 서버에 도달한다. “로컬에서는 되는데 배포하면 안 돼요”의 대부분은 이 계층 어딘가의 설정 문제.
Kubernetes 클러스터 구조
Docker Desktop에서 컨테이너 돌려봤다면 — 그걸 대규모로 관리하는 시스템
Kubernetes(K8s)는 컨테이너 여러 개를 자동으로 관리하는 시스템이다. “Next.js 서버 3개, Spring 서버 2개를 항상 유지해라”고 명시하면 K8s가 알아서 실현한다. Vercel이 내부적으로 이런 일을 하고 있다.
KUBERNETES CLUSTER — 프론트 개발자가 알아야 할 핵심
K8s Cluster — Vercel이 내부적으로 운영하는 것
│
├─ Control Plane — 전체 관리자 (개발자가 직접 볼 일 거의 없음)
│
├─ Worker Node A
│ ├─ Pod: frontend — Next.js 컨테이너 (replicas: 3)
│ └─ Pod: backend — Spring Boot 컨테이너 (replicas: 2)
│
└─ Worker Node B
├─ Pod: frontend
├─ Pod: backend
└─ Pod: redis — 캐시 저장소
Docker 컨테이너를 감싸는 K8s의 최소 단위. 보통 1 Pod = 1 컨테이너. 프론트엔드 Pod 안에는 Next.js가 3000번 포트에서 돌고 있다. next start로 서버 하나 띄우는 것과 같다.
Pod는 죽고 다시 태어나면서 IP가 바뀌지만, Service는 고정 DNS(frontend-svc:3000)를 제공한다. fetch 코드에서 'http://backend-svc:8080'로 호출하면 항상 살아있는 Pod에 연결된다.
'이 앱은 서버 3개로 돌려라'고 선언하면 K8s가 항상 3개를 유지한다. 하나 죽으면 자동으로 새로 띄운다. Vercel이 알아서 여러 인스턴스를 관리하는 것과 같은 원리.
프론트 개발자 관점: 프론트엔드 개발자가 K8s를 직접 다룰 일은 적다. 하지만 “Pod가 3개인데 왜 특정 사용자만 에러가 나요?” 같은 질문에 대답하려면 이 구조를 이해해야 한다.
Next.js — 첫 페이지 로딩
page.tsx가 서버에서 실행된다는 것의 의미
Next.js App Router는 React Server Components(RSC)를 기본으로 사용한다. 서버에서 컴포넌트를 실행해 HTML을 만들고, 클라이언트에서 필요한 부분만 Hydration한다. 'use client' 안 쓰면 전부 서버 컴포넌트.
Next.js 서버가 URL을 받으면 /app 폴더에서 해당 경로의 page.tsx를 찾는다. layout.tsx → loading.tsx → page.tsx 순서로 계층을 해석한다. 이건 프론트 개발자가 매일 하는 일이니 익숙할 것.
'use client' 안 붙인 컴포넌트는 서버에서 실행된다. async/await로 DB나 API를 직접 호출할 수 있다. 이 코드는 JS 번들에 포함되지 않으므로 API 키나 시크릿을 써도 안전하다.
서버 컴포넌트 실행 결과를 RSC Payload(특수 JSON)로 변환하고, HTML을 스트리밍한다. Suspense의 fallback을 먼저 보내고, 데이터가 준비되면 나중에 교체. loading.tsx가 보이다가 진짜 데이터로 바뀌는 게 이것.
서버가 보낸 HTML은 보이지만 클릭해도 반응 없다. JS가 로드되면 React가 이벤트 핸들러를 연결(Hydration)한다. 이 과정이 끝나야 onClick, useState, useEffect가 작동한다. 'use client' 컴포넌트의 JS만 다운로드.
- · async/await로 직접 데이터 페칭
- · DB, 파일시스템, 환경변수 접근
- · JS 번들에 포함되지 않음
- · 서버에서만 실행, 시크릿 안전
⚠ useState, useEffect, onClick 사용 불가
- · useState, useEffect, useRef 사용
- · onClick, onChange 이벤트 핸들러
- · 브라우저 API (window, localStorage)
- · 서드파티 라이브러리 (차트, 에디터)
⚠ async 컴포넌트, 서버 전용 API 사용 불가
서버 → 브라우저 렌더링 흐름
Next.js Server
│
├─ RootLayout (Server) — html, body
│ ├─ Header (Server) — 정적 네비게이션
│ ├─ SearchBar (Client) — useState 필요 → JS 번들에 포함
│ ├─ ProductList (Server) — await fetch(API) → JS에 미포함
│ │ └─ AddToCartBtn (Client) — onClick → JS 번들에 포함
│ └─ Footer (Server) — 정적 → JS에 미포함
│
▼ 브라우저 수신
1. HTML 파싱 → 화면에 콘텐츠 바로 표시 (FCP)
2. JS 로드 → SearchBar.js + AddToCartBtn.js만 다운로드
3. Hydration → 이벤트 핸들러 연결 → 인터랙션 가능 (TTI)
핵심: Server Component = 서버에서 완성(빠르고 SEO 좋음, JS 번들에 미포함). Client Component = 브라우저에서 인터랙션(JS 번들에 포함). 'use client'는 꼭 필요한 곳에만!
프론트엔드 → 백엔드 API 요청
fetch('/api/users') 뒤에서 일어나는 일
사용자가 버튼을 클릭하면 fetch()로 Spring 서버에 요청을 보낸다. Server Component에서 호출하면 서버 → 서버 통신(빠르고 안전), Client Component에서 호출하면 브라우저 → 서버 통신(CORS 필요).
PATH A — SERVER COMPONENT (서버 → 서버, 빠름)
브라우저를 거치지 않음. CORS 불필요. API 키 노출 위험 없음.
PATH B — CLIENT COMPONENT (브라우저 → 서버)
인터넷 경유. DNS → TLS → LB → Ingress 전체 스택 통과. CORS 헤더 필요.
K8s 내부에서 Service DNS(backend-svc:8080)로 직접 호출한다. 브라우저를 거치지 않으므로 빠르고, API 키나 DB 정보가 노출될 위험이 없다. fetch('http://backend-svc:8080/api/users')처럼 사용.
브라우저에서 출발하므로 전체 네트워크 스택을 통과한다. CORS 설정이 필요하고, JWT나 쿠키로 인증해야 한다. fetch('/api/users', { credentials: 'include' })처럼 사용.
'use server' 함수를 클라이언트에서 직접 호출한다. Next.js가 자동으로 POST 요청으로 변환. form action에 바인딩 가능. revalidatePath로 캐시도 갱신. API Route를 안 만들어도 된다.
app/api/*/route.ts에 GET, POST 핸들러를 정의한다. 프록시, 인증 미들웨어, 데이터 변환에 활용. Spring의 Controller와 같은 역할이지만 TypeScript로 작성.
프론트 개발자 관점: 가능하면 Server Component에서 fetch하라. 서버 → 서버 통신이라 빠르고, CORS도 불필요하고, API 키도 안전하다. Client Component의 fetch는 인터랙션(onClick)에서만.
Spring Boot 백엔드 내부 흐름
Next.js API Route의 Java 버전 — 구조는 비슷하다
Spring 서버에 요청이 도착하면 Tomcat → Filter → DispatcherServlet → Controller → Service → Repository → DB 순서로 처리된다. Next.js의 middleware.ts → route.ts → 로직함수 → DB 접근과 같은 패턴이다.
Spring의 내장 웹 서버. 8080번 포트에서 HTTP 요청을 받는다. next dev가 3000번 포트에서 대기하는 것처럼, Tomcat이 8080에서 대기. 프로덕션에서도 이 서버를 그대로 쓴다.
요청이 Controller에 도달하기 전에 공통 처리를 한다. 인증(JWT 검증), 인가(권한 확인), CORS, 로깅 등을 순차 실행. Next.js의 middleware.ts에서 request를 가로채서 처리하는 것과 동일한 개념.
Spring의 핵심 허브. 모든 요청을 받아서 URL 패턴을 보고 적절한 Controller 메서드를 찾아 실행한다. Next.js App Router가 /users → app/users/page.tsx를 매칭하는 것과 같다.
HTTP 요청을 수신하고 파라미터를 바인딩한다. @GetMapping('/api/users')가 프론트의 app/api/users/route.ts export GET()과 같은 것. 입력 검증 후 Service를 호출한다.
핵심 업무 로직을 처리하는 곳. '회원가입 시 이메일 중복 체크 → 비밀번호 암호화 → 저장' 같은 흐름. @Transactional이 붙으면 전부 성공하거나 전부 취소(롤백)된다.
findById(), save(), delete() 같은 메서드로 DB를 조작한다. Spring Data JPA는 함수 이름만 정의하면 SQL을 자동 생성한다. Prisma에서 prisma.user.findUnique()처럼 쓰는 것과 같다.
| Spring (백엔드) | Next.js (프론트) | 하는 일 |
|---|---|---|
| Tomcat :8080 | next start :3000 | HTTP 요청 수신 |
| Filter Chain | middleware.ts | 요청 전처리 (인증, CORS) |
| DispatcherServlet | App Router | URL → 핸들러 매칭 |
| @Controller | route.ts (GET, POST) | 요청/응답 처리 |
| @Service | 유틸 함수 / Custom Hook | 비즈니스 로직 |
| Repository (JPA) | Prisma Client | DB 접근 |
프론트 개발자 관점: Spring의 Controller → Service → Repository는 Next.js의 route.ts → 로직함수 → Prisma와 같은 패턴이다. 이름만 다르지 구조는 같다.
데이터베이스 계층
localStorage의 서버 버전 — 커넥션 풀, JPA, 트랜잭션
프론트에서 localStorage에 데이터를 저장하듯, 서버는 MySQL/PostgreSQL에 저장한다. Prisma로 DB를 다뤄봤다면 JPA도 같은 개념이다. DB 커넥션을 미리 만들어두고 돌려쓰는 커넥션 풀이 핵심.
DB 연결을 매번 새로 만들면 느리다(수십ms). 그래서 10개를 미리 만들어두고 빌려쓰고 반환한다. 프론트에서 HTTP 커넥션을 keep-alive로 재사용하는 것과 같은 원리. 열쇠가 다 나갔으면 반납될 때까지 대기.
SQL을 직접 안 쓰고 findById(), save() 같은 함수로 DB를 조작한다. 메서드 이름만 정의하면 SQL을 자동 생성. Prisma에서 prisma.user.findUnique({ where: { id: 1 } })로 쓰는 것과 같다.
A 계좌에서 빼고 B 계좌에 넣는 것을 하나로 묶어서 처리. 중간에 에러나면 전부 원상복구(Rollback). 프론트에서 여러 API를 호출할 때 하나 실패하면 UI를 되돌리는 것과 비슷하지만, DB 레벨에서 보장.
유저 10명 조회(1번) + 각 유저의 주문 조회(10번) = 11번 쿼리. 프론트에서 목록 API 호출 후 각 항목의 상세 API를 따로 호출하는 것과 같은 비효율. @EntityGraph나 fetch join으로 1번에 해결.
HIKARICP CONNECTION POOL — DB 연결 재사용
Spring Boot Application
│
├─ 요청 A → Connection #1 빌림 (사용 중)
├─ 요청 B → Connection #2 빌림 (사용 중)
├─ 요청 C → Connection #3 반환 (풀로 돌아감)
│
└─ HikariCP Pool (최대 10개)
├─ #1, #2 사용 중
├─ #3~10 대기 중
↕
MySQL / PostgreSQL
프론트 개발자 관점: localStorage, IndexedDB가 브라우저용 미니 DB라면, MySQL/PostgreSQL은 서버용 거대 DB이다. Prisma ≈ JPA, prisma.user.findMany() ≈ userRepository.findAll(). 원리는 같다.
응답 반환 — DB에서 브라우저까지
res.json() → setState → 리렌더링의 전체 경로
DB에서 데이터를 꺼내면 역순으로 돌아간다. Entity → DTO → JSON → HTTP Response → 네트워크 → 브라우저 → setState → 리렌더링.
DB에서 꺼낸 원본 데이터(Entity)에는 비밀번호 등 민감 정보가 있다. DTO로 필요한 필드만 복사하고, Jackson이 JSON으로 변환해서 내보낸다. res.json()으로 파싱하는 그 JSON이 이 과정을 거쳐서 온다.
200: 성공, 201: 생성 완료, 400: 잘못된 요청, 401: 인증 필요, 404: 없음, 500: 서버 에러. fetch 후 response.ok나 response.status로 확인한다. if (!res.ok) throw new Error() 패턴.
fetch의 Promise가 resolve되면 setState로 상태를 변경한다. React가 Virtual DOM을 비교(Reconciliation)해서 실제로 변경된 DOM 노드만 업데이트. 전체 화면을 다시 그리지 않으니 빠르다.
RESPONSE FLOW — DB에서 브라우저까지
DB — SELECT 결과 반환
│
├→ Hibernate → ResultSet → Entity 객체
├→ Service → Entity → DTO (민감정보 제거)
├→ Controller → ResponseEntity + HTTP 200
├→ Jackson → DTO → JSON 직렬화
├→ Tomcat → K8s → LB → 네트워크 전달
│
└→ Browser
├→ res.json() → JSON 파싱
├→ setState(data) → 리렌더링 트리거
├→ Virtual DOM Diff → 변경된 컴포넌트만
└→ DOM 업데이트 → 화면에 새 데이터 표시
프론트 개발자 관점: res.json()으로 파싱하고 setState로 UI를 업데이트하는 것. 이 한 줄 뒤에 DB → Entity → DTO → JSON → 네트워크 → 브라우저의 전체 여정이 숨어있다.
전체 흐름 한눈에 보기
빌드부터 브라우저까지 — 핵심 정리
| 단계 | 소요 시간 | 병목 | 최적화 |
|---|---|---|---|
| DNS | 50-200ms | 낮음 | DNS Prefetch |
| TCP + TLS | 50-150ms | 중간 | HTTP/2, TLS 1.3 |
| LB + Ingress | 1-5ms | 낮음 | Keep-alive |
| Server Component | 10-100ms | 중간 | 캐시, Streaming, Suspense |
| Spring 처리 | 5-50ms | 중간 | Virtual Threads, 캐시 |
| DB Query | 1-100ms | 높음 | 인덱스, N+1 방지 |
| Hydration | 50-300ms | 높음 | JS 번들 최소화, Code Split |
데이터 페칭은 서버에서. 'use client'는 인터랙션 필요한 최소 범위에만. JS 번들이 줄어든다.
Server Component → Spring은 K8s Service DNS로 호출. 인터넷 안 거치니 빠르고 CORS 불필요.
CDN(정적) → Next.js fetch 캐시(ISR) → Redis(세션) → DB. 각 계층에서 불필요한 요청을 차단.
Jenkins + Docker + K8s + ArgoCD = Vercel 내부 동작 원리. 원리를 알면 어디서든 배포할 수 있다.
Controller ≈ route.ts, Service ≈ 로직함수, Repository ≈ Prisma. 구조는 같다.
50-300ms 소요. Server Component로 JS 번들을 줄이고, Code Splitting으로 필요한 것만 로드.
BEGINNER-FRIENDLY ANALOGY GUIDE
위 도표의 모든 단계를 “택배 주문”에 비유해서 설명한다
P1BUILD & DEPLOY — “상품을 만들어서 매장에 진열하는 과정”
개발자가 코드(레시피)를 작성하고 GitHub(본사)에 보내는 것. '이 레시피대로 만들어주세요'라고 요청하는 단계다.
레시피를 받은 공장(Jenkins)이 재료 검수(테스트) → 상품 제조(빌드)를 자동으로 한다. 불량품이 나오면(테스트 실패) 라인을 멈추고 개발자에게 알린다.
완성된 상품(앱)을 표준 규격 박스(Docker 이미지)에 넣는다. 어떤 매장(서버)에서든 이 박스만 열면 바로 진열할 수 있도록 모든 재료(라이브러리, 런타임)가 같이 들어간다. 택배 박스가 규격화되어 있으니 어디서든 동일하게 동작한다.
포장된 박스를 물류 창고(레지스트리)에 보관한다. 'v1.2.3 박스', 'v1.2.4 박스' 이런 식으로 버전별로 정리해둔다. 필요할 때 여기서 꺼내 쓴다.
물류 창고의 재고 목록(Git 매니페스트)을 항상 감시하는 로봇. 새 상품이 들어오면 자동으로 매장(K8s)에 가져가서 진열한다. 목록과 매장 진열 상태가 다르면 즉시 동기화한다.
매장 문을 닫지 않고 상품을 교체하는 기술. 새 진열대를 먼저 세우고(새 Pod 생성) → 상품이 제대로 놓였는지 확인(헬스체크) → 확인되면 손님을 새 진열대로 안내 → 옛 진열대를 치운다(구 Pod 종료). 손님은 교체가 일어났는지도 모른다.
P2USER REQUEST — “손님이 매장을 찾아오는 과정”
손님이 'myapp.com 매장이 어디죠?'라고 물으면, DNS(114)가 '서울시 강남구 ○○빌딩(203.0.113.50)입니다'라고 IP 주소를 알려준다. 한 번 물어보면 메모해두고(캐시) 다음엔 바로 찾아간다.
매장 도착. TCP는 '저 손님이에요(SYN)' → '어서오세요(SYN-ACK)' → '들어갈게요(ACK)' 3번 인사로 서로를 확인하는 과정. TLS는 그 뒤에 '우리 대화를 도청 못하게 암호화합시다'라고 비밀 통로를 만드는 것. 이후 모든 대화가 암호화된다.
본점(오리진 서버)은 서울에 있지만, JS/CSS/이미지 같은 인기 상품은 전국 편의점(엣지 서버)에 미리 갖다 놓는다. 부산 손님이 서울까지 안 가고 부산 편의점에서 바로 받을 수 있다. 이게 CDN 캐시 히트. 없는 물건만 본점에 요청한다.
매장에 입구가 여러 개(서버 여러 대)인데, 안내원(LB)이 '1번 입구는 지금 줄이 길어요, 2번으로 가세요' 하고 손님을 분산시킨다. 한 입구에 사람이 몰리는 걸 방지. 입구 하나가 고장나면 자동으로 다른 입구로 안내한다(헬스체크).
손님이 직접 주방(백엔드)에 들어가면 안 되니까, 안내 데스크(프록시)가 중간에서 '어떤 용건이세요?' 하고 접수한다. 데스크가 주방 위치를 숨기고(보안), 자주 묻는 질문은 바로 답해주고(캐시), 동시에 여러 명이 같은 질문하면 주방에 한 번만 물어본다.
'식당은 2층(backend:8080), 카페는 3층(frontend:3000)' 같은 안내판. myapp.com/api/* 요청은 백엔드 서비스로, 나머지는 프론트 서비스로 보내는 라우팅 규칙을 정의한다.
2층 식당에 셰프(Pod)가 3명 있다. Service는 내선번호(ClusterIP)로 전화하면 비어있는 셰프에게 자동 연결해주는 교환대. 셰프가 퇴근(Pod 죽음)해도 새 셰프(새 Pod)가 같은 내선번호로 연결된다.
손님(브라우저) 앞에서 요리하는 게 아니라, 주방(서버)에서 완성된 요리(HTML)를 만들어 내보낸다. 손님은 재료(데이터)를 볼 필요 없이 완성된 요리만 받는다. 레시피(코드)도 주방에만 있으니 손님 눈에 안 보인다(JS 번들에 미포함).
대부분 요리는 주방에서 오지만, 고기 굽기(인터랙션)처럼 손님이 직접 해야 하는 건 미니 화로('use client')를 테이블에 놓는다. 이 화로만 손님 짐(JS 번들)에 포함된다. 화로가 많을수록 손님 짐이 무거워지니, 꼭 필요한 것만.
서버가 보낸 HTML은 '마른 라면'이다. 보기엔 완성됐는데 아직 맛(인터랙션)이 없다. JS를 로드해서 '뜨거운 물(이벤트 핸들러)'을 부으면 그때서야 클릭, 입력 등이 작동하는 진짜 음식이 된다. 이 과정이 Hydration.
P3DATA MUTATION — “손님이 주문하고 음식을 받는 과정”
손님이 메뉴판에서 '비빔밥 하나요!'(버튼 클릭) 하면, 종업원(fetch)이 주문서(HTTP POST 요청 + JSON body)를 주방에 전달한다.
주문서가 안내원(LB) → 층별 안내판(Ingress) → 해당 주방(Spring Pod)까지 전달되는 과정. 손님은 이 과정을 모르고 그냥 기다린다.
Tomcat(주방 문)을 지나면 Filter(검문소)를 여러 개 통과한다. '회원증(JWT) 확인하겠습니다(Security)', '이 주방에서 주문 가능한 손님이 맞나요(CORS)', '주문서 인코딩 확인(Encoding)'. 통과 못하면 거부당한다(401/403).
모든 주문서가 주방장(DispatcherServlet) 한 명을 거친다. 주방장이 '이 주문은 비빔밥 담당(UserController)에게!' 하고 배분한다. 어떤 담당 셰프에게 보낼지는 메뉴 매핑표(HandlerMapping)를 보고 결정.
주문서를 받아서 내용을 정리한다. '비빔밥 1개, 계란 추가(@RequestBody → DTO)'. 주문 내용이 이상하면 돌려보낸다(@Valid → '가격은 음수일 수 없습니다'). 정리가 끝나면 실제 요리사(Service)에게 넘긴다.
핵심 요리(비즈니스 로직)를 한다. @Transactional은 '이 요리는 전부 성공하거나, 전부 실패여야 한다'는 규칙. 밥은 됐는데 나물이 없으면? 이미 한 밥도 취소(ROLLBACK). 전부 완성되면 서빙(COMMIT).
셰프가 직접 냉장고(DB)를 열지 않는다. 보조(Repository)에게 '달걀 3개 가져와(findById)' 하면 보조가 냉장고에서 찾아온다. JPA는 '달걀'이라고 말하면 자동으로 냉장고 몇 번째 칸인지 알아내는(SQL 자동 생성) 스마트 보조.
냉장고 열쇠(DB 커넥션)는 10개뿐인데 동시에 100명이 요리한다. 매번 열쇠를 만들면(커넥션 생성) 30초 걸리니, 미리 10개 만들어두고(풀) 빌려주고 돌려받는다. 열쇠가 다 나갔으면 반납될 때까지 줄 서서 기다린다(connectionTimeout).
최종적으로 데이터가 저장되는 곳. INSERT는 '새 서류를 금고에 넣기', SELECT는 '금고에서 서류 꺼내 복사하기', UPDATE는 '기존 서류 수정', DELETE는 '서류 파쇄'. 금고 문(커넥션)을 열려면 열쇠(인증)가 필요하다.
금고에서 꺼낸 원본 서류(Entity)에는 비밀 정보가 있다. 손님에게는 필요한 정보만 복사한 사본(DTO)을 만들고, 이걸 손님이 읽을 수 있는 형태(JSON)로 포장해서 내보낸다. 원본은 절대 밖에 안 나간다.
주문한 음식이 도착하면(응답 수신) 전광판(React state)의 숫자를 갱신한다. React는 전광판에서 바뀐 숫자만 찾아서(Virtual DOM Diff) 그 부분만 깜빡이며 업데이트한다. 전광판 전체를 갈아끼우지 않으니 빠르다.