Full-Stack Data Flow

택배 주문부터 배송까지 — 데이터가 흐르는 모든 경로

END-TO-END DATA FLOW — 프론트엔드 관점 요약

코드 한 줄이 사용자 브라우저에 도달하기까지의 전체 여정.

DEPLOYMENT PIPELINE — Vercel이 내부적으로 하는 일

git push코드 올리기
Jenkinsnpm run build
Docker빌드 결과 포장
Registry이미지 보관
ArgoCD자동 배포
K8s서버 실행

USER REQUEST — localhost:3000이 아닌 실제 서비스에서는

브라우저URL 입력
DNSIP 변환
CDN정적 캐시
LB서버 분배
Ingress경로 분기
Pod앱 실행

각 계층별 복잡도

인프라

Jenkins, Docker, K8s, ArgoCD, Ingress
30%

네트워크

DNS, CDN, LB, TLS, Reverse Proxy
20%

프론트엔드

Next.js RSC, Client Components, Hydration
25%

백엔드

Spring Boot, Service, Repository
15%

데이터베이스

Connection Pool, Query, Transaction
10%
1

CI/CD 빌드 & 배포 파이프라인

Vercel의 '배포 완료!' 뒤에서 일어나는 일을 분해하면

Vercel에서 git push하면 자동으로 빌드 → 배포가 되잖아? 대기업에서는 이걸 직접 구축해서 쓴다. Jenkins가 npm run build를 실행하고, Docker로 결과물을 포장하고, ArgoCD가 서버에 올린다.

📤
Git Push → Webhook= “Vercel GitHub 연동과 같은 원리

Vercel이 GitHub 연동하면 push할 때마다 자동 빌드 시작하잖아? 같은 원리다. GitHub이 Jenkins에 '새 코드 왔어!' 알림(Webhook)을 보내면 파이프라인이 시작된다.

🔨
Jenkins (CI)= “npm run build를 자동 실행하는 로봇

npm install → npm run build → npm test를 자동으로 돌려주는 CI 서버. 린트, 테스트도 이때 실행. 빌드 실패하면 배포 자체가 안 된다. Vercel의 빌드 로그에서 보이는 그 과정이 바로 이것.

📦
Docker= “build 폴더 + Node.js를 하나로 묶기

npm run build로 만든 .next 폴더와 Node.js 런타임을 하나의 박스(컨테이너)에 넣는다. 어느 서버에서든 이 박스만 열면 똑같이 실행된다. Vercel이 내부적으로 하는 것도 이거다.

🏪
Container Registry= “npmjs.com이 패키지 저장소이듯, Docker 이미지 저장소

npm publish로 패키지를 npmjs.com에 올리는 것처럼, Docker 이미지를 Registry(Harbor, ECR)에 올린다. 버전 태그를 붙여서 v1.2.3, v1.2.4 식으로 관리한다.

🤖
ArgoCD (CD)= “Vercel의 자동 배포 = 이것

Vercel이 GitHub을 감시해서 새 코드가 올라오면 자동 배포하는 것과 같다. ArgoCD는 Git 저장소의 K8s 설정 파일(YAML)을 감시하고, 변경이 감지되면 자동으로 서버에 새 버전을 올린다.

🔄
K8s Rolling Update= “무중단 배포 — 서버를 안 끄고 교체

기존 서버를 끄지 않고 새 버전을 올리는 기술. 새 서버를 먼저 띄우고, 정상 동작 확인(헬스체크) 후 트래픽을 전환하고, 옛 서버를 내린다. 사용자는 배포가 일어났는지도 모른다.

프론트 개발자 관점: Vercel = Jenkins + Docker + K8s + ArgoCD를 한 곳에 숨겨놓은 것. 원리를 알면 Vercel 없이도 직접 배포 파이프라인을 구축할 수 있다.

2

인프라 & 네트워크 계층

