웹 서비스 전체 구조와 운영체제
|2026. 3. 30. 21:35
(URI) Uniform Resource Identifier
- 자원을 식별하는 가장 큰 개념.
(URL) Uniform Resource Locator
- 리소스가 어디에 있는지 위치를 나타내는 것.
- URL은 URI에 포함되는 관계이다.
HTTP 메시지는 ASCII 문자열로 되어있다.
- HTTP/1.1까지는 텍스트 기반이라 직관적이지만 무거웠다.
- 그래서 HTTP/2.0부터는 바이너리(이진) 프레이밍 계층으로 나누어 전송
HTTP 메시지 구조
- Start Line (시작 줄): HTTP/1.1 200 OK (201, 400, 401, 404, 500 등 응답 코드)
- Header (헤더): 메타 데이터 (Content-Type, 쿠키 등)
- Empty Line (공백 라인): 헤더와 바디를 구분
- Message Body (본문): 실제 데이터 (HTML, JSON 등)
PUT vs PATCH
- PUT (멱등성 O): 자원을 통째로 교체. 없으면 만들고, 있으면 덮어씌운다.
- 100번 보내도 똑같은 데이터로 덮어쓰니 결과가 같다.
- PATCH (멱등성 X): 자원의 일부만 수정한다.
- 조회수 1 증가라는 PATCH 요청은 여러 번 보내면 계속 조회수가 증가하므로 멱등하지 않다.
DELETE와 보안 이슈
- 실무에서 DELETE 메서드를 쓴다고 해서 DB에서 진짜 DELETE 쿼리를 날려 데이터를 하드 삭제(Hard Delete)하는 경우는 드뭅니다. 복구가 불가능해지기 때문입니다.
- 보통은 is_deleted = true 처럼 상태 값만 바꾸는 **소프트 삭제(Soft Delete)**를 주로 사용합니다. (이때 메서드는 팀 컨벤션에 따라 DELETE를 쓰기도, PATCH를 쓰기도 합니다.)
웹 서비스 기본 구조와 역사 (Web Client -> WS -> WAS -> DB)
HTTP 1.0과 HTML의 진화
- 기본적으로 인터넷이란, Client와 Server가 TCP/IP 연결을 맺고 그 위에서 HTTP 요청과 응답을 주고받는 환경임.
- 아주 과거에는 서버가 주는 응답이 텍스트 위주의 단순한 HTML 문서 하나였음.
- 하지만 웹이 발전하면서 사람들은 문서를 더 예쁘고 직관적으로 만들고 싶어 했음. 그래서 HTML 태그 안에 폰트, 색상 같은 시각적 요소(디자인)를 넣기 시작함.
분리의 시작 (관심사 분리)
- HTML 하나에 뼈대(정보)랑 살(디자인)이 다 섞여 있으니까 코드가 지저분해지고 유지보수가 안 되기 시작함.
- 그래서 문서의 구조(HTML)와 시각적 디자인(CSS)을 분리해서 따로 떨어져 나온 것이 CSS임.
- 프론트엔드에서 HTML(구조)과 CSS(디자인)를 분리한 것처럼, 백엔드에서도 코드가 비대해지는 걸 막기 위해 계층을 분리한 것과 결이 같음.
- 과거에 한 곳에 다 때려 넣던 코드를 역할에 따라 Controller(요청/응답), Service(비즈니스 로직), Repository(데이터 접근)로 나누게 된 아주 자연스러운 객체지향적 발전 흐름임.
HTTP 1.0의 한계와 모순 (Stateless vs Stateful)
- HTML과 CSS가 분리되면서, 이제 클라이언트는 화면을 그리기 위해 HTML뿐만 아니라 CSS, JS, 이미지 같은 정적 리소스들을 함께 응답으로 받아와야 했음.
- 여기서 초기 HTTP 통신의 근본적인 한계가 드러남. 기반 기술인 TCP/IP는 통신 전 무거운 3-Way Handshake를 거쳐야 하는 연결 지향형인데, 정작 그 위에서 동작하는 HTTP 1.0은 한 번 응답을 주면 바로 연결을 끊어버리는 비연결성(Connectionless) 특성을 가지고 있었음.
- 만약 화면을 띄우는 데 필요한 정적 리소스가 10개로 늘어난다면, 비연결성 특성상 파일 하나를 받을 때마다 매번 TCP 연결을 맺고 끊는 짓을 10번이나 반복해야 했음.
WS와 WAS의 분리 (현재 구조의 탄생 배경)
- 트래픽이 커지고 처리해야 할 정적 리소스(이미지, CSS 등)가 쏟아지면서 서버가 터지기 시작함.
- 게다가 동적인 비즈니스 로직(DB 조회 등)까지 처리해야 하니 서버 혼자서는 감당이 안 됨.
- 그래서 역할 분담을 한 것이 지금의 구조임.
- WS (Web Server - ex. Nginx, Apache): 가볍고 빠른 정적 리소스 처리 전담.
- WAS (Web Application Server - ex. Tomcat): DB를 조회하고 로직을 계산하는 무거운 동적 처리 전담.
- 이렇게 분리함으로써 WS가 앞단에서 단순 요청을 빠르게 쳐내고 로드 밸런싱을 해주며,
- WAS는 핵심 비즈니스 로직에만 집중할 수 있는 안전하고 효율적인 아키텍처가 완성된 것임.
- 추가로 HTTP 1.1에서는 Keep-Alive로 연결을 재사용하며 네트워크 비용 문제도 해결함
동적인 화면과 JavaScript의 등장 (클라이언트 사이드 실행)
- 문서가 예뻐지긴 했는데, 사용자는 이제 화면에서 버튼을 누르면 팝업이 뜨고 메뉴가 움직이는 동적인 화면(상호작용)을 원하기 시작함.
- 그래서 도입된 것이 JavaScript임. JS에서 가장 중요한 특징은 서버에 저장되어 있지만, 실행은 클라이언트(브라우저)에서 된다는 점임.
- 브라우저는 HTML(구조) -> CSS(디자인) -> Image 순으로 빠르게 화면을 먼저 그려줌(렌더링). 그다음 상대적으로 무거운 JS 엔진을 구동해 동적인 움직임을 부여함. (사용자가 화면을 빨리 보게 하기 위한 최적화)
- JS가 클라이언트에서 실행되다 보니, 해커가 악의적인 스크립트를 심어 다른 도메인의 데이터를 빼가는 보안 문제가 생김.
- 그래서 브라우저가 다른 도메인의 데이터를 함부로 가져오지 못하게 막는 보안 정책인 SOP(Same-Origin Policy)가 등장함.
- 그리고 CORS(Cross-Origin Resource Sharing)는 이 SOP 정책 때문에 프론트(ex: localhost:3000)와 백엔드 API(ex: localhost:8080)가 아예 통신하지 못하는 불편함을 해결하기 위해, 서버 측에서 예외적으로 접속을 허용하는 규칙임.
양방향 상호작용과 기억(State)의 필요성 (DB와 쿠키의 탄생)
- 여기까지는 서버가 일방적으로 문서를 내려주는 단방향이었음. 하지만 게시판에 글을 쓰고 쇼핑몰 장바구니에 물건을 담으려면 양방향 상호작용이 필요해짐.
- 양방향 상호작용을 하려면 필수적으로 상태(State)가 바뀌어야 하고, 바뀐 상태를 기억해야 함.
- 그런데 HTTP는 무상태임. 서버가 상태를 기억하지 않아야 트래픽이 몰릴 때 서버를 여러 대 늘리는 Scale-out이 쉽기 때문임.
- 이렇게 스케일 아웃의 장점을 살리면서, 기억상실증인 HTTP 위에서 상태를 유지하기 위해 고안해 낸 해결책이 바로 서버의 DB와 클라이언트의 쿠키/세션임.
- 서버의 기억: 데이터를 영구적으로 저장하는 DB(Database).
- 클라이언트의 기억: 브라우저에 정보를 임시 저장하는 쿠키(Cookie), 세션(Session). (현대에는 로컬 스토리지와 JWT 토큰으로 발전함)
Zero Trust와 MVC 패턴 (WAS의 역할)
- 클라이언트가 보내는 입력값은 절대 믿어선 안 됨(Zero Trust). 해커가 입력창에 이상한 코드를 넣을 수도 있기 때문임.
- 그래서 DB에 데이터를 넣기 전에 입력을 검증하고 비즈니스 규칙을 처리해야 했는데, 그게 WAS임.
- 데이터(Model)를 어떻게 처리할지 제어(Control)하고, 그 결과를 다시 화면(View)으로 만들어주는 이 흐름에서 MVC(Model-View-Controller) 아키텍처가 탄생함.
SSR에서 CSR로: RESTful API 서버의 탄생 (현대 웹)
- 과거에는 서버가 HTML 문서를 다 완성해서 내려주는 SSR(Server-Side Rendering) 방식이었음.
- 그런데 스마트폰이 등장하면서 문제가 생김. PC 화면, 아이폰 화면, 안드로이드 앱 등 보여줘야 할 환경(View)이 너무 많아짐. 백엔드가 이걸 다 맞춰서 HTML을 만들어주는 건 비효율적임.
- 그래서 백엔드는 핵심인 데이터(JSON)만 던져주고, 화면(HTML)은 클라이언트가 데이터를 가지고 알아서 렌더링 함.
- 이 철학을 바탕으로 CSR이 대세가 되었고, 이를 돕기 위해 프론트엔드 프레임워크(React, Vue.js 등)가 탄생함.
- 결과적으로 현대의 백엔드는 더 이상 HTML을 응답하는 웹 서버가 아니라, 클라이언트의 요청(I/O)에 따라 데이터의 CRUD를 수행하고 JSON을 반환하는 RESTful API 서버로 진화하게 됨.
운영과 모니터링 (3-Tier와 APM)
- 시스템이 이렇게 복잡해지니 어디서 병목이 생기는가를 파악하는 것이 운영의 핵심이 됨.
- 보통 WS - WAS - DB로 이어지는 구조를 3-Tier 아키텍처라고 부름.
- 장애 대응을 위해 응답 시간, DB 쿼리 속도, JVM(자바 가상 머신)의 메모리 상태 등을 실시간으로 감시해야 하는데, 이때 사용하는 도구가 APM(Application Performance Monitoring)임. (예: 제니퍼, Scouter, Pinpoint 등)
- 개발만큼이나 안정적인 운영이 핵심임.
네트워크 보안과 방어선 (DMZ, WAF)
- DMZ (Demilitarized Zone): 외부망(인터넷)과 내부망(DB 등) 사이의 완충 지대. 보통 외부와 직접 닿는 WS는 DMZ에, 중요한 비즈니스 로직과 데이터가 있는 WAS와 DB는 보호된 내부망에 배치함.
- 보안 계층:
- 네트워크 단: IPS(침입 방지 시스템)로 비정상 트래픽 차단.
- 전송 단: SSL/TLS(HTTPS)를 적용해 패킷을 탈취해도 읽을 수 없게 암호화.
- 애플리케이션 단: 클라이언트 입력 폼에 악의적인 자바스크립트를 심는 XSS, DB 쿼리를 조작하는 SQL 인젝션 등의 공격을 방어하기 위해 웹 애플리케이션 방화벽(WAF)을 WS 앞단이나 WS-WAS 사이에 배치함.
운영체제 구조
User mode와 Kernel mode
- 과거와 달리 현대의 멀티 프로세스 환경에서는 수십, 수백 개의 프로그램이 동시에 CPU와 메모리를 사용함. 여기서 OS의 핵심인 커널(Kernel)은 이 수많은 프로세스들이 자원을 독점하거나 충돌하지 않도록 스케줄링하고 제어하는 지휘자 역할을 함.
- CPU에는 권한 수준을 나누는 보호 링이라는 하드웨어적 보안 장치가 있다. (보통 Ring 0이 커널, Ring 3가 유저 모드)
- 우리가 짜는 WAS 로직, 브라우저 등은 모두 권한이 가장 낮은 User Mode에서 실행되는데, 유저 모드 프로그램이 메모리(RAM)나 디스크(SSD)를 직접 건드리게 두면, 해킹이나 버그 한 번에 서버 전체가 셧다운 되기 때문이다.
- 시스템 콜(System Call)과 컨텍스트 스위칭: WAS(User Mode)가 DB(SSD)에서 데이터를 읽고 싶지만 권한이 없으므로 커널에게 요청하는데, 이것이 시스템 콜(System Call)이다.
- 이때 CPU는 하던 일을 멈추고 제어권을 커널로 넘김 (User -> Kernel 모드 전환). 커널이 하드웨어(SSD)에서 데이터를 퍼오면, 다시 원래 프로그램으로 돌아옴 (Kernel -> User 모드 전환).
- 이 전환 과정(Context Switching)은 CPU 입장에서 굉장히 무겁고 비용이 많이 드는 작업이다. 백엔드 성능 최적화는 이 무거운 시스템 콜과 모드 전환을 어떻게 최소화할 것인가에 달려있다.
플랫폼 종속성과 JVM (Native vs Managed Code)
- 플랫폼(Platform)의 의미: 우리가 흔히 개발에서 말하는 플랫폼이란 프로그램이 실행되는 토대, 즉 하드웨어(CPU 물리적 아키텍처) + 운영체제(Kernel)의 조합을 일컫는다. 우리가 만든 애플리케이션(S/W)은 이 플랫폼 위에서 동작함.
- Native Code (C/C++ 등): 코드를 컴파일하면 특정 OS(Windows, Linux)와 특정 CPU 아키텍처(x86, ARM)가 바로 알아들을 수 있는 0과 1의 기계어로 번역된다. 하드웨어와 직결되니 매우 빠르지만, 윈도우용으로 빌드한 파일은 리눅스 서버에서 돌아가지 않는다. (플랫폼에 종속적).
- Managed Code와 JVM (Java, C# 등): "한 번 짜서 어디서든 돌리자(Write Once, Run Anywhere)"는 목표로 등장.
- JVM(Java Virtual Machine)은 OS와 하드웨어를 한데 모은 것이 아니라, OS 위에서 하나의 프로세스로 실행되며 가상의 컴퓨터(VM) 역할을 하는 소프트웨어다.
- 자바 코드는 특정 OS가 아니라 JVM이 알아들을 수 있는 바이트코드로 컴파일된다. 프로그램이 실행될 때 JVM 내의 JIT(Just-In-Time) 컴파일러가 실시간으로 해당 서버의 OS와 CPU에 맞는 Native Code로 번역해 준다.
- Managed(관리되는)의 핵심: 개발자가 메모리 할당/해제를 직접 할 필요 없이, JVM이 백그라운드에서 GC를 돌려 안 쓰는 메모리(RAM)를 알아서 청소하고 관리해 준다.
Virtual Memory
- 개념: 서버의 실제 물리 RAM이 16GB라도, 켜져 있는 프로세스 10개가 각각 개별로 16GB 메모리를 통째로 쓰고 있다고 착각하게 만드는 OS의 핵심 기술.
- 동작 원리 (Paging & Swap):
- 프로세스가 당장 실행하는 핵심 코드와 데이터만 물리적 RAM(실제 메모리)에 올려둠.
- 지금 당장 안 쓰는 데이터는 하드디스크(SSD)의 스왑(Swap) 영역으로 쫓아냄.
- 프로세스가 디스크로 쫓겨난 데이터를 달라고 요구하면, CPU는 페이지 폴트(Page Fault)라는 인터럽트를 발생시키고, 커널이 개입해 SSD에서 다시 RAM으로 데이터를 퍼옴.
- 백엔드적 관점: RAM 접근은 나노초(ns) 단위지만, SSD 접근은 밀리초(ms) 단위임 (수백만 배 차이). 서버에 트래픽이 몰려 RAM 용량이 꽉 차면, OS가 데이터를 RAM과 SSD 사이에서 매우 높은 빈도의 교체(Swapping/Thrashing)를 하느라 서버가 멈춰버리는 현상이 발생함. 이를 모니터링하는 것이 서버 운영의 기본임.
비동기 I/O vs 멀티 스레딩
- 이 둘은 처리 주체와 커널을 다루는 방식이 완전히 다름. (클라이언트 1만 명이 접속하는 상황 가정)
- 동기/블로킹 (전통적인 멀티 스레딩 - ex. Tomcat):
- 1만 개의 요청을 처리하려면 스레드도 1만 개가 필요함.
- 스레드가 DB(네트워크/디스크 I/O)에 데이터를 요청(System Call)하면, 데이터가 올 때까지 이 스레드는 CPU를 놓고 Blocking 됨.
- OS는 1만 개의 스레드를 번갈아 깨워가며(Context Switching) 관리해야 하므로, 메모리가 터지고 CPU는 스레드 교체만 하다가 과부하로 죽어버림 (C10K 문제).
- 비동기/논블로킹 (Event-Driven - ex. Node.js, Spring WebFlux):
- 프로세스(스레드)는 I/O 작업을 커널에 위임하고, 대기하지 않고 바로 다른 유저의 요청을 받으러 감.
- 커널이 백그라운드에서 하드웨어(네트워크 카드, SSD)와 통신하여 데이터를 다 가져오면, 스레드에게 이벤트(Callback)를 던져줌.
- 소수의 스레드(보통 CPU 코어 수만큼)만으로도 수만 개의 요청을 I/O 대기 없이 많은 요청을 쳐낼 수 있는 현대적이고 효율적인 구조임.
브라우저에 URL을 입력하면 일어나는 일
1. 브라우저 캐시 확인 (네트워크 요청 전)
- URL을 입력하자마자 브라우저가 가장 먼저 하는 일은 로컬(디스크/메모리)에 저장된 브라우저 캐시를 뒤지는 것임.
- 정적 리소스(이미지, JS, CSS 등)의 캐시 유효기간(max-age)이 아직 남아있다면? DNS 조회고 TCP 연결이고 다 생략하고 디스크에서 바로 화면에 뿌려버림 (가장 빠름).
- 만약 캐시가 없거나 유효기간이 지났다면, 그때서야 비로소 실제 네트워크 요청을 준비함.
2. IP 주소 획득 (DNS Lookup)
- 도메인은 사람이 읽기 위한 주소이므로, 컴퓨터가 통신하려면 실제 IP 주소가 필요하다.
- 캐시 탐색 순서: IP를 찾기 위해 브라우저 캐시 -> OS 캐시(hosts 파일 포함) -> 라우터 캐시 -> ISP(통신사) DNS 서버 순으로 뒤져봄.
- hosts 파일과 보안: 과거엔 hosts 파일에 도메인과 IP를 직접 적었음. (7.7 디도스 대란 때 해커들이 좀비 PC의 hosts 파일을 변조해서 트래픽을 특정 서버로 몰아버린 공격이 있었음. 그래서 요즘은 OS 단에서 엄격하게 보호함.)
- DNS 동작 방식 (Iterative Query): 캐시에 없다면 통신사 DNS 서버가 IP를 찾아줌.
- Root DNS (전 세계에 13개의 논리적 서버 그룹 존재)에게 ".com 서버 어딨어?" 물어봄.
- TLD DNS (.com 관리 서버)에게 "naver.com 어딨어?" 물어봄.
- Authoritative DNS (네이버의 실제 네임서버)에게 물어봐서 최종 IP 주소를 획득함.
3. 연결 수립 (TCP 3-Way Handshake & TLS)
- TCP 연결: 통신의 신뢰성을 위해 클라이언트와 서버가 3-Way Handshake 과정을 통해 연결을 수립함.
- TLS(SSL) 암호화: 현대 웹은 HTTPS가 기본임. TCP 연결 직후, 데이터를 암호화해서 주고받기 위해 TLS Handshake를 추가로 진행하여 보안 세션을 확립함.
4. HTTP 요청과 캐시 (Browser Cache)
- 연결이 맺어지면 브라우저는 서버에 HTTP Request(요청)를 보냄.
- 캐시 재검증: 1번 단계에서 캐시 유효기간이 지났다고 판단된 파일들의 경우, 무작정 새로 다운받지 않음. 브라우저가 서버에게 이 파일에 대한 수정 여부를 물어봄 (조건부 GET 요청).
- 서버가 확인해 보고 해당 파일이 변하지 않은 상태면 상태 코드 304 Not Modified만 응답함. (파일 바디를 안 보내니 네트워크 대역폭을 아낄 수 있음).
5. 서버의 처리 (WS -> WAS -> DB)
- 드디어 서버 네트워크에 트래픽이 도착함. 방화벽(IPS, WAF)을 무사히 통과한 요청은 DMZ에 위치한 WS(Web Server)가 먼저 받음.
- 단순 정적 리소스 요청이면 WS가 바로 반환하고, 게시글 조회 같은 동적 로직이면 내부망의 WAS와 DB가 비즈니스 로직을 처리(System Call, 비동기 I/O 등)한 후 결과 데이터(JSON/HTML)를 HTTP Response(응답)로 돌려줌.
6. 브라우저 렌더링
- 응답을 받은 브라우저는 화면을 그리기 시작함.
- HTML 구문 분석(DOM 트리 생성) -> CSS 렌더링 -> 동적 움직임을 위한 무거운 JS 엔진 구동 순으로 진행됨 (앞서 정리한 렌더링 최적화 순서).
- 연결이 끝나면 HTTP 특성에 따라 연결을 끊거나, HTTP/1.1 이상의 Keep-Alive를 통해 다음 요청을 위해 연결을 유지함.
