백엔드 CS를 동화처럼 이해하기: 운영체제부터 Spring, 분산 시스템까지

존스노우·2026년 7월 7일

CS 동화시리즈

목록 보기
1/4

백엔드 CS를 동화처럼 이해하기

읽는 법: 비유(실제 CS 개념) 형식으로 붙였습니다.

예: 방 하나를 쓰는 사람(프로세스), 등기 우편 배(TCP), 책 뒤 찾아보기(인덱스)


프롤로그: 백엔드 왕국

옛날 옛날, 거대한 백엔드 왕국(서버 시스템) 이 있었다. 이 왕국은 성 안의 일꾼들, 바깥 강을 오가는 배들, 지하 금고, 마법탑, 시장, 그리고 여러 섬으로 이루어져 있었다. 왕국에서 훌륭한 개발 기사가 되려면, 모든 장소가 어떻게 움직이는지 알아야 했다.


1장. 운영체제 성

성 안에는 각자 독립된 방을 쓰는 주민들(프로세스)이 있었다. 이 주민들은 자기만의 방 구조(Code/Data/Heap/Stack)를 통째로 가지고 살아서 서로의 물건을 쉽게 건드릴 수 없었다. 반면 같은 방을 함께 쓰는 룸메이트들(스레드)은 거실과 냉장고(Code/Data/Heap)는 같이 쓰고, 개인 가방(Stack)만 따로 들고 다녔다. 그래서 룸메이트끼리 일 바꾸기(스레드 컨텍스트 스위칭)는 방을 통째로 옮기는 일(프로세스 컨텍스트 스위칭)보다 가벼웠다.

CPU 기사단(CPU)은 한 번에 한 일만 처리할 수 있었다. 그래서 다른 일을 맡을 때마다 지금 들고 있던 무기 상태(레지스터), 지도에서 현재 위치(프로그램 카운터), 가방 위치(스택 포인터)를 기록하고 다음 사람의 기록을 불러왔다. 이것을 작업 교대(컨텍스트 스위칭)라고 불렀다. 작업 교대가 너무 잦으면 창고 캐시(CPU 캐시)가 자주 비어 왕국 전체가 느려졌다.

어느 날 기사 네 명이 서로 열쇠를 쥔 채 꼼짝 못 했다. 한 방은 한 명만 들어갈 수 있었고(상호 배제), 기사는 이미 가진 열쇠를 놓지 않은 채 다른 열쇠를 기다렸고(점유 대기), 누구도 강제로 열쇠를 빼앗을 수 없었고(비선점), 서로가 서로를 둥글게 기다렸다(순환 대기). 이 네 조건이 동시에 모이면 교착 상태(데드락)가 되었다.

왕은 데드락을 막기 위해 네 가지 방법을 가르쳤다. 애초에 네 조건 중 하나를 못 생기게 하는 법(예방), 열쇠를 주기 전 안전한지 계산하는 법(회피, 은행원 알고리즘), 일이 터진 뒤 찾아내고 희생자를 정하는 법(탐지 후 복구), 그리고 실무 기사들이 자주 쓰는 규칙인 항상 같은 순서로 열쇠를 잡기(락 획득 순서 고정)와 오래 기다리면 포기하기(락 타임아웃)였다.

CPU 기사단장은 줄 세우기 규칙(스케줄링 알고리즘)도 정했다. 먼저 온 사람부터 처리하면 먼저 온 순서 처리(FCFS), 짧은 일부터 처리하면 짧은 작업 우선(SJF), 모두에게 시간을 조금씩 나눠주면 돌림 처리(Round Robin), 중요한 사람부터 처리하면 우선순위 스케줄링(Priority Scheduling)이었다.

성의 마법사는 실제 창고보다 큰 창고가 있는 것처럼 보이게 하는 환상 창고(가상 메모리)를 만들었다. 주민은 큰 주소 공간(논리 주소)을 쓰는 것처럼 느끼지만, 마법사는 필요한 물건만 실제 창고(물리 메모리)에 올렸다. 필요한 상자가 실제 창고에 없으면 창고 경보(페이지 폴트)가 울렸고, 운영체제 집사(OS)가 디스크 창고에서 상자를 가져와 페이지 테이블을 고쳤다.

