sy/dev
Concept
27 min read

백엔드 엔지니어를 위한 지식 지도 — 무엇부터 공부해야 하나

백엔드 지식을 네트워크, HTTP, OS와 프로세스, 데이터베이스, 워커와 큐, 분산 시스템이라는 여섯 영역으로 나누고 무엇부터 공부하면 좋은지 정리한다.

백엔드를 공부하다 보면 늘 비슷한 느낌을 받는다.

배울 건 너무 많고, 각각은 서로 떨어져 있는 것처럼 보인다. HTTP도 알아야 하고, 데이터베이스도 알아야 하고, Docker도 알아야 하고, 큐도 알아야 하고, 네트워크도 알아야 한다. 여기에 분산 시스템, 캐시, 트랜잭션, 락, 타임아웃, 재시도, 모니터링까지 붙으면 어디서부터 손대야 할지 애매해진다.

그래서 많은 사람이 프레임워크부터 익힌다.

Django, FastAPI, Spring, NestJS 같은 것들. 물론 프레임워크는 중요하다. 하지만 프레임워크는 백엔드의 표면에 가깝다. 실제 운영 환경에서 문제가 생기면 프레임워크 문법보다 더 아래에 있는 지식이 필요해진다.

예를 들면 이런 질문들이다.

  • 왜 요청은 성공했는데 클라이언트는 타임아웃을 받았을까?
  • 왜 같은 API를 재시도했더니 데이터가 두 번 저장됐을까?
  • 왜 DB CPU는 낮은데 쿼리는 계속 밀릴까?
  • 왜 컨테이너가 아무 로그도 남기지 않고 죽었을까?
  • 왜 내 노트북에서는 붙는데 배포 서버에서는 안 붙을까?
  • 왜 큐를 붙였는데 오히려 장애 원인을 더 알기 어려워졌을까?

이 질문들에 답하려면 백엔드를 하나의 덩어리로 보면 안 된다. 몇 개의 층으로 나눠서 봐야 한다.

나는 백엔드 지식을 크게 여섯 영역으로 나눠 보는 편이 좋다고 생각한다.

  1. 네트워크
  2. HTTP와 API
  3. OS와 프로세스
  4. 데이터베이스
  5. 워커와 큐
  6. 분산 시스템

각 영역은 따로 배울 수 있지만, 실무에서는 서로 얽혀서 나타난다.

시리즈 전체 지도

이번 글은 시리즈의 첫 편이다. 전체 시리즈는 다음 흐름으로 갈 예정이다.

편주제핵심 질문
1편백엔드 지식 지도백엔드 지식은 왜 여섯 영역으로 나뉘는가
2편데이터베이스동시에 두 요청이 같은 데이터를 바꾸면 무슨 일이 생기나
3편분산 시스템응답 없음에서는 왜 아무것도 추론할 수 없나
4편워커와 큐이 잡이 두 번 실행되면 어떻게 되는가
5편HTTP와 API어떤 요청은 다시 보내도 되고, 어떤 요청은 다시 보내면 안 되는가
6편OS와 프로세스왜 앱 로그에는 아무것도 없는데 프로세스는 죽었나
7편네트워크“연결이 안 된다”를 어떻게 여러 실패로 나눌 것인가
8편장애를 읽는 법로그, 메트릭, 트레이스, 상태코드를 어떻게 연결해 볼 것인가

첫 편에서는 각 영역이 왜 필요한지와 어디부터 공부하면 좋은지를 정리한다.

1. 네트워크: 패킷이 가는 길

네트워크 영역의 목표는 단순하다.

“연결이 안 된다”를 여러 종류의 실패로 나눌 수 있어야 한다.

초보자에게 네트워크 장애는 대체로 하나로 보인다.

“안 붙어요.”

하지만 실제로는 전혀 다른 실패들이 같은 말로 묶인다.

  • DNS가 이름을 못 풀 수도 있다.
  • TCP 연결이 거절될 수도 있다.
  • 방화벽이 조용히 패킷을 버릴 수도 있다.
  • TLS 인증서 검증에서 막힐 수도 있다.
  • 프록시가 upstream을 잘못 보고 있을 수도 있다.
  • 컨테이너 안의 라우팅 테이블이 호스트와 다를 수도 있다.