localhost:3000에서는 생략되는, 실제 서비스의 네트워크 경로

next dev로 개발할 때는 브라우저가 바로 내 컴퓨터에 연결된다. 실제 서비스에서는 DNS → CDN → LB → Proxy → Ingress를 거쳐야 서버에 도달한다. 각 계층이 보안, 성능, 가용성을 담당한다.

📞
DNS Resolution= “도메인을 IP 주소로 바꾸는 전화번호부

naver.com을 치면 DNS가 '223.130.195.200'으로 변환해준다. localhost에서는 이 과정이 없다 — 127.0.0.1이 바로 내 컴퓨터니까. 보통 50-200ms 소요. 한 번 조회하면 TTL 동안 캐시된다.

🤝
TCP + TLS Handshake= “서버와 악수 + 암호화 통로 설정

TCP는 SYN → SYN-ACK → ACK 3번 인사로 서로를 확인하는 과정. TLS는 그 위에 데이터를 암호화하는 것. https://의 's'가 TLS다. 없으면 비밀번호가 평문으로 오간다. TLS 1.3은 1-RTT로 빠르게 완료.

🌐
CDN= “Vercel Edge Network — 정적 파일을 가까운 서버에서

JS 번들, CSS, 이미지를 사용자와 물리적으로 가까운 서버에서 제공한다. 서울 사용자가 미국 서버까지 가지 않아도 되게. 캐시 히트 시 10-50ms로 응답. Vercel, Cloudflare, CloudFront가 CDN이다.

🚦
Load Balancer= “서버 여러 대에 요청을 분산하는 교통정리

서버가 3대인데 1번에만 몰리면 터진다. LB가 '1번 바쁘니까 2번으로!' 하고 분산시킨다. 서버 하나가 죽으면 자동으로 나머지로 보낸다(헬스체크). L7 LB는 URL 경로별로 다른 서버에 보낼 수도 있다.

🛡️
Reverse Proxy (Nginx)= “Next.js middleware.ts의 서버 버전

클라이언트와 백엔드 사이의 중개자. SSL 종료, 요청 압축(gzip), 캐싱, CORS 처리를 담당한다. 백엔드 서버의 존재를 클라이언트로부터 숨긴다. middleware.ts에서 rewrite, redirect 하는 것과 같은 역할.