성문에는 두 종류의 출입 도구가 있었다. 하나뿐인 화장실 열쇠(뮤텍스)는 한 번에 한 명만 들어가며, 잠근 사람이 직접 열어야 했다. 여러 장 있는 놀이기구 입장권(세마포어)은 정해진 수만큼 여러 명이 동시에 들어갈 수 있었고, 열쇠 주인이라는 개념은 없었다.


2장. 네트워크 강

왕국 밖에는 네트워크 강(네트워크)이 흘렀다. 강에는 등기 우편 배(TCP)와 일반 엽서 배(UDP)가 다녔다. 등기 우편 배(TCP)는 도착 확인, 순서 보장, 재전송을 해 주었고, 일반 엽서 배(UDP)는 빠르게 보내지만 도착과 순서를 보장하지 않았다.

등기 우편 배(TCP)는 출항 전 세 번 인사했다. 손님이 “내 시작 번호는 X야”(SYN)라고 말하면, 선장은 “확인했고, 내 시작 번호는 Y야”(SYN+ACK)라고 답했다. 손님이 다시 “확인했어”(ACK)라고 하면 연결(3-way handshake)이 열렸다. 헤어질 때는 양쪽이 각자 문을 닫아야 해서 FIN, ACK, FIN, ACK의 네 단계(4-way handshake)를 거쳤다.

왕국 사람들은 평문 길(HTTP)보다 갑옷 입은 길(HTTPS)을 선호했다. 갑옷 입은 길(HTTPS)은 기존 편지 전달 방식(HTTP)에 암호화 갑옷(TLS/SSL)을 입힌 것이다. 처음에는 신분증 확인(TLS Handshake, 서버 인증서 검증)을 하고, 이후에는 함께 만든 비밀 열쇠(대칭키)로 대화를 암호화했다.

API 우체국(REST API)은 물건 이름(URI)과 행동(HTTP 메서드)을 분리해서 일했다. 물건을 조회하면 GET, 새로 만들면 POST, 통째로 바꾸면 PUT, 지우면 DELETE였다. 우체국은 손님의 이전 사정을 기억하지 않는 규칙(Stateless)을 지켰고, 누구나 같은 방식으로 이해하는 창구(Uniform Interface)를 중요하게 여겼다.

손님은 작은 표식(쿠키)을 자기 주머니(브라우저)에 넣고 다녔다. 반면 자세한 방문 기록(세션 데이터)은 성 안 창고(서버)에 있었고, 손님은 그 창고 칸 번호(세션 ID)만 들고 다녔다. 성이 여러 채가 되자, 어느 성에서도 같은 기록을 보려면 공동 창고(Redis 같은 외부 세션 저장소)나 성끼리 동기화(세션 클러스터링)가 필요했다.

주소 안내원(DNS)은 이름표 도메인(example.com)을 실제 집 주소(IP)로 바꿔주었다. 먼저 손님 수첩(브라우저 캐시)을 보고, 없으면 마을 수첩(OS 캐시), 동네 안내소(로컬 리졸버), 세계 지도관(Root DNS), 지역 지도관(TLD DNS), 최종 집주인(Authoritative DNS)에게 차례로 물었다.

강가의 교통 지휘관(로드밸런서)은 손님을 여러 성(서버)으로 나눠 보냈다. 순서대로 보내면 Round Robin, 가장 한가한 성으로 보내면 Least Connection, 손님 주소를 기준으로 보내면 IP Hash였다. 항구 번호까지 보고 나누는 지휘관(L4 로드밸런서)도 있었고, 편지 내용과 URL 길까지 보고 나누는 지휘관(L7 로드밸런서)도 있었다.