이걸 구분하지 못하면 앱 로그만 보게 된다. 그런데 네트워크 층의 실패는 앱 로그에 남지 않는 경우가 많다.

예를 들어 ECONNREFUSED와 timeout은 완전히 다른 문제다.

ECONNREFUSED는 보통 목적지까지는 갔는데, 그 포트에서 아무도 듣고 있지 않다는 뜻이다. 반면 timeout은 중간 어딘가에서 응답이 오지 않았다는 뜻이다. 방화벽이 DROP하고 있을 수도 있고, 라우팅이 잘못됐을 수도 있다.

둘 다 “연결 실패”지만 고칠 사람과 봐야 할 지점은 다르다.

네트워크를 공부한다는 것은 모든 RFC를 외우는 일이 아니다. 실무적으로는 다음 순서를 나눠 볼 수 있으면 된다.

DNS → TCP → TLS → HTTP

이 중 어디까지 성공했고, 어디서 실패했는지 가르는 것. 그게 네트워크 진단의 출발점이다.

2. HTTP와 API: 서비스 사이의 계약

HTTP는 단순한 전송 포맷이 아니다. 서비스와 서비스 사이의 계약이다.

특히 중요한 것은 메서드와 상태코드의 의미다.

GET은 안전하다. PUT과 DELETE는 멱등이다. POST는 기본적으로 멱등이 아니다. 이 차이는 “재시도해도 되는가”를 결정한다.

예를 들어 클라이언트가 결제 요청을 POST로 보냈다. 서버는 결제를 처리했고 DB에도 저장했다. 그런데 응답이 돌아오는 길에 네트워크가 끊겼다.

클라이언트 입장에서는 응답을 못 받았다. 그렇다고 요청이 처리되지 않았다고 말할 수 있을까?

말할 수 없다.

요청은 이미 처리됐고, 응답만 유실됐을 수 있다. 이 상태에서 같은 POST를 다시 보내면 결제가 두 번 일어날 수 있다.

그래서 POST 재시도에는 멱등키가 필요하다.

응답 없음 ≠ 처리 안 됨

이 한 줄을 이해하면 HTTP를 보는 관점이 바뀐다.

상태코드도 마찬가지다. 500, 502, 503, 504는 모두 서버 쪽 문제처럼 보이지만 실제 화자는 다를 수 있다.

  • 500: 앱 코드가 터졌을 가능성이 높다.
  • 502: 프록시가 upstream에 못 붙었거나 응답을 제대로 못 받았다.
  • 503: 앱 또는 프록시가 지금 받을 수 없다고 말한다.
  • 504: 프록시가 upstream 응답을 제시간에 못 받았다.

특히 504는 조심해야 한다.

504는 “서버가 처리하지 않았다”가 아니다. “프록시가 정해진 시간 안에 응답을 못 받았다”이다. 서버는 여전히 처리 중일 수 있다. 그래서 POST 요청을 504 이후 바로 재시도하면 중복 처리가 생길 수 있다.

HTTP를 제대로 안다는 것은 상태코드를 외우는 것이 아니다. 요청이 실패했을 때 “다시 보내도 되는지” 판단할 수 있는 것이다.

3. OS와 프로세스: 코드가 실제로 도는 자리

백엔드 코드는 결국 프로세스 안에서 돈다.

프레임워크가 아무리 추상화해도, 운영 중에는 OS 수준의 일이 계속 벌어진다.

  • 프로세스가 뜬다.
  • 스레드가 생긴다.
  • 소켓이 열린다.
  • 파일 디스크립터가 쌓인다.
  • 시그널을 받는다.
  • 메모리 제한을 넘으면 죽는다.
  • CPU quota를 다 쓰면 throttling된다.

이 영역을 모르면 이런 장애를 해석하기 어렵다.

“앱 로그에는 아무것도 없는데 컨테이너가 재시작됐어요.”

이런 경우 가장 먼저 의심할 것 중 하나는 OOM이다. 컨테이너가 메모리 한도를 넘으면 커널이 프로세스를 죽인다. 앱 입장에서는 자기가 죽는다는 사실을 기록할 기회가 없다. 그래서 앱 로그에는 아무것도 안 남는다.

또 다른 예는 SIGTERM이다.