🚪
K8s Ingress= “/api/*는 Spring, 나머지는 Next.js — 경로 기반 라우팅

myapp.com/api/* 요청은 Spring 서버로, 나머지는 Next.js 서버로 보내는 규칙. Next.js의 rewrites()에서 /api/* 를 외부 서버로 프록시하는 것과 같은 역할이지만, 인프라 레벨에서 처리한다.

📍
K8s Service → Pod= “고정 주소로 여러 서버에 자동 분배

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를 거쳐야 서버에 도달한다. “로컬에서는 되는데 배포하면 안 돼요”의 대부분은 이 계층 어딘가의 설정 문제.

3

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 — 캐시 저장소

📦
Pod= “컨테이너 1개 = 서버 1개

Docker 컨테이너를 감싸는 K8s의 최소 단위. 보통 1 Pod = 1 컨테이너. 프론트엔드 Pod 안에는 Next.js가 3000번 포트에서 돌고 있다. next start로 서버 하나 띄우는 것과 같다.

📍
Service= “고정 DNS — Pod가 죽어도 같은 주소로 접근

Pod는 죽고 다시 태어나면서 IP가 바뀌지만, Service는 고정 DNS(frontend-svc:3000)를 제공한다. fetch 코드에서 'http://backend-svc:8080'로 호출하면 항상 살아있는 Pod에 연결된다.

📋
Deployment= “replicas: 3 → 항상 서버 3개 유지

'이 앱은 서버 3개로 돌려라'고 선언하면 K8s가 항상 3개를 유지한다. 하나 죽으면 자동으로 새로 띄운다. Vercel이 알아서 여러 인스턴스를 관리하는 것과 같은 원리.

프론트 개발자 관점: 프론트엔드 개발자가 K8s를 직접 다룰 일은 적다. 하지만 “Pod가 3개인데 왜 특정 사용자만 에러가 나요?” 같은 질문에 대답하려면 이 구조를 이해해야 한다.

4

Next.js — 첫 페이지 로딩

page.tsx가 서버에서 실행된다는 것의 의미

Next.js App Router는 React Server Components(RSC)를 기본으로 사용한다. 서버에서 컴포넌트를 실행해 HTML을 만들고, 클라이언트에서 필요한 부분만 Hydration한다. 'use client' 안 쓰면 전부 서버 컴포넌트.

🗂️
Route 매칭= “URL → app 디렉토리에서 page.tsx 찾기

Next.js 서버가 URL을 받으면 /app 폴더에서 해당 경로의 page.tsx를 찾는다. layout.tsx → loading.tsx → page.tsx 순서로 계층을 해석한다. 이건 프론트 개발자가 매일 하는 일이니 익숙할 것.

Server Component 실행= “page.tsx를 서버에서 실행 — async/await로 데이터 직접 fetch

'use client' 안 붙인 컴포넌트는 서버에서 실행된다. async/await로 DB나 API를 직접 호출할 수 있다. 이 코드는 JS 번들에 포함되지 않으므로 API 키나 시크릿을 써도 안전하다.

📡
RSC Payload + HTML 스트리밍= “준비된 부분부터 먼저 보내기 (Suspense)

서버 컴포넌트 실행 결과를 RSC Payload(특수 JSON)로 변환하고, HTML을 스트리밍한다. Suspense의 fallback을 먼저 보내고, 데이터가 준비되면 나중에 교체. loading.tsx가 보이다가 진짜 데이터로 바뀌는 게 이것.

💧
Hydration= “서버 HTML에 onClick, useState를 연결하는 과정

서버가 보낸 HTML은 보이지만 클릭해도 반응 없다. JS가 로드되면 React가 이벤트 핸들러를 연결(Hydration)한다. 이 과정이 끝나야 onClick, useState, useEffect가 작동한다. 'use client' 컴포넌트의 JS만 다운로드.

Server Component기본 (default)
  • · async/await로 직접 데이터 페칭
  • · DB, 파일시스템, 환경변수 접근
  • · JS 번들에 포함되지 않음
  • · 서버에서만 실행, 시크릿 안전

⚠ useState, useEffect, onClick 사용 불가

Client Component'use client'
  • · 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'는 꼭 필요한 곳에만!

5

프론트엔드 → 백엔드 API 요청

fetch('/api/users') 뒤에서 일어나는 일

사용자가 버튼을 클릭하면 fetch()로 Spring 서버에 요청을 보낸다. Server Component에서 호출하면 서버 → 서버 통신(빠르고 안전), Client Component에서 호출하면 브라우저 → 서버 통신(CORS 필요).

PATH A — SERVER COMPONENT (서버 → 서버, 빠름)

RSCServer Component
fetch()K8s 내부 호출
K8s ServiceClusterIP
Spring Podbackend:8080

브라우저를 거치지 않음. CORS 불필요. API 키 노출 위험 없음.

PATH B — CLIENT COMPONENT (브라우저 → 서버)

BrowseronClick / fetch
CDN바이패스
LB로드밸런서
Ingress/api/* 라우팅
Spring Podbackend:8080

인터넷 경유. DNS → TLS → LB → Ingress 전체 스택 통과. CORS 헤더 필요.

Server Component fetch= “서버 → 서버 내부 통신 (빠르고 안전)

K8s 내부에서 Service DNS(backend-svc:8080)로 직접 호출한다. 브라우저를 거치지 않으므로 빠르고, API 키나 DB 정보가 노출될 위험이 없다. fetch('http://backend-svc:8080/api/users')처럼 사용.

🌐
Client Component fetch= “브라우저 → 서버 외부 통신 (CORS 필요)

브라우저에서 출발하므로 전체 네트워크 스택을 통과한다. CORS 설정이 필요하고, JWT나 쿠키로 인증해야 한다. fetch('/api/users', { credentials: 'include' })처럼 사용.

🔥
Server Action= “fetch 없이 서버 함수를 직접 호출 — RPC 스타일

'use server' 함수를 클라이언트에서 직접 호출한다. Next.js가 자동으로 POST 요청으로 변환. form action에 바인딩 가능. revalidatePath로 캐시도 갱신. API Route를 안 만들어도 된다.

📡
API Route= “Next.js 내장 API 엔드포인트 (route.ts)

app/api/*/route.ts에 GET, POST 핸들러를 정의한다. 프록시, 인증 미들웨어, 데이터 변환에 활용. Spring의 Controller와 같은 역할이지만 TypeScript로 작성.