다른 왕국에서 온 편지(다른 Origin 요청)는 기본적으로 성문 경비(Same-Origin Policy)가 막았다. 하지만 성주가 “이 왕국은 허용한다”(Access-Control-Allow-Origin 같은 CORS 헤더)고 써 주면, 예외적으로 통과할 수 있었다. 이것이 다른 출처 허용 규칙(CORS)이었다.


3장. 데이터베이스 금고

지하 금고(DB)에는 네 가지 법(ACID)이 있었다. 모든 거래는 전부 성공하거나 전부 실패해야 했고(Atomicity), 거래 전후 금고 규칙이 깨지면 안 됐고(Consistency), 동시에 온 손님들이 서로 간섭하면 안 됐고(Isolation), 한번 확정된 기록은 남아야 했다(Durability).

금고에는 소란 방지 등급(트랜잭션 격리 수준)이 있었다. 남이 아직 확정하지 않은 기록도 보는 가장 낮은 등급(Read Uncommitted), 확정된 기록만 보는 등급(Read Committed), 같은 거래 안에서는 같은 값을 계속 보는 등급(Repeatable Read), 가장 엄격하게 줄 세우는 등급(Serializable)이 있었다. 등급이 높을수록 안전하지만 손님 처리 속도(동시성)는 낮아질 수 있었다.

금고지기는 두꺼운 장부를 빨리 찾기 위해 책 뒤 찾아보기(인덱스)를 만들었다. 찾아보기(인덱스)가 있으면 장부 첫 장부터 넘기는 일(Full Scan)을 줄일 수 있지만, 새 기록을 넣거나 고칠 때 찾아보기도 함께 고쳐야 해서 쓰기 비용이 늘었다.

찾아보기는 대개 잎사귀 나무(B+Tree)로 만들어졌다. 일반 나무(B-Tree)는 중간 가지에도 실제 물건을 둘 수 있지만, 잎사귀 나무(B+Tree)는 실제 데이터가 주로 맨 아래 잎사귀(리프 노드)에 모이고 잎사귀끼리 연결된다. 그래서 “이 값부터 저 값까지”(범위 검색)를 찾기 좋았다.

금고 설계자들은 같은 정보를 여러 곳에 적지 않기 위해 장부를 잘게 나누었다(정규화). 중복이 많으면 한 장부만 고치고 다른 장부를 안 고쳐 기록 이상(이상현상)이 생기기 때문이다. 반대로 조회 속도를 위해 일부러 중복을 허용하는 경우(반정규화)도 있었다.

InnoDB 경비병(MySQL InnoDB)은 세 종류의 자물쇠를 썼다. 특정 줄 하나를 잠그는 자물쇠(Record Lock), 줄과 줄 사이 빈 공간을 잠그는 자물쇠(Gap Lock), 둘을 합친 자물쇠(Next-Key Lock)가 있었다. 특히 Repeatable Read에서는 새 줄이 끼어드는 현상(팬텀 리드)을 막기 위해 이런 잠금이 중요했다.

금고 책사는 실행 순서표(Execution Plan)를 만들었다. 어떤 장부를 먼저 보고, 어떤 찾아보기(인덱스)를 쓰고, 어디서 전체 장부 스캔(Full Scan)이 나는지 알려주는 표였다. 개발 기사는 EXPLAIN이라는 거울로 이 표를 확인했다.

JPA 마을에서는 한 번 목록을 가져온 뒤(1번 쿼리), 목록의 각 항목마다 다시 세부 정보를 가져오는 일(N번 쿼리)이 자주 생겼다. 이것을 N+1 문제라고 불렀다. 기사들은 한 번에 같이 데려오는 마차(Fetch Join), 묶음 호출(@BatchSize/default_batch_fetch_size), 미리 정한 경로(EntityGraph)로 이를 줄였다.


4장. 자료구조 숲과 알고리즘 길