docker stop은 보통 프로세스에 SIGTERM을 보내고, 일정 시간 기다린 뒤 그래도 안 죽으면 SIGKILL을 보낸다. SIGTERM은 “정리하고 나가라”는 신호다. SIGKILL은 “지금 즉시 죽어라”다.

SIGKILL을 받으면 애플리케이션은 정리 코드를 실행할 수 없다. 진행 중이던 요청, 잡, 트랜잭션을 안전하게 마무리하려면 SIGTERM을 받아서 graceful shutdown을 해야 한다.

그런데 컨테이너에서 PID 1 문제가 끼면 SIGTERM이 제대로 전달되지 않는 경우가 있다. 셸 스크립트가 PID 1이 되어 자식 프로세스에 신호를 넘기지 않거나, 애플리케이션이 PID 1 특성을 모르고 기본 핸들러에 기대는 식이다.

이런 문제는 프레임워크 문서만 봐서는 잘 보이지 않는다.

백엔드 엔지니어에게 OS 지식이 필요한 이유는 여기에 있다. 코드는 추상화 위에서 쓰지만, 장애는 추상화 아래에서 올라온다.

4. 데이터베이스: 상태가 사는 곳

백엔드 지식 중 실무 효과가 가장 큰 영역을 하나 고르라면 나는 데이터베이스를 고르겠다.

이유는 간단하다.

상태가 거기에 살기 때문이다.

대부분의 백엔드 장애는 결국 상태와 관련된다.

  • 동시에 두 요청이 같은 행을 바꾼다.
  • 트랜잭션이 오래 열려 있다.
  • 락이 풀리지 않는다.
  • 마이그레이션이 운영 쿼리를 막는다.
  • 인덱스를 추가했는데 오히려 장애가 난다.
  • 외부 API 호출을 트랜잭션 안에서 기다린다.

데이터베이스를 공부할 때 가장 중요한 질문은 이것이다.

동시에 두 사람이 같은 데이터를 만지면 어떻게 되는가?

이 질문에 답하려면 트랜잭션, 격리 수준, MVCC, 행 잠금, 교착, 인덱스, 실행 계획을 알아야 한다.

예를 들어 PostgreSQL에서 외래키가 있는 자식 테이블에 INSERT를 하면 부모 행에 FOR KEY SHARE 잠금이 잡힌다. 부모 키가 사라지면 안 되기 때문이다.

즉, INSERT도 잠금을 잡는다.

이 사실을 모르면 “우리는 읽기와 INSERT만 하는데 왜 막히지?” 같은 장애를 이해하기 어렵다.

또 하나 자주 나오는 문제는 트랜잭션 안에서 외부 API를 호출하는 것이다.

트랜잭션 시작
→ DB row 잠금
→ 외부 API 호출
→ 응답 대기
→ DB 업데이트
→ 커밋

겉으로는 자연스러워 보인다. 하지만 외부 API가 느려지면 그 시간 동안 DB 잠금과 커넥션을 계속 잡고 있게 된다.

그 결과 같은 데이터를 만지는 다른 요청이 밀리고, 커넥션 풀이 고갈되고, 장애 원인은 외부 API인데 증상은 DB 장애처럼 보인다.

그래서 트랜잭션 경계 설계가 중요하다.

DB는 단순 저장소가 아니다. 동시성 제어 장치다.

5. 워커와 큐: 요청 밖에서 일하기

큐를 쓰는 이유는 보통 셋이다.

  1. 사용자를 기다리게 하지 않기 위해
  2. 순간 부하를 흡수하기 위해
  3. 실패한 일을 다시 시도하기 위해

하지만 큐를 붙이면 문제가 사라지는 게 아니다. 문제의 위치가 바뀐다.

큐를 쓰는 순간 가장 먼저 해야 할 질문은 이것이다.

이 잡이 두 번 실행되면 무슨 일이 일어나는가?

많은 큐 시스템은 기본적으로 at-least-once에 가깝다. 즉, 최소 한 번은 실행되지만 중복 실행될 수 있다.

네트워크 위에서 “정확히 한 번”은 생각보다 어렵다. 작업은 성공했는데 ack가 유실될 수 있고, 워커는 죽었는데 브로커는 그 사실을 늦게 알 수 있다.

