R+7

[TIL] 차근차근 CS (3) - Backend 역사와 발전 과정 본문

TIL

[TIL] 차근차근 CS (3) - Backend 역사와 발전 과정

prgmd 2026. 7. 10. 17:28

Today I Learned

동적 웹의 탄생: CGI와 서버 사이드 렌더링(SSR)

정적 웹 페이지 상으로는 모든 경우의 수에 따라 파일을 미리 만들어놓기란 불가능했으므로, 사용자 요청마다 그 자리에서 새로운 HTML을 만들어서 제공하는 SSR(서버 사이드 렌더링) 방식의 CGI(Common Gateway Interface) 방식이 등장했다. 웹 서버가 직접 처리할 수 없는 동적 기능은 외부 프로그램에 작업을 넘겨주고, 그 프로그램이 처리한 결과를 다시 웹 서버를 통해 사용자에게 전달하는 최초의 표준 인터페이스 규약인 셈.

 

SSR 방식은 이후로도 계속 이어졌는데, 1994년 PHP와 1999년 JSP가 대표적이다. CGI 방식은 요청이 올 때마다 새로운 프로세스를 띄워 서버 메모리를 잡아먹는 단점이 존재했고, 이를 해결하기 위해 웹 서버 내부 엔진에서 부드럽게 돌아가면서 HTML 코드 내 서버 로직을 직접 삽입하는 PHP와 JSP 같은 웹 전용 언어가 등장해 대세가 됐다.

백엔드 생태계: 주요 프로그래밍 언어

사실 모든 프로그래밍 언어는 5가지 본질(변수, 조건문(분기), 반복문, 함수, 데이터 타입)로 이뤄져 있고, 그 위에 클래스, 예외 처리, 모듈, 라이브러리 등이 쌓이는 구조다. 개발자 사이에서는 '언어 전이'라는 용어가 있는데, 이는 하나의 언어를 마스터하면 다른 언어를 배울 때 문법만 바뀔 뿐 문제를 해결하는 논리적 흐름과 제어 구조 자체는 거의 동일하기 때문에 금방 습득한다는 뜻이다.

백엔드 언어는 다음과 같다.

  • PHP: 웹이 동적으로 변하던 시절 HTML 내 서버 코드를 바로 끼워 넣는 방식으로 폭발적 인기를 끔 (한때 점유율 80%). Wordpress가 PHP 기반이이며, 프레임워크는 Laravel을 사용한다.
  • Python: 백엔드 언어는 아니지만 2005년 Django가, 2018년에는 FastAPI가 인기를 끌었다. FastAPI는 API에 최적화 되어 있고, 코드도 짧고, Swagger로 자동 명세화도 된다.
  • Ruby: 개발자가 즐거워야 한다는 철학이 강한 언어. Ruby on Rails는 혁신적이었다고. '설정보다 관례'를 중시하기에 세세한 환경 설정보다, 미리 정해둔 폴더 구조와 네이밍 규칙만 지키면 수많은 설정 코드를 생략하는 방식으로 개발 속도를 엄청 높였다.
  • Java / Spring: 우리나라 주력 프레임워크로, 2000년대 초반 대기업, 금융권, 공공기관이 사용했고 지금도 사용 중. 안정성이 높고 대규모 시스템 운영에 수십 년간 검증된 언어다. 특히 엄격한 정적 타입 언어로 대규모 트래픽에 강하고, 트랜잭션 같은 정밀 통제에 우수하다.
  • ASP.NET: Microsoft C# 기반. 윈도우 생태계에 최적화.
  • Go: 구글에서 만듦. 빠르고 가벼우며 수천개 요청 처리에 특화됨. 복잡한 문법 없이도 성능이 좋아 대규모 트래픽 서버 인프라에서 많이 사용한다. Goroutine의 성능이 말이 안 되는데, 메모리 몇 KB 만으로도 수만 개 초경량 스레드를 동시에 제어하고 처리하는 압도적 능력을 자랑함.
  • Next.js (JavaScript + Node.js): 특이 케이스. 원래 JavaScript는 브라우저에서만 동작했지만, 2009년 Node.js의 출시로 브라우저 밖에서도 사용이 가능해졌다. 장점은 프론트 개발자와 백 개발자가 같은 언어로 개발이 가능하다는 것. 또한 자유도가 너무 높은 기존 버전 대비 조금은 엄격하게 가이드라인을 강제해 아키텍처를 보다 구조화한 버전이라 알려져 있다.

데이터 저장과 구조화: RDBMS와 MVC 패턴