숲에는 긴 선반(배열)과 사슬 목걸이(연결 리스트)가 있었다. 긴 선반(배열)은 번호만 알면 바로 찾을 수 있었지만(O(1)), 중간에 새 물건을 넣으면 뒤 물건을 밀어야 했다(O(n)). 사슬 목걸이(연결 리스트)는 원하는 위치까지 따라가야 했지만(O(n)), 위치만 알면 고리만 바꿔 끼워 넣고 뺄 수 있었다(O(1)).

마법 사전(해시 테이블)은 물건 이름(key)을 마법 숫자(hash)로 바꿔 선반 위치(버킷)를 찾았다. 보통은 빠르게 찾을 수 있었지만(평균 O(1)), 서로 다른 이름이 같은 선반으로 가면 충돌(해시 충돌)이 생겼다. 충돌은 같은 선반에 줄줄이 매다는 방식(체이닝)이나 다른 빈 선반을 찾아가는 방식(오픈 어드레싱)으로 해결했다.

탑에는 접시 더미(스택)와 줄 서는 대기열(큐)이 있었다. 접시 더미(스택)는 마지막에 올린 접시가 먼저 나갔고(LIFO), 대기열(큐)은 먼저 선 사람이 먼저 나갔다(FIFO).

알고리즘 길의 표지판(Big-O)은 손님 수가 늘 때 일이 얼마나 빨리 늘어나는지 알려주었다. 빠른 정렬 장인 퀵정렬(Quick Sort)은 평균 O(n log n)이지만 운이 나쁘면 O(n²)이 될 수 있었고, 병합정렬(Merge Sort)은 항상 O(n log n)을 보장했다. 느린 장인 버블정렬과 삽입정렬은 보통 O(n²)였다.

정렬된 서가(정렬된 데이터)에서는 가운데 책부터 확인하는 반씩 줄이기 탐색(이진 탐색)을 쓸 수 있었다. 매번 절반을 버리니 시간은 O(log n)이었다.

탐험가 둘도 있었다. 가까운 마을부터 넓게 방문하는 탐험가(BFS)는 줄 서는 대기열(큐)을 썼고, 최단 경로에 유리했다. 한 길을 끝까지 파고드는 탐험가(DFS)는 접시 더미나 재귀(스택/재귀)를 썼고, 모든 경로 탐색이나 사이클 탐지에 유리했다.

마법 주문을 자기 자신에게 다시 거는 기술(재귀)은 분할정복 문제를 예쁘게 표현했지만, 주문이 너무 깊어지면 호출 기록장(Stack)이 터졌다(StackOverflow). Java 마법탑은 꼬리 재귀 최적화(컴파일러가 꼬리 재귀를 반복문처럼 바꾸는 최적화)를 보장하지 않았다.

성의 작은 기억 창고(캐시)는 자리가 부족하면 가장 오래 안 쓴 물건을 버렸다. 이것을 오래 안 쓴 것 내보내기 정책(LRU 캐시 교체 정책)이라고 했다. Java 시장에서는 접근 순서를 기억하는 장부(LinkedHashMap accessOrder)로 간단한 LRU를 만들 수 있었다.


5장. Java와 JVM 마법탑

마법탑(JVM)에는 여러 방이 있었다. 클래스 도서관(Method Area)에는 클래스 정보와 static 정보가 있었고, 물건 창고(Heap)에는 객체 인스턴스가 있었다. 각 일꾼의 호출 기록장(Stack)에는 지역변수와 메서드 호출 정보가 쌓였고, 현재 위치 표시기(PC Register)와 원주민 주문방(Native Method Stack)도 있었다.

청소부(GC)는 대부분 물건이 금방 버려진다는 믿음(Weak Generational Hypothesis)을 가지고 일했다. 새 물건 구역(Young Generation, Eden+Survivor)은 자주 청소했고(Minor GC), 오래 살아남은 물건 구역(Old Generation)은 덜 자주 청소했다(Major/Full GC).

G1 청소부(G1GC)는 큰 창고(Heap)를 똑같은 구역들(Region)로 나누고, 쓰레기가 많은 구역부터 먼저 치웠다(Garbage First). 목표는 모두를 오래 멈추게 하는 시간(Stop-the-World pause)을 예측 가능한 범위로 줄이는 것이었다.