프론트 개발자 관점: 가능하면 Server Component에서 fetch하라. 서버 → 서버 통신이라 빠르고, CORS도 불필요하고, API 키도 안전하다. Client Component의 fetch는 인터랙션(onClick)에서만.

6

Spring Boot 백엔드 내부 흐름

Next.js API Route의 Java 버전 — 구조는 비슷하다

Spring 서버에 요청이 도착하면 Tomcat → Filter → DispatcherServlet → Controller → Service → Repository → DB 순서로 처리된다. Next.js의 middleware.ts → route.ts → 로직함수 → DB 접근과 같은 패턴이다.

🚪
Tomcat= “next start가 3000번 포트에서 대기하는 것과 같음

Spring의 내장 웹 서버. 8080번 포트에서 HTTP 요청을 받는다. next dev가 3000번 포트에서 대기하는 것처럼, Tomcat이 8080에서 대기. 프로덕션에서도 이 서버를 그대로 쓴다.

🛡️
Filter Chain= “middleware.ts와 같은 역할

요청이 Controller에 도달하기 전에 공통 처리를 한다. 인증(JWT 검증), 인가(권한 확인), CORS, 로깅 등을 순차 실행. Next.js의 middleware.ts에서 request를 가로채서 처리하는 것과 동일한 개념.

👨‍💼
DispatcherServlet= “App Router가 URL 보고 page.tsx 찾는 것과 같음

Spring의 핵심 허브. 모든 요청을 받아서 URL 패턴을 보고 적절한 Controller 메서드를 찾아 실행한다. Next.js App Router가 /users → app/users/page.tsx를 매칭하는 것과 같다.

📋
Controller= “route.ts의 GET, POST 핸들러

HTTP 요청을 수신하고 파라미터를 바인딩한다. @GetMapping('/api/users')가 프론트의 app/api/users/route.ts export GET()과 같은 것. 입력 검증 후 Service를 호출한다.

🧠
Service= “비즈니스 로직을 분리한 함수 — Custom Hook과 비슷

핵심 업무 로직을 처리하는 곳. '회원가입 시 이메일 중복 체크 → 비밀번호 암호화 → 저장' 같은 흐름. @Transactional이 붙으면 전부 성공하거나 전부 취소(롤백)된다.

🗄️
Repository= “Prisma Client와 같은 역할 — DB 접근 함수

findById(), save(), delete() 같은 메서드로 DB를 조작한다. Spring Data JPA는 함수 이름만 정의하면 SQL을 자동 생성한다. Prisma에서 prisma.user.findUnique()처럼 쓰는 것과 같다.

