서버 내부
요청이 도착했을 때 서버 내부에서 어떤 일이 일어나는지, 라우팅과 로직 실행부터 응답 생성까지 따라가 보세요.
서버가 실제로 무엇인지
서버는 단순히 요청을 계속해서 듣고 그에 응답하는, 지속적으로 실행되는 프로그램입니다.
서버는 요청을 지속적으로 수신합니다
요청이 서버에 들어옴 → 내부에서 처리됨 → 응답이 다시 클라이언트로 나감
- 서버는 운영체제의 제어 아래 실행되는 하나의 프로그램일 뿐입니다.
- 특정 포트에서 대기하며 들어오는 요청을 기다립니다.
- 오랜 시간 살아 있으면서, 시간이 지나며 많은 요청을 처리합니다.
상세정보
“서버”라는 단어는 특별한 기계를 떠올리게 하지만, 기술적으로는 그냥 프로그램입니다. 백엔드 애플리케이션을 시작하면 운영체제가 프로세스를 생성하고 메모리에서 계속 실행되도록 유지합니다.
한 번 실행되고 종료되는 짧은 스크립트와 달리, 서버는 오래 살아 있도록 설계됩니다. 네트워크 소켓을 열고, 포트(예: 80 또는 443)에 바인딩한 뒤, 들어오는 연결을 기다립니다.
요청이 도착하면 서버는 데이터를 읽고, 처리한 다음, 응답을 보냅니다. 작업이 끝나도 종료되지 않고, 다음 요청을 계속 기다립니다.
핵심적인 사고 모델은 이것입니다: 백엔드 = 운영체제 제어 아래에서 지속적으로 실행되는 프로그램. 프레임워크, 데이터베이스, API 등 나머지 요소들은 모두 이 단순한 기반 위에 만들어집니다.
네트워킹 계층
서버는 소켓을 통해 운영 체제와 통신하고, 실제 네트워크 통신은 OS가 처리합니다.
- 서버가 네트워크 하드웨어와 직접 통신하지는 않습니다.
- 운영 체제는 네트워크 데이터를 수신하고 연결을 관리합니다.
- 소켓은 서버가 데이터를 보내고 받기 위해 사용하는 인터페이스입니다.
상세정보
서버를 시작하고 “포트에서 listen”할 때, 여러분은 소켓이라고 불리는 것을 생성하는 것입니다. 소켓은 운영 체제가 제공하는 통신 채널일 뿐입니다.
인터넷에서 데이터가 도착하면, 먼저 머신의 네트워크 하드웨어에 도달합니다. 운영 체제의 커널은 그 데이터를 처리하고, 포트 번호를 기준으로 어떤 프로그램이 받아야 하는지 결정합니다.
그다음 OS는 데이터를 올바른 소켓에 연결된 버퍼에 넣습니다. 서버는 요청을 처리할 준비가 되었을 때 그 소켓에서 데이터를 읽습니다.
즉, 서버는 원시 전기 신호나 물리적인 패킷을 직접 다루지 않습니다. OS가 제공하는 깔끔한 소프트웨어 인터페이스를 통해 데이터를 읽고 씁니다.
소켓은 백엔드 코드와 외부 세계를 연결하는 다리이며, 운영 체제는 그 아래에서 복잡한 네트워킹 세부 사항을 조용히 관리합니다.
운영 체제
서버가 작동하는 이유는 운영 체제가 보이지 않는 곳에서 하드웨어, 리소스, 그리고 격리를 관리하기 때문입니다.
- OS는 CPU, 메모리, 저장소, 네트워킹에 대한 접근을 제어합니다.
- 프로세스를 격리해서 한 프로그램이 다른 프로그램을 손상시키지 못하게 합니다.
- 실행을 스케줄링하고 리소스 할당을 관리합니다.
상세정보
운영 체제(OS)는 서버와 물리적 하드웨어 사이에 위치합니다. 백엔드 코드는 CPU, RAM, 디스크, 네트워크 카드에 직접 접근하지 않습니다. 그 역할은 OS가 합니다.
서버가 실행되면 OS는 CPU 시간을 할당하고, 메모리를 배정하고, 네트워크 소켓을 열어 주며, 파일을 읽고 쓸 수 있게 합니다. 이런 중재가 없다면 프로그램들은 하드웨어 접근을 두고 혼란스럽게 경쟁하게 됩니다.
OS는 격리도 강제합니다. 각 서버 프로세스는 자체 가상 메모리 공간을 가지므로, 실행 중인 다른 프로그램과의 우발적인 간섭을 막을 수 있습니다.
고성능 백엔드 시스템도 결국 OS 스케줄러에 전적으로 의존해 언제 실행할지, 얼마나 오래 실행할지를 결정합니다. 어떤 서버도 이 계층을 우회하지 않습니다.
간단히 말해, 운영 체제는 서버 실행을 가능하게 하는 보이지 않는 기반입니다.
동시성 모델
서버는 동시에 많은 요청을 처리해야 하며, 동시성 모델이 그 방식을 결정합니다.
- 여러 요청이 동시에 도착할 수 있습니다.
- 서버는 스레드 또는 이벤트 루프를 사용해 동시 작업을 관리합니다.
- 동시성 모델은 성능과 확장성에 직접적인 영향을 줍니다.
상세정보
실제 시스템에서는 요청이 하나씩 순서대로 도착하지 않습니다. 수백 또는 수천 개의 클라이언트가 동시에 요청을 보낼 수 있습니다. 서버는 다른 요청을 멈추게 하거나 블로킹하지 않으면서 여러 요청을 계속 처리할 수 있어야 합니다.
한 가지 일반적인 방법은 스레드 기반 모델입니다. 여러 스레드가 병렬로 실행되며 각 스레드가 하나의 요청을 처리합니다. 이 방식은 이해하기 쉽지만, 너무 많은 스레드를 생성하면 메모리와 CPU를 많이 사용할 수 있습니다.
또 다른 방법은 이벤트 기반 모델입니다. 하나의 스레드가 논블로킹 I/O를 사용하고 요청을 비동기적으로 처리합니다. 데이터베이스 호출처럼 느린 작업을 기다리는 대신, 다른 작업을 계속 수행하다가 준비되면 다시 이어서 처리합니다.
두 모델 모두 자원을 낭비하지 않으면서 처리량을 최대화하는 것을 목표로 합니다. 어떤 동시성 모델을 선택하느냐에 따라 서버가 높은 부하에서 얼마나 잘 동작하는지가 달라집니다.
동시성을 이해하는 것은 매우 중요합니다. 여러 요청을 효율적으로 처리하는 능력이 있어야 단순한 프로그램이 확장 가능한 백엔드 시스템이 되기 때문입니다.
요청 생명주기
서버의 관점에서 보면, 요청은 OS, 프레임워크, 핸들러 코드, 그리고 다시 응답으로 돌아가는 과정을 거칩니다.
- 운영 체제는 네트워크 데이터를 수신하여 서버 프로세스에 전달합니다.
- 프레임워크는 요청을 파싱하고 올바른 핸들러로 라우팅합니다.
- 코드는 로직을 실행하고, 필요하면 데이터베이스와 통신한 뒤 응답을 반환합니다.
상세정보
클라이언트가 HTTP 요청을 보내면, 먼저 머신의 네트워크 인터페이스에 도달합니다. 운영 체제는 패킷을 처리하고 데이터를 서버와 연결된 소켓 버퍼에 넣습니다.
그다음 서버가 들어오는 데이터를 읽고, 프레임워크(예: Express, Django, FastAPI)가 이를 파싱합니다. 여기에는 헤더, URL 경로, 쿼리 파라미터, 요청 본문을 추출하는 작업이 포함됩니다.
이후 프레임워크는 요청을 적절한 핸들러 함수 — 여러분이 작성한 코드의 일부 — 로 라우팅합니다. 여기서 비즈니스 로직이 실행됩니다: 입력 검증, 계산 수행, 데이터베이스 조회, 외부 서비스 호출 등이 이루어집니다.
핸들러가 끝나면 응답 객체를 반환합니다. 프레임워크는 이를 HTTP 응답으로 변환하고, OS는 이를 네트워크 스택을 통해 전송하며, 최종적으로 클라이언트로 돌아갑니다.
서버의 관점에서 이 생명주기는 계속 반복됩니다: 수신 → 처리 → 응답. 모든 백엔드 요청은 이 구조화된 흐름을 따릅니다.
핸들러 내부
핸들러는 서버의 실제 비즈니스 로직이 실행되고, 요청을 의미 있는 응답으로 바꾸는 곳입니다.
if (!req.body.email) {
return error("missing email")
}
if (!user.canPurchase) {
return error("permission denied")
}const price = cart.total()
if (price > creditLimit) {
throw new Error("limit")
}
applyDiscount(cart)const user = await db.users.find(id) await cache.set( "cart:" + user.id, cart )
return {
status: "success",
orderId: order.id,
total: price
}- 핸들러는 들어오는 요청 데이터를 검증하고 해석합니다.
- 계산이나 규칙 검사 같은 비즈니스 로직을 실행합니다.
- 응답을 반환하기 전에 캐시, 데이터베이스, 또는 외부 서비스와 상호작용할 수 있습니다.
상세정보
프레임워크가 요청을 파싱한 뒤에는, 여러분이 직접 작성한 코드인 handler function을 호출합니다. 실제 작업은 여기서 이루어집니다.
핸들러는 보통 입력 검증부터 시작합니다. 필요한 필드가 존재하는지, 값의 형식이 올바른지, 그리고 사용자가 해당 작업을 수행할 권한이 있는지를 확인합니다.
그다음은 비즈니스 로직입니다. 여기에는 계산, 규칙 적용, 데이터 변환, 또는 쿼리 준비가 포함될 수 있습니다. 필요하다면 핸들러는 데이터베이스를 호출하거나, 캐시에 접근하거나, 다른 서비스와 통신할 수 있습니다.
작업이 끝나면 핸들러는 response object를 구성합니다. 여기에는 JSON 데이터, 성공 메시지, 또는 오류 설명이 포함될 수 있습니다.
핸들러는 백엔드 시스템의 핵심입니다. 그 앞의 모든 과정은 요청을 준비하고, 그 뒤의 모든 과정은 결과를 전달합니다. 하지만 실제로 결정이 내려지는 곳은 핸들러 내부입니다.
캐싱 계층
캐시는 자주 사용되는 데이터를 빠른 메모리에 저장해 두어, 서버가 매번 데이터베이스를 조회하지 않아도 되게 합니다.
- 서버는 데이터베이스를 조회하기 전에 먼저 캐시를 확인합니다.
- 캐시 적중(cache hit)이 발생하면 비싼 데이터베이스 작업 없이 데이터를 빠르게 반환합니다.
- 캐시 미스(cache miss)가 발생하면 데이터베이스를 조회한 뒤, 결과를 저장해 나중에 다시 사용합니다.
상세정보
데이터베이스는 강력하지만 메모리에 비해 상대적으로 느립니다. 모든 요청이 직접 데이터베이스를 조회하면, 트래픽이 많을 때 시스템이 금방 병목이 됩니다.
캐시는 최근에 사용했거나 자주 접근하는 데이터를 빠른 메모리(보통 RAM)에 저장합니다. 요청이 들어오면 서버는 먼저 필요한 데이터가 캐시에 이미 있는지 확인합니다.
데이터를 찾으면(캐시 적중), 서버는 즉시 결과를 반환할 수 있습니다. 이렇게 하면 지연 시간과 데이터베이스 부하가 크게 줄어듭니다.
데이터를 찾지 못하면(캐시 미스), 서버는 데이터베이스를 조회해 결과를 가져오고, 이를 캐시에 저장한 뒤 클라이언트에 반환합니다.
캐싱은 성능과 확장성을 향상시키지만, 캐시된 데이터와 데이터베이스의 일관성을 유지하는 것 같은 복잡성도 추가합니다. 올바르게 구현하면 시스템 부담을 크게 줄일 수 있습니다.
데이터베이스 상호작용
데이터가 메모리나 캐시에 없을 때, 서버는 데이터베이스를 조회하여 영구적인 정보를 가져오거나 수정합니다.
- 핸들러는 저장된 데이터가 필요할 때 데이터베이스에 쿼리를 보냅니다.
- 데이터베이스는 쿼리를 처리하고 결과를 반환합니다.
- 데이터베이스의 속도와 설계는 서버 성능에 직접적인 영향을 줍니다.
상세정보
데이터베이스는 사용자 계정, 주문, 메시지, 애플리케이션 상태와 같은 데이터를 영구적으로 저장하는 역할을 합니다. 서버가 메모리나 캐시에 없는 정보를 필요로 할 때, 데이터베이스에 쿼리를 보냅니다.
데이터베이스는 쿼리를 파싱하고, 인덱스와 테이블 같은 저장 구조를 검색한 뒤, 요청된 데이터를 반환합니다. 이 과정은 디스크 접근이나 복잡한 조회가 필요할 수 있으므로, 일반적으로 메모리 내 연산보다 느립니다.
결과를 받은 후 서버는 데이터를 변환하거나, 비즈니스 로직을 적용하거나, 응답 형식으로 가공한 뒤 클라이언트에 다시 보낼 수 있습니다.
최적화되지 않은 쿼리, 부족한 인덱스, 또는 높은 트래픽은 데이터베이스를 병목 지점으로 만들 수 있습니다. 그래서 데이터베이스 설계와 쿼리 효율성은 백엔드 성능에 매우 중요합니다.
대부분의 실제 시스템에서 데이터베이스 상호작용은 요청 생명주기에서 가장 비용이 큰 부분 중 하나입니다.
프로세스 생명주기 및 리소스 관리
서버 프로세스에는 생명주기가 있습니다. 시작하고, 소켓과 파일 디스크립터 같은 리소스를 획득한 뒤, 계속 실행되며, 마지막에는 깔끔하게 종료되어야 합니다.
- 시작 시 서버는 설정을 초기화하고, 소켓을 열고, 의존 서비스에 연결합니다.
- 실행 중에는 메모리, 파일 디스크립터, 네트워크 연결 같은 리소스를 보유합니다.
- 종료 시에는 연결을 닫고 리소스를 안전하게 해제해야 합니다.
상세정보
서버 프로세스가 시작되면 초기화 단계를 수행합니다. 여기에는 보통 설정 로드, 리스닝 소켓 열기, 데이터베이스 연결, 캐시 준비가 포함됩니다.
실행되는 동안 프로세스는 여러 개의 file descriptors를 소유합니다. 이는 네트워크 소켓, 열린 파일, 파이프 같은 리소스를 가리키는 핸들입니다. 운영 체제는 이를 추적하며, 동시에 열 수 있는 개수에는 시스템 제한이 있습니다.
서버가 파일 디스크립터를 누수하거나 연결을 제대로 닫지 못하면, 결국 시스템 한도에 도달해 새 요청을 받지 못할 수 있습니다.
배포, 스케일링, 장애 등 어떤 이유로 종료하더라도, 잘 설계된 서버는 graceful shutdown을 수행합니다. 새 요청 수락을 중지하고, 진행 중인 작업을 마무리하며, 데이터베이스 연결을 닫고, 리소스를 해제합니다.
적절한 생명주기 관리는 메모리 누수, 디스크립터 고갈, 재시작 중의 불일치 상태를 방지합니다. 안정적인 백엔드 시스템은 엄격한 리소스 관리에 크게 의존합니다.
서버 장애 시나리오
서버는 여러 가지 이유로 실패할 수 있으며, 실패가 어디에서 발생하는지 이해하면 더 안정적인 시스템을 설계하는 데 도움이 됩니다.
- 애플리케이션 오류는 크래시나 500 응답을 유발할 수 있습니다.
- 리소스 고갈(CPU, 메모리, 파일 디스크립터)은 서버가 정상적으로 동작하지 못하게 만들 수 있습니다.
- 데이터베이스 같은 외부 의존성은 병목이 되거나 완전히 실패할 수 있습니다.
상세정보
실패는 서버 스택의 여러 계층에서 발생할 수 있습니다. 애플리케이션 수준에서는 처리되지 않은 예외나 로직 버그로 인해 500 오류가 발생하거나 프로세스가 크래시할 수 있습니다.
리소스 수준에서는 트래픽이 많아지면 CPU, 메모리, 파일 디스크립터 한도가 고갈될 수 있습니다. 예를 들어, 열린 연결이 너무 많으면 새로운 클라이언트가 연결하지 못할 수 있습니다. 메모리 사용량이 과도하면 운영 체제가 out-of-memory (OOM) kill을 실행할 수 있습니다.
동시성 문제도 문제를 일으킬 수 있습니다. 블로킹 작업은 요청 타임아웃으로 이어질 수 있고, 레이스 컨디션은 일관되지 않은 동작을 만들어낼 수 있습니다.
외부 시스템은 추가적인 실패 지점을 만듭니다. 데이터베이스가 느려지거나 사용할 수 없게 되면 요청이 쌓이고 시스템 전반의 지연 시간이 증가할 수 있습니다.
이러한 실패 시나리오를 이해하는 것은 매우 중요합니다. 실제 백엔드 시스템은 이상적인 조건에서 얼마나 잘 동작하는지가 아니라, 문제가 생겼을 때 어떻게 동작하는지로 평가되기 때문입니다.
질문 섹션
1 / 5