Java 주민들은 같은지 판단하는 약속(equals/hashCode 계약)을 중요하게 여겼다. HashMap 상점은 먼저 주민의 주소 번호(hashCode)로 선반을 찾고, 같은 선반 안에서 진짜 같은지(equals) 확인했다. 그래서 equals()를 바꾸면 hashCode()도 함께 바꿔야 했다.

문자 두루마리(String)는 불변이라 고치면 새 두루마리가 생겼다. 빠르게 고치는 작업대(StringBuilder)는 혼자 일할 때 빠르고, 잠금 달린 작업대(StringBuffer)는 synchronized가 붙어 여러 사람이 써도 안전하지만 상대적으로 느렸다.

예외 전령에는 두 종류가 있었다. 반드시 처리하거나 밖으로 넘겨야 하는 전령(체크 예외, checked exception)과, 컴파일러가 강제하지 않는 전령(언체크 예외, RuntimeException 계열)이 있었다. 실무에서는 복구하기 어려운 오류를 언체크 예외로 표현하는 경우가 많았다.

가문의 부모(추상 클래스)는 공통 필드, 생성자, 구현 메서드를 가질 수 있지만 한 부모만 상속할 수 있었다. 능력 계약서(인터페이스)는 여러 개 구현할 수 있고, “이 행동을 할 수 있어야 한다”는 약속을 표현했다. Java 8 이후에는 계약서에도 기본 구현(default method)과 static method를 둘 수 있었다.

쌍둥이 판별에서 == 표식은 기본 타입이면 값 자체를 비교하고, 객체면 같은 사람인지(같은 참조인지)를 보았다. equals() 표식은 기본 구현은 ==와 같지만, String처럼 논리적으로 같은 값인지 비교하도록 바꿔 쓰는 경우가 많았다.


6장. Spring 마을

Spring 마을에서는 주민이 자기 도구를 직접 만들지 않았다. 마을 관리소(IoC Container)가 객체를 만들고 관리했으며, 필요한 도구를 주민에게 넣어 주었다(Dependency Injection, DI). 이렇게 제어권이 개발자에서 컨테이너로 넘어간 것을 제어의 역전(IoC)이라고 불렀다.

마을 주민(Bean)에게는 거주 방식(Bean Scope)이 있었다. 마을 전체에 하나만 사는 주민(Singleton)이 기본이었고, 부를 때마다 새로 태어나는 주민(Prototype), 요청마다 사는 주민(Request), 로그인 세션마다 사는 주민(Session), 애플리케이션 전체에 사는 주민(Application)도 있었다. Singleton 주민에게 변하는 개인 상태(가변 필드)를 두면 여러 요청이 동시에 건드려 문제가 생길 수 있었다.

거래소 직원(@Transactional 프록시)은 손님이 정문 밖에서 들어올 때 메서드 호출을 가로챘다(Spring AOP 프록시). 직원은 거래를 열고, 원본 일을 실행한 뒤, 결과에 따라 커밋하거나 롤백했다. 기본적으로 큰 사고 전령(RuntimeException/unchecked exception)이 오면 자동 롤백하고, 예상 가능한 업무 전령(checked exception)은 자동 롤백하지 않았다. 체크 예외에도 롤백하려면 rollbackFor 규칙을 써야 했다.

하지만 같은 집 안에서 자기 메서드를 바로 부르는 일(self-invocation)은 정문 직원을 거치지 않았다. 그래서 같은 클래스 내부 호출은 프록시를 통과하지 않아 @Transactional이 적용되지 않을 수 있었다. 해결하려면 구조를 나누거나, 자기 자신의 프록시를 통해 호출해야 했다.

Spring MVC 관문에서는 중앙 접수원(DispatcherServlet)이 모든 요청을 받았다. 접수원은 담당 컨트롤러를 찾는 지도(HandlerMapping)를 보고, 실행 도구(HandlerAdapter)로 컨트롤러를 호출했다. REST 응답이면 번역가(HttpMessageConverter)가 객체를 JSON 같은 응답으로 바꿔 보냈다.