TomcatHTTP 수신
Filtermiddleware
DispatcherApp Router
Controllerroute.ts
Service로직
RepositoryPrisma
DBMySQL
Spring (백엔드)Next.js (프론트)하는 일
Tomcat :8080next start :3000HTTP 요청 수신
Filter Chainmiddleware.ts요청 전처리 (인증, CORS)
DispatcherServletApp RouterURL → 핸들러 매칭
@Controllerroute.ts (GET, POST)요청/응답 처리
@Service유틸 함수 / Custom Hook비즈니스 로직
Repository (JPA)Prisma ClientDB 접근

프론트 개발자 관점: Spring의 Controller → Service → Repository는 Next.js의 route.ts → 로직함수 → Prisma와 같은 패턴이다. 이름만 다르지 구조는 같다.

7

데이터베이스 계층

localStorage의 서버 버전 — 커넥션 풀, JPA, 트랜잭션

프론트에서 localStorage에 데이터를 저장하듯, 서버는 MySQL/PostgreSQL에 저장한다. Prisma로 DB를 다뤄봤다면 JPA도 같은 개념이다. DB 커넥션을 미리 만들어두고 돌려쓰는 커넥션 풀이 핵심.

🔑
HikariCP (커넥션 풀)= “HTTP keep-alive처럼 연결을 재사용

DB 연결을 매번 새로 만들면 느리다(수십ms). 그래서 10개를 미리 만들어두고 빌려쓰고 반환한다. 프론트에서 HTTP 커넥션을 keep-alive로 재사용하는 것과 같은 원리. 열쇠가 다 나갔으면 반납될 때까지 대기.

📝
JPA / Hibernate= “Prisma ORM과 같은 것 — SQL 대신 함수로 DB 조작

SQL을 직접 안 쓰고 findById(), save() 같은 함수로 DB를 조작한다. 메서드 이름만 정의하면 SQL을 자동 생성. Prisma에서 prisma.user.findUnique({ where: { id: 1 } })로 쓰는 것과 같다.

🔄
@Transactional= “전부 성공하거나 전부 취소 — 계좌이체 원리

A 계좌에서 빼고 B 계좌에 넣는 것을 하나로 묶어서 처리. 중간에 에러나면 전부 원상복구(Rollback). 프론트에서 여러 API를 호출할 때 하나 실패하면 UI를 되돌리는 것과 비슷하지만, DB 레벨에서 보장.

⚠️
N+1 문제= “JPA 대표적 성능 함정 — 1번 조회할 걸 11번 조회

유저 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(). 원리는 같다.

8

응답 반환 — DB에서 브라우저까지

res.json() → setState → 리렌더링의 전체 경로

DB에서 데이터를 꺼내면 역순으로 돌아간다. Entity → DTO → JSON → HTTP Response → 네트워크 → 브라우저 → setState → 리렌더링.

📦
Entity → DTO → JSON= “DB 원본 → 필요한 것만 추출 → JSON 변환

DB에서 꺼낸 원본 데이터(Entity)에는 비밀번호 등 민감 정보가 있다. DTO로 필요한 필드만 복사하고, Jackson이 JSON으로 변환해서 내보낸다. res.json()으로 파싱하는 그 JSON이 이 과정을 거쳐서 온다.

🏷️
HTTP Status Code= “응답의 상태 표시 — 200, 404, 500

200: 성공, 201: 생성 완료, 400: 잘못된 요청, 401: 인증 필요, 404: 없음, 500: 서버 에러. fetch 후 response.ok나 response.status로 확인한다. if (!res.ok) throw new Error() 패턴.

🖥️
setState → 리렌더링= “데이터 도착 → state 업데이트 → Virtual DOM Diff → 화면 갱신

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 → 네트워크 → 브라우저의 전체 여정이 숨어있다.

9

전체 흐름 한눈에 보기

빌드부터 브라우저까지 — 핵심 정리

단계소요 시간병목최적화
DNS50-200ms낮음DNS Prefetch
TCP + TLS50-150ms중간HTTP/2, TLS 1.3
LB + Ingress1-5ms낮음Keep-alive
Server Component10-100ms중간캐시, Streaming, Suspense
Spring 처리5-50ms중간Virtual Threads, 캐시
DB Query1-100ms높음인덱스, N+1 방지
Hydration50-300ms높음JS 번들 최소화, Code Split
01Server Component 최대 활용