이제 문제는 그렇게 요청에 따라 새로운 HTML을 제공하는 데는 성공했으나, 해당 데이터를 HTML 문서나 txt 같은 텍스트 파일 위에 계속 쌓는 구조였기 때문에 수천개 데이터가 쌓이면 찾기 어려워진다는 한계에 봉착했다. 그래서 등장한 개념이 'RDBMS'다. 데이터를 표(테이블) 형태로 저장하고, SQL 질의를 통해 데이터를 정의, 조작, 제어하는 방식을 도입했다. 1995년 등장한 MySQL이 대표적.

 

그리고 모든 코드를 한 파일 안에 담아서 개발을 하다보니 정리가 필요해졌다. 이때 정립된 대표적인 아키텍처가 MVC 패턴이다. 각 역할에 따라 Model, View, Controller로 분리한 형태로, 모델은 데이터를 다루는 부분으로써 DB에서 데이터를 가져오거나 저장한다. 뷰는 화면 담당으로 사용자에게 보여줄 HTML을 만든다. 컨트롤러는 교통 정리 담당으로 요청이 들어오면 어떤 데이터를 꺼내 어떤 화면에 보여줄지 결정한다. 코드가 겹치지 않아 꼬이지 않는데다 유지보수가 편했다. 여기에 현실 세계의 데이터 가공 절차 및 규칙을 의미하는 '비즈니스 로직'이 내부적으로 들어가는 구조라고 보면 된다.

모바일 시대와 통신 표준: REST API와 HTTP 메서드

하지만 2007년 아이폰이 등장하면서 모바일 시장이 열리게 됐고, 스마트폰의 앱은 브라우저처럼 서버가 준 HTML 전체를 받아 그리는 것이 아니라 데이터만을 필요로 했기 때문에 서버의 역할이 점차 바뀌게 된다. 이제 HTML을 제작하는 역할이 아닌 데이터를 주는 역할로 바뀌게 된 것. (애초에 앱을 까는 순간 화면이 전부 다 담겨 있기 때문)

 

(REST) API가 여기서 탄생하게 된다. 웹의 기존 HTTP 인프라를 그대로 활용해 주소(URI)에는 제어할 자원을 명시하고, HTTP 메서드(GET, POST...)를 통해 해당 자원의 행위를 정의하는 깔끔하고 직관적인 웹 아키텍처 스타일이다. 웹이든 앱이든 이 창구로 요청을 보내면 서버가 데이터를 돌려주는 방식이다.

 

REST API의 데이터 포맷은 다음과 같다.

  • JSON: 데이터를 주고 받을 때 사용하는 형식으로, 키 밸류 쌍으로 가볍고 단순하다. 과거 복잡한 XML 형식을 대체해 현대 웹/앱 통신의 글로벌 표준으로 자리잡았다.
  • Endpoint: 데이터를 보낼 창구 주소로, 팀원들과 협의해서 사용하면 된다.
  • CRUD: HTTP 메서드를 매칭하는 4가지 동작. 데이터 생성(Create)은 POST, 수정(Update)은 PUT/PATCH, 삭제(Delete)는 DELETE, 읽기(Read)는 GET.

이 때부터 프론트와 백이 독립적인 영역으로 나뉘게 된다. 서버는 데이터만 주면 되고, 화면은 받은 데이터를 기반으로 알아서 프론트가 만드는 방식.

 

HTTP 메서드란 클라이언트가 서버에게 사용자가 어떤 요청을 보냈는지 목적과 종류를 알리는 수단. 가장 많이 쓰이는 건 역시 GET과 POST다. GET은 리소스를 조회한다는 의미로, URL의 Query String에 포함되어 보내진다. (URL 뒤에 ?를 붙이고 키-밸류 형대로 보내는 식) 다만 URL 길이에 제한이 있기에 대용량 전송은 어려운 편이고, 브라우저에 결과가 저장되기에 캐싱이 가능하다. 서버 리소스를 변경하지 않기에 안전하고, 여러 번 연속 보내는 것과 서버 상태가 같기에 멱등성을 보장한다. 아, 그렇다고 해서 절대 민감한 정보를 담아서는 안 된다. 브라우저 주소창과 히스토리에 그대로 노출되기 때문.

 

반면 POST는 리소스를 생성하고 처리하는 작업을 의미하며, 주로 Body에 포함해서 보낸다. 제한이 없기에 대용량 파일 전송이 가능하지만 캐싱, 안정성, 멱등성을 충족하지는 않는다. 서버 리소스를 변경하는 데다 반복 수행시 계속 바뀌기 때문. PATCH는 말 그대로 패치를 붙이듯 리소스의 일부 항목만 수정할 때 사용하고, DELETE는 특정 리소스를 삭제할 때 사용한다.

상태 유지와 보안: 세션/쿠키와 JWT 인증