성문 앞 경비(Filter)는 서블릿 스펙 영역에서 DispatcherServlet 앞뒤를 지켰다. 반면 내부 안내원(Interceptor)은 Spring MVC 안쪽에서 컨트롤러 전후를 보며, 컨트롤러 정보와 Spring Bean에 접근하기 쉬웠다.

Spring Security 왕국에는 긴 보안 검문 줄(FilterChainProxy와 Security Filters)이 있었다. 요청은 여러 보안 필터를 순서대로 지나갔고, 인증된 사람의 신분증(Authentication)은 현재 일꾼 전용 주머니(SecurityContextHolder, 보통 ThreadLocal)에 담겼다. 이후 문마다 권한 검사(인가)를 했다.


7장. 동시성 섬과 분산 왕국

왕국이 여러 섬(여러 서버 인스턴스)으로 늘어나자, 한 섬 안에서만 통하는 자물쇠(synchronized)는 부족했다. 여러 섬이 함께 보는 중앙 자물쇠(분산 락)가 필요했다. 이 중앙 자물쇠는 Redis의 SETNX/Redisson, ZooKeeper, 또는 DB 자물쇠(SELECT ... FOR UPDATE)로 만들 수 있었다.

충돌을 대하는 태도도 달랐다. 걱정 많은 경비병(비관적 락)은 누가 건드릴까 봐 미리 잠갔다. 낙천적인 기록관(낙관적 락)은 일단 잠그지 않고 진행하다가, 저장할 때 버전 도장(version 컬럼)을 확인해 누가 먼저 바꿨는지 판단했다. 충돌이 드물면 낙관적 락이 유리했다.

전령 시스템도 두 종류가 유명했다. 긴 사건 장부(Kafka)는 파티션 안에서 순서를 보장하고 로그를 일정 기간 보관해 재처리할 수 있어 대용량 이벤트 스트리밍에 잘 맞았다. 전통 우체국(RabbitMQ)은 Exchange와 Routing Key로 유연하게 라우팅하고, 처리된 메시지는 보통 큐에서 사라지는 방식에 가깝다.

외부 왕국이 아플 때 계속 편지를 보내면 우리 왕국까지 병들 수 있었다. 그래서 차단기(서킷 브레이커)가 생겼다. 정상 상태(CLOSED)에서는 요청을 보내고, 실패가 많아지면 열린 상태(OPEN)로 바뀌어 요청을 즉시 막았다. 시간이 지나면 반쯤 열린 상태(HALF-OPEN)로 일부 요청만 보내 회복됐는지 확인했다.


8장. 컬렉션 시장

컬렉션 시장에는 세 큰 상점이 있었다. 줄 서는 상점(List)은 순서가 있고 중복을 허용했다. 출석부 상점(Set)은 중복을 허용하지 않았고, 순서는 구현에 따라 달랐다. 사전 상점(Map)은 단어와 뜻(Key-Value)을 묶어 보관하며, 단어(Key)는 중복될 수 없었다.

배열 가게(ArrayList)는 내부에 긴 배열을 두어 번호표(index)로 바로 찾기 좋았다(O(1)). 하지만 중간에 물건을 넣거나 빼면 뒤 물건을 밀어야 했다(O(n)). 사슬 가게(LinkedList)는 이중 연결 리스트라 위치를 이미 알면 고리만 바꿔 삽입/삭제할 수 있지만(O(1)), 그 위치까지 찾아가는 데 O(n)이 걸렸다.

마법 사전 가게(HashMap)는 손님의 key에서 hashCode를 얻어 선반(bucket)을 정했다. 같은 선반에 여러 key가 몰리면 처음에는 줄(연결 리스트)로 이어 붙였다. Java 8 이후에는 한 선반의 줄이 너무 길고 조건이 맞으면, 그 선반을 균형 잡힌 나무(레드-블랙 트리)로 바꿔 최악 조회 성능을 줄였다. 대표 기준은 treeify threshold 8, untreeify threshold 6, 최소 테이블 크기 64로 알려져 있다.