그래서 실무에서는 보통 이렇게 간다.

at-least-once delivery
+ idempotent consumer
= effectively-once

즉, 중복 전달은 허용하되 소비자를 멱등하게 만든다.

방법은 여러 가지다.

  • 멱등키 테이블을 둔다.
  • 자연키에 ON CONFLICT를 건다.
  • 처리 완료 기록을 같은 트랜잭션에 저장한다.
  • 버전 비교로 조건부 업데이트를 한다.

큐에서 또 중요한 것은 공정성이다.

긴 잡과 짧은 잡을 같은 큐에 넣으면 긴 잡이 워커 슬롯을 다 차지하는 순간 짧은 잡은 뒤에서 계속 기다린다. 처리 시간 평균만 보면 멀쩡해 보일 수 있지만, 사용자 입장에서는 짧은 요청도 몇십 분씩 밀린다.

이게 head-of-line blocking이다.

해법은 큐를 나누고 전용 워커를 두거나, 긴 잡을 체크포인트가 있는 작은 잡으로 쪼개는 것이다.

큐는 비동기 처리를 가능하게 하지만, 동시에 실패를 더 복잡하게 만든다. 그래서 큐를 잘 쓴다는 것은 라이브러리를 붙이는 것이 아니라 실패 경로를 설계하는 것이다.

6. 분산 시스템: 부분 실패를 다루는 사고방식

분산 시스템은 특정 기술 하나가 아니다. 사고방식에 가깝다.

여러 프로세스, 여러 서버, 여러 네트워크 호출이 얽히는 순간 가장 중요한 전제가 생긴다.

응답이 없다는 사실만으로는 아무것도 추론할 수 없다.

상대가 요청을 못 받았을 수도 있다. 받았지만 처리 중일 수도 있다. 처리는 끝났지만 응답이 유실됐을 수도 있다. 응답은 왔지만 중간 프록시가 끊었을 수도 있다.

그래서 분산 시스템에서는 타임아웃, 재시도, 멱등성을 따로 보면 안 된다. 항상 같이 봐야 한다.

타임아웃을 짧게 잡으면 실패를 빨리 감지할 수 있다. 하지만 너무 짧으면 실제로는 처리 가능한 요청을 실패로 오판한다.

재시도를 켜면 일시적 실패를 복구할 수 있다. 하지만 이미 느려진 시스템에 재시도를 추가하면 부하가 더 늘어나 장애가 커질 수 있다.

멱등성이 없으면 재시도는 데이터 중복을 만든다.

이 셋은 삼각형이다.

timeout
retry
idempotency

하나를 바꾸면 나머지 둘의 전제가 바뀐다.

분산 시스템에서 또 중요한 것은 과부하 대응이다.

시스템이 감당할 수 없는 요청을 받았을 때, 모든 요청을 끝까지 붙잡고 있으면 전체가 느려진다. 큐가 길어지고, 타임아웃이 늘고, 재시도가 늘고, 다시 큐가 길어진다.

이런 상황에서는 일부 요청을 빨리 거절하는 것이 더 낫다. 이걸 load shedding이라고 한다.

또는 외부 서비스가 계속 실패할 때 아예 호출을 잠시 끊어 빠르게 실패시키는 circuit breaker를 둘 수 있다. 특정 기능의 커넥션 풀이나 워커 풀을 분리해서 한쪽 장애가 전체로 번지지 않게 하는 bulkhead도 같은 계열의 사고방식이다.

분산 시스템은 “절대 실패하지 않는 시스템”을 만드는 지식이 아니다. 실패가 일부에서 시작됐을 때 전체로 번지지 않게 하는 지식이다.

그럼 어디부터 공부해야 할까

여섯 영역을 반드시 순서대로 공부할 필요는 없다.

하지만 굳이 우선순위를 정한다면 나는 이렇게 추천한다.

데이터베이스
→ 분산 시스템
→ 워커와 큐
→ HTTP와 API
→ OS와 프로세스
→ 네트워크

이 순서가 좋은 이유는 실무 체감 때문이다.

데이터베이스는 거의 모든 백엔드 서비스의 중심이다. 트랜잭션, 락, 인덱스, 마이그레이션을 알면 바로 장애를 줄일 수 있다.