데이터 페칭은 서버에서. 'use client'는 인터랙션 필요한 최소 범위에만. JS 번들이 줄어든다.

02K8s 내부 통신 활용

Server Component → Spring은 K8s Service DNS로 호출. 인터넷 안 거치니 빠르고 CORS 불필요.

03캐시 전략 계층화

CDN(정적) → Next.js fetch 캐시(ISR) → Redis(세션) → DB. 각 계층에서 불필요한 요청을 차단.

04Vercel은 이것의 추상화

Jenkins + Docker + K8s + ArgoCD = Vercel 내부 동작 원리. 원리를 알면 어디서든 배포할 수 있다.

05Spring = Next.js API Route의 Java 버전

Controller ≈ route.ts, Service ≈ 로직함수, Repository ≈ Prisma. 구조는 같다.

06Hydration이 가장 큰 병목

50-300ms 소요. Server Component로 JS 번들을 줄이고, Code Splitting으로 필요한 것만 로드.

BEGINNER-FRIENDLY ANALOGY GUIDE

위 도표의 모든 단계를 “택배 주문”에 비유해서 설명한다

P1BUILD & DEPLOY — “상품을 만들어서 매장에 진열하는 과정”

✍️
Git Push= “레시피를 본사에 제출

개발자가 코드(레시피)를 작성하고 GitHub(본사)에 보내는 것. '이 레시피대로 만들어주세요'라고 요청하는 단계다.

🏭
Jenkins (CI)= “공장에서 상품 생산

레시피를 받은 공장(Jenkins)이 재료 검수(테스트) → 상품 제조(빌드)를 자동으로 한다. 불량품이 나오면(테스트 실패) 라인을 멈추고 개발자에게 알린다.

📦
Docker= “상품을 택배 박스에 포장

완성된 상품(앱)을 표준 규격 박스(Docker 이미지)에 넣는다. 어떤 매장(서버)에서든 이 박스만 열면 바로 진열할 수 있도록 모든 재료(라이브러리, 런타임)가 같이 들어간다. 택배 박스가 규격화되어 있으니 어디서든 동일하게 동작한다.

🏪
Registry (Harbor/ECR)= “물류 창고에 보관

포장된 박스를 물류 창고(레지스트리)에 보관한다. 'v1.2.3 박스', 'v1.2.4 박스' 이런 식으로 버전별로 정리해둔다. 필요할 때 여기서 꺼내 쓴다.

🤖
ArgoCD (CD)= “자동 진열 로봇

물류 창고의 재고 목록(Git 매니페스트)을 항상 감시하는 로봇. 새 상품이 들어오면 자동으로 매장(K8s)에 가져가서 진열한다. 목록과 매장 진열 상태가 다르면 즉시 동기화한다.

🔄
K8s Rolling Update= “영업 중에 진열대 교체

매장 문을 닫지 않고 상품을 교체하는 기술. 새 진열대를 먼저 세우고(새 Pod 생성) → 상품이 제대로 놓였는지 확인(헬스체크) → 확인되면 손님을 새 진열대로 안내 → 옛 진열대를 치운다(구 Pod 종료). 손님은 교체가 일어났는지도 모른다.

P2USER REQUEST — “손님이 매장을 찾아오는 과정”

📞
DNS= “114에 전화해서 매장 주소 물어보기

손님이 'myapp.com 매장이 어디죠?'라고 물으면, DNS(114)가 '서울시 강남구 ○○빌딩(203.0.113.50)입니다'라고 IP 주소를 알려준다. 한 번 물어보면 메모해두고(캐시) 다음엔 바로 찾아간다.

🤝
TCP + TLS= “매장 입구에서 신원확인 + 암호 통로 설정