오래된 금고(Hashtable)는 메서드마다 synchronized가 걸려 있어 스레드 안전하지만 느리고 null key/value를 허용하지 않았다. 일반 사전(HashMap)은 동기화되지 않지만 빠르고 null을 허용했다. 여러 스레드가 함께 쓰는 실무 상황에서는 보통 ConcurrentHashMap을 쓴다.

동시 사전(ConcurrentHashMap)은 Java 8 기준으로 전체 창고를 통째로 잠그지 않고, 버킷 단위 CAS와 synchronized를 조합해 락 범위를 줄인다. Java 7까지의 세그먼트 락 구조와 Java 8 이후 구조는 다르므로, 면접에서는 Java 8 기준이라고 말하는 것이 안전하다.

중복 없는 출석부(HashSet)는 내부적으로 HashMap을 사용했다. 원소는 HashMap의 key로 들어가고, value 자리에는 의미 없는 고정 더미 객체가 들어갔다.

정렬 사전(TreeMap)과 정렬 출석부(TreeSet)는 레드-블랙 트리 기반이라 항상 정렬 상태를 유지했다. 삽입, 삭제, 조회는 O(log n)이었고, 정렬 기준은 Comparable 또는 Comparator로 정했다.

순서 기억 사전(LinkedHashMap)은 HashMap에 이중 연결 리스트를 붙여 삽입 순서를 기억했다. accessOrder=true를 켜면 접근 순서를 기억해, 가장 오래 안 쓴 항목을 찾는 LRU 캐시를 만들기 쉬웠다.

HashMap 창고는 기본 방 크기(초기 용량)가 16이고, 75%쯤 차면(로드 팩터 0.75) 방을 두 배로 넓히며 물건을 다시 배치했다(rehashing). 대량 저장이 예상되면 처음부터 큰 방을 잡아 리사이징 비용을 줄일 수 있었다.

순회자(Iterator)도 성격이 있었다. 예민한 순회자(Fail-Fast Iterator)는 순회 중 구조가 바뀌면 ConcurrentModificationException을 던졌다. 스냅샷 순회자(Fail-Safe 성격의 Iterator)는 순회 시점의 복사본이나 약한 일관성으로 돌기 때문에 예외는 덜 나지만 최신 변경이 바로 보이지 않을 수 있었다. CopyOnWriteArrayList와 ConcurrentHashMap이 대표적으로 자주 언급된다.

정렬 기준 계약도 두 가지였다. 자기소개 정렬(Comparable)은 클래스 안에 compareTo()를 넣어 기본 정렬 하나를 정한다. 외부 심판 정렬(Comparator)은 클래스 밖에서 여러 기준을 만들어 주입할 수 있다.


9장. 추가 CS 시험장

왕은 마지막 시험으로 통신 지도(OSI 7계층)를 펼쳤다. 아래부터 전기 신호 길(물리 계층), MAC과 프레임 길(데이터링크 계층), IP와 라우팅 길(네트워크 계층), TCP/UDP와 포트 길(전송 계층), 대화 관리 길(세션 계층), 표현 변환 길(표현 계층), HTTP 같은 서비스 길(응용 계층)이 있었다. 실무에서는 네트워크 인터페이스, 인터넷, 전송, 응용으로 보는 TCP/IP 4계층도 자주 썼다.

HTTP 우체국에서 조회 편지(GET)는 데이터를 URL 쿼리에 담고, 캐시나 북마크가 쉬우며, 안전하고 멱등인 메서드로 다뤄진다. 생성/처리 편지(POST)는 보통 body에 데이터를 담고, 같은 요청을 여러 번 보내면 결과가 달라질 수 있어 안전하지도 멱등하지도 않은 메서드로 다뤄진다.