분산 시스템은 나머지 모든 영역에 걸친 사고방식이다. 타임아웃, 재시도, 멱등성, 과부하 대응은 HTTP, 큐, DB, 외부 API 어디에나 붙는다.

워커와 큐는 요청 밖의 일을 다루는 순간 필요해진다. 비동기 처리를 도입하면 실패와 중복, 지연, 회수 문제를 피할 수 없다.

HTTP와 API는 서비스 사이의 계약이다. 상태코드와 메서드 의미론을 알아야 안전한 재시도와 하위호환 API를 만들 수 있다.

OS와 프로세스는 코드가 실제로 도는 자리다. 컨테이너, 시그널, OOM, 파일 디스크립터, 이벤트 루프 문제를 이해하려면 필요하다.

네트워크는 빈도는 상대적으로 낮아 보일 수 있지만, 한 번 터지면 가장 진단하기 어렵다. 특히 컨테이너, 프록시, 방화벽, TLS가 얽히면 앱 코드만 봐서는 답이 안 나온다.

공부법: “아는 것 같은데 설명이 안 되는 것”을 찾기

이런 지식 지도는 처음부터 끝까지 통독하는 용도가 아니다.

가장 좋은 사용법은 항목을 훑으면서 스스로에게 묻는 것이다.

  • 이걸 남에게 설명할 수 있나?
  • 장애 상황에서 이 개념을 써먹어 본 적이 있나?
  • 비슷한 용어와 구분할 수 있나?
  • 로그나 지표에서 이 문제가 어떻게 보이는지 아나?

예를 들어 “멱등성”이라는 단어를 안다고 해서 충분한 것은 아니다.

다음 질문에 답할 수 있어야 한다.

POST 요청을 보냈고 504를 받았다. 재시도해도 되는가?

여기서 “504니까 서버 오류고 재시도하면 된다”고 답하면 위험하다. 504는 프록시가 upstream 응답을 제시간에 못 받았다는 뜻이다. 서버는 이미 처리했을 수 있다. POST라면 멱등키가 있어야 안전하다.

이렇게 질문으로 바꿔 보면 안다고 생각했던 개념이 실제로는 흐릿했다는 게 드러난다.

백엔드 공부는 키워드를 많이 아는 싸움이 아니다. 실패 상황에서 개념을 구분해 내는 싸움이다.

마무리

백엔드는 프레임워크만으로 이해되지 않는다.

프레임워크는 요청을 받고 응답을 만드는 데 도움을 준다. 하지만 운영 환경에서 실제 문제를 만드는 것은 그 아래와 바깥에 있는 것들이다.

패킷은 네트워크를 지나고, 요청은 HTTP 계약을 따르며, 코드는 프로세스 안에서 돌고, 상태는 데이터베이스에 저장된다. 긴 일은 큐로 넘어가고, 여러 서비스가 얽히는 순간 부분 실패와 재시도가 문제를 만든다.

그래서 백엔드 지식은 이렇게 봐야 한다.

네트워크: 패킷이 가는 길
HTTP와 API: 서비스 사이의 계약
OS와 프로세스: 코드가 도는 자리
데이터베이스: 상태가 사는 곳
워커와 큐: 요청 밖에서 일하기
분산 시스템: 부분 실패를 다루는 규칙

이 지도를 머릿속에 두면 새로운 기술을 배울 때도 위치가 잡힌다.

Redis를 배운다면 캐시이면서 큐일 수 있다. Kafka를 배운다면 큐라기보다 로그와 스트림에 가깝다. Kubernetes를 배운다면 OS, 네트워크, 프로세스, 배포 모델이 함께 나온다. gRPC를 배운다면 HTTP, 타임아웃, 데드라인 전파, 재시도가 같이 나온다.

중요한 건 모든 걸 한 번에 깊게 파는 것이 아니다. 지금 내가 모르는 줄이 어디인지 아는 것이다.

그 줄을 하나씩 지워 나가면 된다.

다음 편에서는 이 지도에서 가장 실무 효과가 큰 영역, 데이터베이스부터 볼 것이다.

주제는 이것이다.

동시에 두 요청이 같은 데이터를 바꾸면, 데이터베이스는 무엇을 보장하고 무엇을 보장하지 않는가.

Comments