매장 도착. TCP는 '저 손님이에요(SYN)' → '어서오세요(SYN-ACK)' → '들어갈게요(ACK)' 3번 인사로 서로를 확인하는 과정. TLS는 그 뒤에 '우리 대화를 도청 못하게 암호화합시다'라고 비밀 통로를 만드는 것. 이후 모든 대화가 암호화된다.

🏪
CDN= “동네 편의점 (전국 지점)

본점(오리진 서버)은 서울에 있지만, JS/CSS/이미지 같은 인기 상품은 전국 편의점(엣지 서버)에 미리 갖다 놓는다. 부산 손님이 서울까지 안 가고 부산 편의점에서 바로 받을 수 있다. 이게 CDN 캐시 히트. 없는 물건만 본점에 요청한다.

🚦
Load Balancer= “주차장 입구 안내원

매장에 입구가 여러 개(서버 여러 대)인데, 안내원(LB)이 '1번 입구는 지금 줄이 길어요, 2번으로 가세요' 하고 손님을 분산시킨다. 한 입구에 사람이 몰리는 걸 방지. 입구 하나가 고장나면 자동으로 다른 입구로 안내한다(헬스체크).

🛡️
Reverse Proxy= “매장 안내 데스크

손님이 직접 주방(백엔드)에 들어가면 안 되니까, 안내 데스크(프록시)가 중간에서 '어떤 용건이세요?' 하고 접수한다. 데스크가 주방 위치를 숨기고(보안), 자주 묻는 질문은 바로 답해주고(캐시), 동시에 여러 명이 같은 질문하면 주방에 한 번만 물어본다.

🚪
K8s Ingress= “매장 층별 안내판