기본적으로 HTTP 통신은 무상태(Stateless). 독립적이며, 기억력이 없다. 로그인을 성공하더라도 다음 페이지로 이동하면 로그인을 통과했다는 기억을 하지 못한다. 이를 해결하기 위해 등장한 것이 세션과 쿠키다. 다시 말하지만, 세션과 쿠키를 쓴다고 해서 HTTP의 Stateless 성질이 변하는 것은 아니며, 이를 해결하기 위한 보완 장치일 뿐이다.

 

로그인을 예로 들자. 세션은 로그인 성공 정보를 '서버 메모리'에 저장한다. 로그인된 유저의 정보를 서버가 기억하고 있다가, 유저에게 세션 ID 번호표를 발급한다. 쿠키는 '브라우저'에 저장하는 식이다. 브라우저가 요청을 보낼 때마다 번호표를 가지고 다니는 식이다. (HTTP 헤더에 쿠키를 자동으로 얹어 전송)

 

인증과 인가 역시 구분할 필요가 있다. 인증은 그 사람이 '누구인지'를 판별하고, 인가는 그 사람이 '접근할 권한이 있는지'를 판별한다. 서버가 여러 대가 되면 세션 정보 관리가 어려워진다.

 

그래서 JWT가 등장했다. JWT의 구조는 점으로 세 덩어리가 구분된 형태이며 각각 헤더(어떤 방식으로 설명할건지), 페이로드(실제 정보가 담긴 구간), 시그니처(서명 위조 방지용 도장)을 base64 형태로 인코딩한 형태다. 참고로 암호화가 아니다. 누구나 안을 들여다 볼 수 있기 때문에, JWT 페이로드에는 절대 민감한 개인정보를 넣으면 안 된다. 이 경우에는 단방향 해시 함수(SHA-256, bcrypt)를 이용해 원본을 알 수 없도록 해싱하는 과정을 따로 거쳐야 한다.

 

근데 암호화가 되지 않는데 사용하는 이유가 뭘까? 그건 데이터 위변조를 방지하기 때문. 시그니처 영역 덕분에, Payload 내용을 강제로 조작해도 '시그니처' 값이 일치하지 않으면 데이터가 변조되었다고 감지한다. 즉, 기밀성을 보장하는 것이 아니라 무결성을 보장하는 시스템이다.

 

JWT의 송수신 흐름은 다음과 같다. ① 클라이언트가 아이디와 비밀번호를 서버에 전송 ② 서버는 사용자 인증 후, 회원 정보(ID, 권한)를 담은 JWT를 생성해 클라이언트에게 응답으로 보냄 ③ 클라이언트는 받은 JWT를 브라우저 저장소나 쿠키에 저장 ④ 이후 클라이언트가 인증이 필요한 API를 요청할 때 HTTP 헤더에 토큰을 실어 보냄 ⑤ 서버는 전달받은 토큰의 서명(시그니처)을 검증하고 유효하면 요청을 승인함.

 

핵심은 DB 조회를 하지 않는 것. 클라이언트가 JWT를 헤더에 담아 API를 요청하면, 원래는 DB를 조회해서 사용자가 맞는지 체크했지만 키에 있는 정보만 확인하고 응답한다. 또한 서버를 scale-out 해도 토큰이 동일 적용된다. 어떤 서버에도 인증이 가능하다는 뜻. 물론 보안이 중요하면 DB를 매번 조회하는 Token 방식을 추천한다. 정보를 서버가 가지고 오가는 키에는 정보를 담지 않는 방식이다.

 

서버는 유저의 로그인 상태를 보관하지 않기 때문에, 대신 유저의 정보를 담은 토큰에 서버만 아는 비밀키로 암호화된 서명을 담아 유저에게 준다. 유저가 이 토큰을 들고 오면 서버는 자기가 가진 비밀키로 도장이 위조되었는지만 판별한다. 서버 메모리를 쓰지 않아 서버가 늘어나도 상관이 없다.

서버 성능 최적화: 동시성 처리와 캐싱

동시에 여러 요청을 수행하는 방식에는 여러 방식이 있다. Java와 같은 멀티 스레드는 요청이 올 때마다 전담 직원을 하나씩 붙여 처리하는 식이다. 컴퓨터 CPU 성능이 좋으면 유리하지만, 관리 비용(컨텍스트 스위칭, 메모리 소모)이 크다. Node.js 같은 이벤트 루프는 비동기로, 싱글 스레드 안에서 오래 시간이 걸리는 작업은 DB 조회 같이 오랜 시간이 걸리는 작업을 받을 경우 기다리는 동안 다른 요청을 수행하는 것이다. 여기서 핵심 철학은 Non-blocking이다. 하나가 막혀도 다른 것이 진행되어야 한다는 의미로, 시스템 자원의 효율을 극대화하는 방식이다.

 