기억 창고 관리법(Cache Invalidation)도 시험에 나왔다. 시간이 지나면 자동으로 버리는 모래시계(TTL), DB에 쓸 때 캐시도 같이 쓰는 방식(Write-Through), 캐시에 먼저 쓰고 나중에 DB에 반영하는 방식(Write-Behind), 캐시에 없으면 DB에서 가져와 채우는 방식(Cache-Aside)이 있었다.

교통 지휘관은 같은 손님을 항상 같은 성으로 보내는 끈끈한 배정(Sticky Session)도 할 수 있었다. 서버 로컬 메모리에 세션이 있을 때 편하지만, 특정 서버에 손님이 몰리거나 그 서버가 죽으면 세션이 사라질 수 있어 공동 세션 창고(Redis)가 더 견고한 대안이 되곤 했다.

전령 보장 방식도 세 가지였다. 최대 한 번 배달(At-most-once)은 유실될 수 있지만 중복은 적다. 최소 한 번 배달(At-least-once)은 유실을 줄이지만 중복될 수 있다. 정확히 한 번 배달(Exactly-once)은 이상적이지만 구현이 어렵다. 실무에서는 최소 한 번 배달(At-least-once)에 중복되어도 결과가 같게 만드는 멱등 처리(Idempotency)를 더해 사실상 정확히 한 번처럼 만드는 경우가 많다.

분산 왕국의 CAP 예언도 있었다. 여러 섬이 네트워크 단절(Partition)을 겪을 수밖에 없다면, 모든 손님에게 항상 응답하는 것(Availability)과 모두가 항상 같은 값을 보는 것(Consistency)을 동시에 완벽히 잡기는 어렵다. 그래서 현실의 분산 시스템은 단절 허용(Partition Tolerance)을 전제로 일관성과 가용성 사이에서 선택과 타협을 한다.

멱등성(Idempotency)은 같은 요청을 여러 번 보내도 한 번 보낸 것과 결과가 같은 성질이다. PUT과 DELETE는 일반적으로 멱등으로 설계되고, POST는 보통 멱등이 아니다. 그래서 결제 같은 중요한 API에는 중복 방지 표식(Idempotency Key)을 붙인다.

성문 문지기(Rate Limiter)는 요청 속도를 제한했다. 고정 시간마다 카운트를 세는 방식(Fixed Window), 움직이는 시간 구간으로 더 정밀하게 세는 방식(Sliding Window), 일정 속도로 토큰을 채우고 요청마다 토큰을 쓰는 방식(Token Bucket), 요청을 일정 속도로 흘려보내는 방식(Leaky Bucket)이 있었다.

삭제에도 두 가지가 있었다. 실제 줄을 장부에서 없애는 삭제(Hard Delete)와, deleted_at 같은 표시만 남기는 삭제(Soft Delete)였다. Soft Delete는 복구와 이력 관리에 좋지만, 조회마다 삭제 제외 조건이 필요하고 데이터가 계속 쌓일 수 있었다.

마지막으로 왕립 기록관(Event Sourcing)은 현재 상태만 적지 않았다. 대신 “무슨 일이 일어났는지” 사건 기록(Event Log)을 순서대로 저장했다. 현재 상태는 그 사건들을 처음부터 재생해 계산했다. 이 방식은 감사 로그와 이력 추적에 강하고, 조회 모델과 명령 모델을 나누는 방식(CQRS)과 자주 함께 쓰인다.


에필로그

왕은 신입 기사에게 말했다.

“빠른 백엔드는 자료구조와 인덱스를 알고, 안전한 백엔드는 트랜잭션과 락을 안다. 오래 버티는 백엔드는 네트워크, JVM, Spring, 분산 시스템이 한 흐름으로 이어진다는 걸 안다.”

신입 기사는 고개를 끄덕였다. 이제 그는 외울 단어를 보는 것이 아니라, 하나의 왕국이 움직이는 모습을 떠올릴 수 있게 되었다.

profile
어제의 나보다 한걸음 더

0개의 댓글