'식당은 2층(backend:8080), 카페는 3층(frontend:3000)' 같은 안내판. myapp.com/api/* 요청은 백엔드 서비스로, 나머지는 프론트 서비스로 보내는 라우팅 규칙을 정의한다.

📍
K8s Service → Pod= “내선번호로 담당자 연결

2층 식당에 셰프(Pod)가 3명 있다. Service는 내선번호(ClusterIP)로 전화하면 비어있는 셰프에게 자동 연결해주는 교환대. 셰프가 퇴근(Pod 죽음)해도 새 셰프(새 Pod)가 같은 내선번호로 연결된다.

👨‍🍳
Next.js Server Component= “주방에서 요리를 다 만들어서 서빙

손님(브라우저) 앞에서 요리하는 게 아니라, 주방(서버)에서 완성된 요리(HTML)를 만들어 내보낸다. 손님은 재료(데이터)를 볼 필요 없이 완성된 요리만 받는다. 레시피(코드)도 주방에만 있으니 손님 눈에 안 보인다(JS 번들에 미포함).

🍳
Client Component= “손님 테이블 위 미니 화로

대부분 요리는 주방에서 오지만, 고기 굽기(인터랙션)처럼 손님이 직접 해야 하는 건 미니 화로('use client')를 테이블에 놓는다. 이 화로만 손님 짐(JS 번들)에 포함된다. 화로가 많을수록 손님 짐이 무거워지니, 꼭 필요한 것만.

💧
Hydration= “인스턴트 음식에 뜨거운 물 붓기

서버가 보낸 HTML은 '마른 라면'이다. 보기엔 완성됐는데 아직 맛(인터랙션)이 없다. JS를 로드해서 '뜨거운 물(이벤트 핸들러)'을 부으면 그때서야 클릭, 입력 등이 작동하는 진짜 음식이 된다. 이 과정이 Hydration.

P3DATA MUTATION — “손님이 주문하고 음식을 받는 과정”

📝
fetch() / onClick= “주문서 작성

손님이 메뉴판에서 '비빔밥 하나요!'(버튼 클릭) 하면, 종업원(fetch)이 주문서(HTTP POST 요청 + JSON body)를 주방에 전달한다.

🏃
LB → Ingress → Spring Pod= “주문서가 주방으로 전달되는 경로

주문서가 안내원(LB) → 층별 안내판(Ingress) → 해당 주방(Spring Pod)까지 전달되는 과정. 손님은 이 과정을 모르고 그냥 기다린다.

🚧
Tomcat + Filter Chain= “주방 입구 검문소

Tomcat(주방 문)을 지나면 Filter(검문소)를 여러 개 통과한다. '회원증(JWT) 확인하겠습니다(Security)', '이 주방에서 주문 가능한 손님이 맞나요(CORS)', '주문서 인코딩 확인(Encoding)'. 통과 못하면 거부당한다(401/403).

👨‍💼
DispatcherServlet= “주방장 (총괄 매니저)

모든 주문서가 주방장(DispatcherServlet) 한 명을 거친다. 주방장이 '이 주문은 비빔밥 담당(UserController)에게!' 하고 배분한다. 어떤 담당 셰프에게 보낼지는 메뉴 매핑표(HandlerMapping)를 보고 결정.

📋
Controller= “주문 접수 담당

주문서를 받아서 내용을 정리한다. '비빔밥 1개, 계란 추가(@RequestBody → DTO)'. 주문 내용이 이상하면 돌려보낸다(@Valid → '가격은 음수일 수 없습니다'). 정리가 끝나면 실제 요리사(Service)에게 넘긴다.

👨‍🍳
Service + @Transactional= “실제 요리하는 셰프

핵심 요리(비즈니스 로직)를 한다. @Transactional은 '이 요리는 전부 성공하거나, 전부 실패여야 한다'는 규칙. 밥은 됐는데 나물이 없으면? 이미 한 밥도 취소(ROLLBACK). 전부 완성되면 서빙(COMMIT).

🗄️
Repository + JPA= “냉장고에서 재료 꺼내기

셰프가 직접 냉장고(DB)를 열지 않는다. 보조(Repository)에게 '달걀 3개 가져와(findById)' 하면 보조가 냉장고에서 찾아온다. JPA는 '달걀'이라고 말하면 자동으로 냉장고 몇 번째 칸인지 알아내는(SQL 자동 생성) 스마트 보조.

🔑
HikariCP (DB 커넥션 풀)= “냉장고 열쇠 대여소

냉장고 열쇠(DB 커넥션)는 10개뿐인데 동시에 100명이 요리한다. 매번 열쇠를 만들면(커넥션 생성) 30초 걸리니, 미리 10개 만들어두고(풀) 빌려주고 돌려받는다. 열쇠가 다 나갔으면 반납될 때까지 줄 서서 기다린다(connectionTimeout).

🏦
DB (MySQL/PostgreSQL)= “금고 (데이터 저장소)

최종적으로 데이터가 저장되는 곳. INSERT는 '새 서류를 금고에 넣기', SELECT는 '금고에서 서류 꺼내 복사하기', UPDATE는 '기존 서류 수정', DELETE는 '서류 파쇄'. 금고 문(커넥션)을 열려면 열쇠(인증)가 필요하다.

📦
Entity → DTO → JSON= “요리를 포장해서 서빙

금고에서 꺼낸 원본 서류(Entity)에는 비밀 정보가 있다. 손님에게는 필요한 정보만 복사한 사본(DTO)을 만들고, 이걸 손님이 읽을 수 있는 형태(JSON)로 포장해서 내보낸다. 원본은 절대 밖에 안 나간다.

🖼️
setState → 리렌더링= “전광판 숫자가 바뀌는 것

주문한 음식이 도착하면(응답 수신) 전광판(React state)의 숫자를 갱신한다. React는 전광판에서 바뀐 숫자만 찾아서(Virtual DOM Diff) 그 부분만 깜빡이며 업데이트한다. 전광판 전체를 갈아끼우지 않으니 빠르다.