서버 속도는 비동기로 어느 정도 해결했다지만, DB 조회는 여전히 느리다. 이 경우 자주 요청되는 데이터를 조회 속도가 매우 빠른 '메모리'에 올려두는 '캐싱' 방식을 사용한다. 인메모리 DB인 Redis가 대표적으로, 가장 빠른 임시 기억장치 RAM에 데이터를 키-밸류 형태로 상주시켜 처리 속도를 극대화하는 캐시 전용 시스템이다.

 

캐시에는 TTL(Time To Live)라는 만료 시간 개념이 있는데, 오래된 캐시는 지우고 한번씩 갱신을 해줘야 한다. 데이터가 업데이트됐는데 실제 DB와 다를 경우, 사용자에게는 변경 전 옛날 정보가 노출되는 버그가 생길수도 있기 때문. 캐시 기술 중 대표적인 하나가 CDN이다. 이미지나 동영상 등을 전세계 여러 곳에 미리 복사해두고 사용자 위치가 가까운 서버에서 가져오는 식이다. 아무리 네트워크라도 물리적 거리는 영향이 크다.

데이터 무결성과 트랜잭션

트랜잭션 역시 중요. 중간에 일이 그르칠 경우 아예 없던 일로 처리하고, 모든 일이 종료되면 그제서야 반영을 하는 식이다. 서버에서 트랜잭션을 수행하면 임시 작업 메모장이 열리고, 작업들이 순서대로 실행된 뒤 모두 성공해야 커밋이 이뤄진다. ACID(원자성, 일관성, 격리성, 지속성)을 준수하는 것이 중요.

대규모 트래픽 인프라에 맞춘 확장 기술

서버 인프라를 확장하는 법은 두 가지가 있다. 서버 성능 자체를 좋은 것으로 바꾸는 수직 확장. 서버의 개수를 늘리는 수평 확장. 수직 확장은 비싸고 확장에 한계가 존재한다. 수평 확장은 저렴한 서버를 여러 대 묶어 무한 확장이 가능하지만 요청이 분산되는 문제가 있다. 따라서 로드 밸런서를 추가해 서버 요청을 골고루 분배할 필요가 있다. 대체로 로드 밸런서는 Round Robin 방식을 사용하거나, 현재 연결 수가 가장 적은 Least Connection을 사용한다.

 

모든 코드가 한 서버에 들어가 있던 Monolithic의 경우 기능 하나만 바꾸려고 해도 전체를 다시 배포해야 하는 문제가 있었는데, 기능 별로 서버를 나누는 MSA(Microservice Architecture)가 등장하면서 흐름이 바뀐다. 하나의 거대 서비스를 잘게 쪼개 배포 단위를 완전히 격리하는 아키텍처인 셈. 특정 기능의 장애가 전파되는 사태를 막는다.

 

여기서 중요한 점은 API Gateway의 등장이다. 요청이 어느 서버로 가야하는지 정리해줄 중앙 문지기가 필요했고, 일단 모든 요청을 API Gateway에 보낸 뒤 게이트웨이가 적절한 서비스로 연결하는 방식을 채택했다. Django의 urls.py라고 보면 된다.

 

이 과정에서 메시지 큐를 사용한다. 서버와 서버가 직접 통신할 때 상대 서버가 죽으면 데이터가 유실되거나 대기 상태로 멈춰버리는데, 할 일을 큐에 쌓아두고 이를 순차적으로 수행하도록 하면 처리하는 서버가 자기 페이스에 맞춰 하나씩 꺼내 가므로 급격한 대규모 트래픽 폭주에도 역할을 완벽히 해낸다. RabbitMQ나 Kafka가 이에 속하며, 비동기 메시징 시스템이라 부른다. 이런 일이 안 일어날 것 같다고? 올림픽 경기 때 배민 터지는 것만 봐도, 내가 넣은 주문이 잘 들어갔는지 고민 안 하는 것부터가 메시지 큐 덕을 보고 있는 셈이다.

CI/CD와 컨테이너 환경

이후 CI/CD가 보급되면서 코드를 수정만 해도 자동으로 테스트를 거치고, 검증이 끝나면 빌드를 한 뒤 자동으로 배포한 뒤 서버가 재시작이 되는 시스템이 정착되고, Docker로 환경을 함께 감싸는 컨테이너 개념이 도입되면서 환경 충돌이 해결됐다. 그리고 Docker의 인프라 격리는 꽤 중요한 요소. 실운영과 개발 환경의 분리는 필수적이다. 개발 환경에서 편하게 테스트도 하고, 코드도 망가뜨려 보고... 만약 실운영 환경에서 실수로 데이터라도 지우면 복구도 불가능하다.


Today I Listend to