백엔드 왕국(서버 시스템)의 신입 기사는 이제 코드를 짜는 법과 운영하는 법을 어느 정도 알게 되었다. 하지만 왕은 새로운 질문을 던졌다.
“좋다. 그런데 그 서버는 어디에 세울 것이냐? 누가 들어올 수 있고, 누가 못 들어오게 할 것이냐? 파일은 어디에 저장할 것이냐? 요청이 몰릴 때 일꾼은 기다릴 것이냐, 다른 일을 할 것이냐?”
신입 기사는 깨달았다. 백엔드는 코드만으로 서 있지 않았다. 땅(네트워크), 성벽(보안), 도로(라우팅), 창고(스토리지), 일꾼의 대기 방식(I/O 모델)이 함께 맞물려야 진짜 서비스가 되었다.
왕국을 세우기 전, 건축가는 먼저 큰 지도(인프라 아키텍처)를 그렸다. 지도에는 손님이 들어오는 길, 서버가 사는 구역, 금고(DB)가 숨는 구역, 바깥 세상으로 나가는 길이 모두 표시되어야 했다.
첫 번째로 정한 것은 땅의 경계(VPC)였다. VPC는 왕국이 차지하는 사설 네트워크 영역이었다. 이 안에서 어떤 주소 대역을 쓸지 정하고, 서버와 DB를 어느 구역에 둘지 나눴다.
두 번째는 밝은 거리(Public Subnet)와 안쪽 거리(Private Subnet)를 나누는 일이었다. 손님이 직접 찾아와야 하는 입구 서버나 로드밸런서는 Public Subnet에 둘 수 있었다. 하지만 DB, 캐시, 내부 API 서버처럼 직접 노출될 필요가 없는 자원은 Private Subnet에 두는 것이 안전했다.
세 번째는 외출 문(NAT Gateway)이었다. Private Subnet에 있는 서버는 바깥 인터넷에서 직접 들어올 수 없지만, 업데이트 파일을 받거나 외부 API를 호출해야 할 수 있다. 이때 안쪽 서버가 바깥으로만 나갈 수 있게 해 주는 문이 NAT Gateway였다.
네 번째는 길 안내표(Route Table)였다. 어떤 요청이 인터넷 관문(Internet Gateway)으로 갈지, NAT Gateway로 갈지, 내부망으로 갈지 정하는 표였다. 서버가 같은 VPC에 있어도 길 안내표가 맞지 않으면 서로 통신하지 못할 수 있었다.
다섯 번째는 성문 규칙(Security Group과 Firewall)이었다. 서버는 필요한 문만 열어야 했다. 웹 서버는 80, 443을 열 수 있지만, DB의 3306이나 5432는 외부 전체에 열면 안 된다. DB는 애플리케이션 서버에서만 들어오게 하고, 운영자는 VPN이나 Bastion을 거쳐 들어오게 하는 식으로 문을 좁혀야 했다.
인프라 설계의 핵심은 “돌아가게 만들기”가 아니라 “잘못 들어올 수 없게 만들기”였다. 처음에는 조금 불편해도, 네트워크 경계와 접근 경로를 명확히 잡아두면 사고 가능성이 크게 줄어든다.
왕국의 운영 서버 배치소(Server Provisioning)는 실제 일꾼들이 살 방을 만드는 곳이었다. 여기서 가장 먼저 고르는 것은 서버의 크기(Instance Type)였다. CPU를 많이 쓰는 계산 서버인지, 메모리를 많이 쓰는 캐시 서버인지, 디스크 I/O가 중요한 로그 서버인지에 따라 맞는 방이 달랐다.
서버 하나만 세우면 간단하지만, 그 서버가 죽으면 서비스도 같이 멈춘다. 그래서 왕국은 여러 방을 나눠 두었다. 서로 다른 건물 구역(Availability Zone)에 서버를 나눠 놓으면, 한 구역에 문제가 생겨도 다른 구역이 버틸 수 있었다.
성문 앞에는 교통 분배관(Load Balancer)이 섰다. 손님은 개별 서버 주소를 알 필요 없이 분배관 주소만 바라봤다. 분배관은 살아 있는 서버에게 요청을 나눠 보냈고, 아픈 서버는 건강 확인(Health Check)에서 제외했다.
일꾼 수 자동 조절 장치(Auto Scaling)는 손님이 많아지면 서버를 늘리고, 한가해지면 줄였다. 하지만 아무 기준 없이 늘리면 비용이 커지고, 너무 늦게 늘리면 장애가 난다. 그래서 CPU 사용률, 요청 수, 큐 적체량 같은 지표를 기준으로 조절했다.
운영자는 서버에 직접 들어가 수작업으로 설정을 바꾸고 싶은 유혹을 받는다. 하지만 손으로 고친 서버는 나중에 똑같이 재현하기 어렵다. 그래서 왕국은 인프라 설계도(IaC, Infrastructure as Code)를 사용했다. Terraform, CloudFormation 같은 도구로 네트워크, 서버, DB, 보안 규칙을 코드처럼 관리했다.
좋은 운영 서버는 “지금 살아 있음”만으로 충분하지 않았다. 다시 만들 수 있어야 했고, 누가 언제 어떤 설정을 바꿨는지 추적할 수 있어야 했다.
서버 안으로 들어가면 리눅스 성(Linux Server)이 있었다. 성에는 여러 층이 있었다. 가장 아래에는 장비를 직접 다루는 심장(Kernel)이 있었고, 그 위에는 명령을 내리는 도구(Shell), 파일과 프로세스를 관리하는 여러 집사(System Utilities)가 있었다.
리눅스 성에서는 모든 것이 파일처럼 보이는 경우가 많았다. 일반 파일, 디렉터리, 장치, 소켓, 파이프가 모두 파일 인터페이스로 다뤄질 수 있었다. 그래서 개발자는 파일을 열고 읽고 쓰는 방식으로 다양한 자원을 다룰 수 있었다.
왕국의 일꾼 목록(Process Table)에는 지금 실행 중인 프로세스들이 기록되었다. ps, top, htop 같은 거울을 보면 어떤 프로세스가 CPU와 메모리를 쓰는지 볼 수 있었다. 문제가 생겼을 때 “서버가 느리다”에서 멈추지 않고, 어떤 프로세스가 어떤 자원을 먹고 있는지 확인해야 했다.
서버의 기억 창고(Memory)는 빠르지만 한정되어 있었다. 메모리가 부족하면 디스크 일부를 임시 기억처럼 쓰는 공간(Swap)을 사용할 수 있었다. 하지만 Swap은 실제 메모리보다 훨씬 느리다. Swap 사용량이 계속 늘어난다는 것은 메모리 압박이 심하다는 신호일 수 있었다.
서버의 서비스 관리자(systemd)는 데몬(Service)을 켜고 끄고 감시했다. systemctl status로 상태를 보고, journalctl로 서비스 로그를 확인했다. 운영 서버에서는 애플리케이션이 그냥 터미널에서 실행되는 것이 아니라, 서비스 단위로 관리되는 경우가 많았다.
운영 기사는 서버 안에서 세 가지 질문을 자주 던졌다.
느린 서버를 볼 때 이 세 질문을 순서대로 확인하면, 막연한 감각보다 훨씬 빠르게 원인에 접근할 수 있다.
리눅스 성의 파일시스템(File System)은 물건을 이름과 위치로 보관하는 창고였다. 파일은 내용만 있는 것이 아니라, 주인, 권한, 크기, 수정 시간, 실제 블록 위치 같은 정보도 함께 가지고 있었다. 이 정보를 담는 표식이 inode였다.
파일 이름은 창고 물건 자체가 아니라, inode를 가리키는 이름표에 가까웠다. 그래서 같은 inode를 여러 이름으로 가리킬 수도 있었다. 이것이 하드 링크(Hard Link)였다. 반대로 심볼릭 링크(Symbolic Link)는 다른 경로를 가리키는 바로가기 표식이었다.
프로세스가 파일을 열면 파일 손잡이(File Descriptor)를 받았다. 이 손잡이는 열린 파일, 소켓, 파이프 등을 가리키는 번호였다. 손잡이를 너무 많이 열고 닫지 않으면 “too many open files” 같은 문제가 생길 수 있었다. 운영 서버에서는 파일 디스크립터 제한(ulimit)도 중요한 설정이었다.
파일을 쓴다고 해서 항상 즉시 디스크에 박히는 것은 아니었다. 운영체제는 빠른 처리를 위해 페이지 캐시(Page Cache)에 데이터를 잠시 담아둘 수 있었다. 그래서 쓰기 호출이 성공해도 실제 디스크 반영은 조금 뒤에 일어날 수 있다. 반드시 디스크에 내려야 하는 중요한 기록은 fsync 같은 동기화 호출이 필요할 수 있다.
저널링 파일시스템(Journaling File System)은 장부를 고치기 전, 변경 예정 기록을 먼저 남겼다. 갑자기 전원이 나가도 “어디까지 고치다 말았는지”를 파악해 복구하기 쉽게 만드는 방식이었다. ext4, XFS 같은 파일시스템은 운영 환경에서 자주 쓰인다.
파일시스템 장애는 디스크 용량만 보는 것으로 끝나지 않는다. 용량은 남았는데 inode가 다 떨어질 수도 있다. 작은 파일이 너무 많이 생기면 저장 공간은 남아 있어도 새 파일을 만들 수 없다. 그래서 운영자는 df -h로 용량을 보고, df -i로 inode도 확인해야 한다.
로그 파일도 창고를 가득 채울 수 있다. 로그가 계속 쌓이면 디스크가 꽉 차고, DB나 애플리케이션이 더 이상 쓰지 못해 장애가 날 수 있다. 그래서 로그 회전(Log Rotation)을 설정해 오래된 로그를 압축하거나 삭제한다.
파일 저장소에도 종류가 있었다. 서버 안 디스크(Local Disk)는 빠르지만 서버가 사라지면 같이 사라질 수 있다. 블록 스토리지(Block Storage)는 서버에 붙이는 외장 디스크처럼 쓸 수 있다. 파일 스토리지(File Storage)는 여러 서버가 함께 마운트해 쓸 수 있다. 객체 스토리지(Object Storage)는 이미지, 백업, 첨부파일처럼 HTTP API로 저장하고 꺼내는 큰 창고에 가깝다.
실무에서는 사용자가 올린 첨부파일을 애플리케이션 서버 로컬 디스크에만 저장하면 위험하다. 서버가 여러 대면 어느 서버에 파일이 있는지 달라지고, 서버 교체 시 파일이 사라질 수 있다. 그래서 첨부파일은 보통 객체 스토리지나 공유 스토리지로 분리한다.
서버의 일꾼(Thread)은 CPU 계산만 하는 것이 아니었다. 파일을 읽고, DB에 묻고, 외부 API를 호출하고, 네트워크 응답을 기다렸다. 이런 입출력 작업을 I/O라고 했다.
I/O의 핵심 문제는 기다림이었다. CPU 계산은 일꾼이 직접 머리를 쓰는 시간이다. 반면 DB 응답이나 네트워크 응답을 기다리는 시간은 일꾼이 아무 일도 못 하고 기다릴 수 있는 시간이다.
블로킹 I/O(Blocking I/O)는 일꾼이 창구 앞에서 답이 올 때까지 서 있는 방식이었다. 예를 들어 DB에 쿼리를 보낸 뒤 결과가 올 때까지 그 Thread가 기다린다. 이해하기 쉽고 코드 흐름도 단순하지만, 동시에 많은 요청이 오면 기다리는 Thread가 많이 필요하다.
논블로킹 I/O(Non-blocking I/O)는 창구에 물어봤을 때 준비가 안 됐으면 “아직 없음”이라는 답을 바로 받고 돌아오는 방식이었다. 일꾼은 그 자리에서 멈추지 않고 다른 일을 할 수 있다. 다만 준비됐는지 계속 확인하거나, 이벤트 알림 구조와 함께 써야 한다.
이벤트 알림소(Event Demultiplexer)는 여러 창구를 대신 지켜보다가 “이 소켓은 읽을 준비가 됐다”, “저 소켓은 쓸 준비가 됐다”고 알려준다. 리눅스에서는 select, poll, epoll 같은 방식이 이 역할을 한다. 많은 연결을 적은 Thread로 다루는 서버는 이런 구조를 활용한다.
파일 I/O와 네트워크 I/O는 성격이 조금 다르다. 네트워크 소켓은 준비 여부를 이벤트로 감시하기 좋다. 반면 일반 파일 읽기는 파일시스템과 디스크, 페이지 캐시 상태에 따라 동작이 달라진다. 그래서 “논블로킹”이라는 말을 쓸 때는 대상이 소켓인지 파일인지 구분해야 한다.
좋은 서버는 요청을 처리할 때 “어디서 기다리는가”를 알아야 한다. CPU가 바쁜지, DB 응답을 기다리는지, 외부 API를 기다리는지, 디스크 쓰기를 기다리는지에 따라 해결 방법이 완전히 달라진다.
왕국의 시험관은 신입 기사에게 네 단어를 던졌다. 동기, 비동기, 블로킹, 논블로킹. 많은 기사들이 이 단어를 섞어 썼지만, 사실 두 축이 다르다.
동기(Synchronous)와 비동기(Asynchronous)는 결과를 받는 방식에 대한 이야기다. 동기는 일을 시킨 사람이 결과가 끝나는 흐름까지 직접 챙기는 방식이다. 비동기는 일을 맡긴 뒤 나중에 알림, 콜백, Future, Promise, 이벤트, 큐 같은 방식으로 결과를 받는 방식이다.
블로킹(Blocking)과 논블로킹(Non-blocking)은 기다리는 동안 실행 흐름이 멈추는지에 대한 이야기다. 블로킹은 호출한 Thread가 결과가 준비될 때까지 멈춘다. 논블로킹은 결과가 아직 없으면 즉시 돌아와 다른 일을 할 수 있다.
둘은 같은 말이 아니다.
동기 + 블로킹은 손님이 창구에 서서 “끝날 때까지 여기서 기다릴게요”라고 말하는 방식이다.
요청 Thread
-> DB 쿼리 호출
-> 결과가 올 때까지 대기
-> 결과를 받아 다음 로직 실행
전통적인 Servlet MVC와 JDBC 조합에서 자주 보는 방식이다. 코드는 읽기 쉽고 디버깅하기 좋다. 하지만 요청이 많고 외부 대기가 길면, 기다리는 Thread가 늘어나 Thread Pool이 고갈될 수 있다.
동기 + 논블로킹은 손님이 창구에 계속 물어보는 방식이다.
요청자
-> 준비됐나요?
-> 아직입니다
-> 다른 일 잠깐
-> 준비됐나요?
-> 됐습니다
호출은 즉시 돌아오지만, 결과가 필요하기 때문에 호출자가 직접 준비 여부를 확인한다. 단순히 반복 확인(Polling)을 많이 하면 CPU를 낭비할 수 있다. 그래서 실무에서는 이벤트 알림 방식과 함께 설계하는 경우가 많다.
비동기 + 블로킹은 일을 다른 작업자에게 맡기지만, 그 작업자 내부에서는 블로킹 방식으로 처리하는 구조에서 볼 수 있다.
API Thread
-> 작업 큐에 일 등록
-> 바로 응답
Worker Thread
-> 외부 API 호출
-> 응답 올 때까지 대기
-> 결과 저장
사용자 요청 관점에서는 비동기다. 하지만 뒤쪽 Worker는 블로킹 I/O를 사용할 수 있다. 메일 발송, 정산, 리포트 생성처럼 사용자에게 즉시 결과를 줄 필요가 없는 작업에 자주 쓰인다.
비동기 + 논블로킹은 적은 일꾼이 많은 창구를 이벤트로 다루는 방식이다.
Event Loop
-> 요청 등록
-> 다른 요청 처리
-> I/O 준비 이벤트 수신
-> 콜백 또는 다음 단계 실행
Node.js, Netty, WebFlux 같은 모델에서 자주 언급된다. Thread를 많이 만들지 않고도 많은 연결을 다룰 수 있다. 하지만 코드 흐름이 복잡해질 수 있고, 중간에 블로킹 호출이 섞이면 이벤트 루프 전체가 느려질 수 있다.
정리하면 다음과 같다.
| 구분 | 핵심 질문 | 의미 |
|---|---|---|
| 동기/비동기 | 결과를 누가, 어떤 흐름으로 받는가? | 결과 통지 방식 |
| 블로킹/논블로킹 | 기다리는 동안 Thread가 멈추는가? | 실행 대기 방식 |
면접이나 실무 대화에서는 “비동기라서 논블로킹이다”라고 단정하면 위험하다. 비동기 작업 안에서도 블로킹 I/O를 쓸 수 있고, 논블로킹 호출도 결과를 동기적으로 계속 확인하면 동기 흐름이 될 수 있다.
왕국의 앞문에는 웹 서버(Web Server)가 있었다. Nginx나 Apache 같은 문지기는 정적 파일을 빠르게 내보내고, TLS를 처리하고, 요청을 뒤쪽 애플리케이션 서버로 전달했다.
뒤쪽에는 애플리케이션 서버(WAS)가 있었다. Spring Boot, Tomcat 같은 일꾼들이 비즈니스 로직을 처리하고 DB와 대화했다. 모든 일을 WAS가 직접 해도 되지만, 앞단 웹 서버나 로드밸런서가 정적 파일, 압축, TLS, 라우팅을 맡으면 역할이 깔끔해진다.
리버스 프록시(Reverse Proxy)는 손님 대신 뒤쪽 서버를 찾아주는 문지기였다. 손님은 내부 서버 주소를 몰라도 되고, 운영자는 프록시에서 라우팅, 인증 일부, 헤더 처리, 압축, 캐싱을 조절할 수 있었다.
TLS 종료(TLS Termination)는 암호화 통신을 어느 지점에서 풀지 정하는 문제였다. 로드밸런서나 Nginx에서 TLS를 풀면 뒤쪽 WAS는 HTTP로 받을 수 있어 설정이 단순해진다. 하지만 내부망 보안 요구가 강하면 뒤쪽까지 다시 암호화할 수도 있다.
정적 파일(Static Assets)은 가능하면 애플리케이션 서버 부담을 줄이는 방향으로 배치한다. 이미지, CSS, JS 파일은 CDN이나 객체 스토리지, 웹 서버에서 처리하고, WAS는 동적 API 처리에 집중하게 할 수 있다.
운영 트래픽 흐름은 보통 다음과 같이 그릴 수 있다.
사용자
-> DNS
-> CDN 또는 Load Balancer
-> Nginx 또는 Ingress
-> Application Server
-> DB / Cache / Queue / Object Storage
이 흐름을 머릿속에 그릴 수 있어야 장애가 났을 때 어느 구간을 먼저 볼지 판단할 수 있다.
왕국은 모든 요청을 본성까지 들여보내지 않았다. 자주 찾는 물건은 앞쪽 전초기지(Cache)에 두었다. 캐시가 잘 맞으면 DB와 애플리케이션 서버의 부담이 줄고 응답도 빨라졌다.
브라우저 캐시(Browser Cache)는 손님 주머니에 저장되는 작은 창고였다. Cache-Control, ETag, Last-Modified 같은 표식으로 다시 받을지, 그대로 쓸지 결정했다.
CDN(Content Delivery Network)은 세계 여러 지역에 있는 전초기지였다. 사용자는 가까운 CDN 지점에서 이미지, JS, CSS 같은 정적 파일을 받았다. 원본 서버까지 매번 오지 않아도 되므로 속도와 안정성이 좋아졌다.
서버 캐시(Server Cache)는 API 결과나 자주 쓰는 데이터를 Redis 같은 빠른 창고에 담는 방식이었다. 하지만 캐시는 원본이 아니다. 원본 DB가 바뀌었을 때 캐시를 언제 지울지, 언제 새로 채울지 정해야 한다.
캐시에서 가장 어려운 문제는 빠르게 만드는 것이 아니라, 틀린 값을 오래 보여주지 않는 것이다. 그래서 TTL을 두거나, 데이터 변경 시 캐시를 지우거나, 버전이 들어간 키를 쓰는 전략을 선택한다.
캐시 폭주(Cache Stampede)도 조심해야 한다. 인기 있는 캐시가 동시에 만료되면 많은 요청이 한꺼번에 DB로 몰릴 수 있다. 이를 막기 위해 만료 시간을 조금씩 다르게 주거나, 한 요청만 원본을 갱신하게 하는 잠금 전략을 쓸 수 있다.
이제 신입 기사는 실제로 작은 서비스를 운영한다고 상상했다. 왕은 구축 순서를 이렇게 적어 주었다.
왕은 말했다.
“구축은 한 번에 화려하게 끝내는 일이 아니다. 작게 시작하되, 나중에 커졌을 때 어디를 바꿔야 하는지 알고 있어야 한다.”
처음부터 모든 것을 쿠버네티스로 올릴 필요는 없을 수 있다. 작은 서비스라면 관리형 DB, 로드밸런서, VM 몇 대, 객체 스토리지, CI/CD, 모니터링만으로도 충분할 수 있다. 중요한 것은 현재 규모에 맞되, 장애와 확장 경로를 막지 않는 설계다.
왕은 세 번째 여행을 마친 기사에게 말했다.
“서버는 코드가 사는 집이고, 인프라는 그 집이 서 있는 땅이다. 땅이 흔들리면 아무리 좋은 코드도 오래 서 있을 수 없다. 파일이 어디에 쌓이는지, 요청이 어디서 기다리는지, 네트워크 문이 어디로 열려 있는지 아는 개발자가 운영에서도 강하다.”
신입 기사는 이제 배운 단어를 외우는 데서 멈추지 않았다. 요청 하나가 DNS를 지나 로드밸런서와 프록시를 거쳐 서버에 도착하고, Thread가 I/O를 기다리며, 파일시스템과 DB와 캐시를 지나 다시 손님에게 돌아가는 전체 길을 떠올릴 수 있게 되었다.