웹 개발을 처음 배우면 프레임워크 사용법부터 익히게 된다. 스프링으로 컨트롤러를 만들고, JPA로 엔티티를 매핑하고, React로 화면을 그린다. 그런데 막상 면접에서 “왜 WAS가 따로 있나요”, “세션은 왜 필요한가요”라는 질문을 받으면 막힌다. 도구는 쓸 줄 알지만 그 도구가 왜 생겼는지를 모르기 때문이다.
웹 서비스 구조는 한 번에 설계된 것이 아니다. 단방향 요청에서 시작해 양방향 상호작용으로, 다시 송수신·처리·자료를 분리한 3-tier 구조로 진화했다. 이 글은 그 진화의 흐름을 단계별로 따라간다. 각 단계에서 어떤 문제가 생겼고, 그 문제를 풀기 위해 무엇이 등장했는지를 순서대로 짚으면 지금의 웹 구조가 왜 이렇게 생겼는지가 한 장의 지도로 정리된다
단방향 요청에서 양방향 상호작용으로
초기 웹은 단방향이었다. 클라이언트가 서버에 문서나 리소스를 요청하면 서버가 그것을 돌려준다. 도서관에서 책을 빌려 읽는 것과 같다. 내가 읽고 끝날 뿐, 책의 내용이 나 때문에 바뀌지는 않는다.
여기에 곧 한계가 생긴다. 사용자가 단순히 정보를 받기만 하는 것이 아니라, 결과 자체에 개입하고 싶어진다. 라이브 방송에 채팅으로 참여해 방송 내용에 영향을 주는 것과 같다. 이 지점에서 웹은 양방향 상호작용으로 전환된다. 클라이언트가 서버에 값을 던지는 데 그치지 않고, 서버가 만들어 내는 결과에 직접 관여하는 구조다.
양방향이 되면 새로운 문제가 따라온다. 상호작용은 절차를 만들고, 절차는 상태(state)를 만든다. 그리고 상태는 전이(transition)한다. 로그인을 떠올리면 쉽다. 로그인하기 전과 후는 명백히 다른 상태이고, 사용자의 행동에 따라 한 상태에서 다른 상태로 넘어간다. 이 상태 개념이 이후 웹 구조 전체를 복잡하게 만드는 출발점이다
GET이 읽기라면 POST는 업로드다
로그인을 기능 단위로 뜯어보자. 보통 아이디와 패스워드 같은 지식 기반 인증 정보를 입력받고, 사용자가 버튼을 클릭한다. 이때 서버로 HTTP 요청이 가긴 가는데, 기존 GET과는 의미가 다르다
GET은 “내가 무엇을 선택했다”는 의미에 가깝다. 서버에 정보를 요청하고, 받은 것을 화면에 표현한다. 본질적으로 읽기(Read)다. 반면 로그인처럼 폼 데이터를 서버에 제출해 그 처리 결과를 받아야 할 때는 클라이언트가 서버로 정보를 올려보내는 성격이 강하다. 이때 쓰는 메서드가 POST다
| 구분 | GET | POST |
|---|---|---|
| 의미 | 선택·조회 | 제출·전송 |
| 성격 | 읽기(Read) | 업로드 |
| 데이터 위치 | URL 쿼리스트링 | 요청 본문(body) |
| 대표 용도 | 페이지 조회, 검색 | 로그인, 회원가입, 글 등록 |
서버 입장에서 POST로 들어온 아이디·패스워드는 원격지 사용자 입력(remote user input)이다. 이 입력은 그에 따른 처리를 요구한다. 그리고 바로 이 “처리”를 누가, 어떻게 할 것인가에서 웹 구조의 진화가 시작된다
CGI는 플랫폼에 직접 묶여 있었다
처리를 담당하던 초기 방식이 CGI(Common Gateway Interface)다. 과거 CGI는 C 언어로 많이 작성했다. 작은 리소스로 좋은 성능을 냈지만 구조적인 약점이 있었다. 이 약점을 이해하려면 컴퓨터의 구성부터 짚어야 한다
컴퓨터는 하드웨어와 소프트웨어로 나뉘고, 소프트웨어는 다시 시스템 소프트웨어와 애플리케이션으로 나뉜다. 그리고 컴퓨터의 정체성을 결정하는 핵심은 CPU다. CPU는 자신이 지원하는 코드 단위로만 명령을 인식하는데, 이를 머신 코드(machine code)라고 부른다. 그래서 CPU를 머신(machine)이라고도 한다. 여기에 시스템의 핵심인 운영체제가 더해지면, CPU와 OS를 합쳐 플랫폼(platform)이라 부른다. 이 플랫폼에 직접 의존하는 코드를 네이티브 코드(native code)라 하고, C 언어가 바로 여기에 해당한다
문제가 여기서 드러난다. C로 작성한 CGI는 플랫폼에 직접 의존한다
[웹 서버] → [CGI 프로그램(C로 작성, 네이티브 코드)] → 플랫폼에 직접 의존
↑
OS가 바뀌면 다시 작성·배포해야 함
웹 자체는 특정 OS에 종속되지 않는다. 그런데 구현 언어가 플랫폼에 묶이는 바람에, 서비스를 구동하는 OS 환경이 바뀌면 CGI를 다시 작성해 배포해야 했다. 보안 이슈까지 더해지면서, C가 아닌 대체 수단을 찾게 된다
자바와 WAS가 처리를 전담하게 됐다
이 시점에 등장한 것이 자바다. 자바의 핵심 장점은 특정 플랫폼에 종속되지 않는다는 점이다. 환경이 바뀐다고 서비스 로직을 다시 작성할 필요가 없다. “자바로 서버 측 처리를 구현하면 어떨까”라는 발상이 자연스럽게 나온다
여기에 관심사 분리라는 발상이 더해진다. 웹 서버가 하는 일은 본질적으로 송수신이다. 송수신을 담당하는 서버에게 복잡한 연산까지 맡기는 것은 적절하지 않다. 그래서 처리(연산)만 전문으로 담당하는 존재를 따로 둔다. 웹 환경에서 애플리케이션 역할을 하는 이 서버를 웹 애플리케이션 서버(WAS, Web Application Server)라 부른다. 국내에서는 이 WAS를 자바로 구현하는 것이 사실상 일반적인 형태가 됐다
구조는 다음과 같이 바뀐다
[클라이언트] --요청--> [웹 서버(송수신)] --전달--> [WAS(처리)] --조회--> [DB]
웹 서버는 원격지 사용자 입력을 수신해 처리 주체인 WAS에 넘긴다. WAS는 주어진 입력으로 연산을 수행한다. 예를 들어 문서 템플릿을 만들어 두고 그 안의 빈 자리에 값을 채운다. “○○님 안녕하세요” 템플릿에서 사용자가 철수임이 확인되면 “철수님 안녕하세요”가 든 HTML을 새로 만들어 돌려준다
이렇게 만들어진 HTML은 기존 HTML과 다르다. 디스크에 저장된 파일이 아니라, 요청 시점에 연산으로 생성된 문서다. 이런 문서를 동적 문서(dynamic document)라 부른다. 템플릿 형태의 틀에 구멍을 뚫어 두고 서버가 값을 채워 내보내는 이 방식이 JSP(JavaServer Pages)다. JSP는 지금은 주류 방식이 아니다. 현재는 뒤에서 다룰 REST API와 프론트엔드 프레임워크 조합이 일반적이지만, 동적 문서가 어떻게 등장했는지를 이해하는 출발점으로는 여전히 유효하다
상태를 기억하기 위해 세션·쿠키·DB가 동원됐다
로그인은 양방향 상호작용의 결과를 요구하고, 상태를 전이시킨다. 그러면 “어디까지 무엇을 했는가”를 어딘가에 기억해 둬야 한다. 문제는 서버가 상대하는 클라이언트가 매우 많다는 점이다. 한두 명이 아니라 수많은 사용자의 상태를 동시에 기억해야 한다
그래서 서버는 대규모 데이터 처리 체계, 즉 데이터베이스를 도입한다. “누구와 어디까지 어떤 상호작용을 했는가”를 한시적으로 메모리에 담기도 하지만, 영속적인 정보는 DB에 쌓는다. 클라이언트 쪽은 이 기억을 쿠키(cookie)로 구현한다
쿠키는 기본적으로 키와 값으로 이루어지고, 적용 범위(도메인·경로)와 유효 기간 같은 속성이 덧붙는다. 파일로 저장될 수도 있다. 정리하면 양방향 상호작용 과정에서 생겨나는 정보를 서버는 DB에, 클라이언트는 쿠키에 나눠 기억한다
로그인 전에서 로그인 후로 넘어간 상태는 그 뒤로도 지속되어야 한다. 서버가 이 상태를 관리할 때, 일련의 양방향 상호작용을 하나의 단위로 묶은 것을 세션(session)이라 부른다. “사용자 세션이 시작됐다”는 표현이 여기서 나온다
그런데 왜 이런 상태 관리를 쿠키·세션·DB라는 별도 장치로 구현했을까. HTML이나 HTTP가 직접 해 주면 되지 않았을까. 답은 HTTP의 설계에 있다. HTTP는 처음 설계될 때 상태 개념이 없었다. 무상태 프로토콜(stateless protocol)이다. 한 요청과 다음 요청은 서로를 기억하지 못한다. 그래서 상태를 유지하려면 쿠키와 세션이라는 별도 메커니즘이 필요했던 것이다
처리 구조는 MVC로 정리됐다
웹 서비스의 동작을 사용자 관점에서 보면 단순하다. 사용자가 브라우저에서 버튼을 클릭하는 행위 자체가 시스템에 대한 제어(control)다. 이 제어가 HTTP 요청을 발생시키고, 요청이 WAS에 전달되면 WAS가 그에 따른 처리를 한다. 즉 사용자 요청은 곧 처리를 유발하는 제어 명령이다
WAS를 기준으로 양쪽을 나눠 보면 역할이 분명해진다
[사용자 제어] → Controller(제어 처리) → Model(데이터 처리) → View(HTML 출력) 클릭·입력 요청 해석 DB 입출력 눈에 보이는 결과
들어오는 쪽에는 제어 명령을 받는 컨트롤러(Controller)가 있고, 데이터를 다루는 모델(Model)이 있으며, 산출물을 사람이 볼 수 있는 HTML로 내보내는 뷰(View)가 있다. 머리글자를 따면 MVC다. MVC 아키텍처는 결국 사용자 인터페이스·데이터·로직을 분리하라는 소프트웨어 설계 원칙을 웹에 적용한 결과물이다.
처리가 전부 WAS로 넘어가면서 CGI는 사실상 역사 속으로 물러났다. 아직 쓰이긴 하지만, C로 웹을 개발하는 경우는 매우 드문 예외다
데이터 접근은 SQL 위에 추상화 계층이 쌓였다
WAS는 데이터베이스와 직접 연결된다. 이 연결을 표준 인터페이스로 다루는데, 일반적으로 ODBC라 하고 자바에서는 JDBC(Java Database Connectivity)를 쓴다. JDBC가 제공하는 API로 DB 입출력을 수행한다
JDBC 위로는 더 쓰기 편하게 해 주는 계층이 겹겹이 쌓였다. MyBatis 같은 SQL 매퍼가 나왔고, Hibernate 같은 ORM이 등장했으며, 자바 표준인 JPA(Java Persistence API)가 자리 잡았다. 위로 갈수록 SQL을 직접 다루는 부담을 줄이고 객체 중심으로 DB를 다루도록 추상화한다
[애플리케이션 코드]
│
JPA / Hibernate / MyBatis ← 추상화 계층 (쓰기 편함)
│
JDBC ← 표준 인터페이스
│
[데이터베이스]
추상화가 올라가도 바닥에서는 결국 SQL로 DB를 통제한다. 로그인을 예로 들면 SELECT가 쓰인다
사용자 테이블에서 아이디로 사용자를 조회한다
SELECT * FROM users WHERE id = ?;
조회된 사용자가 있으면(found) 저장된 패스워드 해시와 입력값을 비교해 로그인 성공 여부를 판정한다. 성공하면 “철수님 안녕하세요” 화면으로, 사용자를 찾지 못하거나(not found) 패스워드가 일치하지 않으면 “누구신가요” 같은 화면으로 상태가 전이한다. 상태 전이가 조회 결과에 따라 갈린다
한 가지 짚을 점이 있다. 아이디와 패스워드를 한 번에 비교하는 WHERE id = ? AND password = ? 형태의 평문 비교 쿼리는 실제 서비스에서 쓰지 않는다. 패스워드는 해시로 저장하고, 아이디로 조회한 뒤 해시를 비교하는 것이 현재의 기본이다. 흐름을 이해하기 위한 예시로만 보면 된다
스프링은 의존성 관리를 프레임워크가 맡게 했다
자바로 WAS를 개발하면서 새로운 고민이 생긴다. 객체지향에서 설계란 결국 클래스 간의 관계를 다루는 일이고, 관계를 논하면 늘 의존성(dependency)이 따라온다. 의존성을 잘 떨어뜨려야 유지보수성이 올라간다
의존성을 극단적으로 낮추기 위해, 개발자가 코드에서 직접 new로 인스턴스를 생성하는 대신 인스턴스 생성과 생명 주기를 대신 관리해 주는 메커니즘이 등장했다. 이것이 아예 프레임워크 형태로 만들어졌다. 정해진 규칙에 따라 클래스를 만들어 등록만 하면 프레임워크가 인스턴스를 생성하고 관리한다. 이때의 인스턴스를 빈(bean), 자바 빈이라 부른다. 이 프레임워크가 스프링(Spring Framework)이고, 국내 전자정부 표준 프레임워크의 기반이기도 하다
핵심 개념을 한 줄로 정리하면 이렇다. 객체 생성과 연결의 제어권을 개발자가 아니라 프레임워크가 갖는 제어의 역전(IoC)이고, 필요한 의존 객체를 프레임워크가 주입해 주는 것이 의존성 주입(DI)이다
모든 입출력은 REST API의 CRUD로 수렴했다
웹 서비스를 충분히 만들어 보니 하는 일의 본질이 단순했다. 요청(request)이 가면 응답(response)이 온다. 그 요청은 결국 데이터에 대한 입출력(I/O)을 유발한다
데이터에 대한 입출력을 나눠 보면 네 가지로 정리된다. 새로 만들거나(Create), 읽거나(Read), 수정하거나(Update), 삭제(Delete)하는 것이다. 이 네 가지가 하나의 단위 기능으로 매핑되어 기본 서비스 기능이 된다. 이를 API 형태로 구현한 것이 REST API다
요청(Request) → DB I/O → 응답(Response) Create → INSERT → 생성된 자원 Read → SELECT → 조회 결과 Update → UPDATE → 변경 결과 Delete → DELETE → 삭제 결과
REST에서는 이 CRUD를 HTTP 메서드와 대응시킨다. 보통 Create는 POST, Read는 GET, Update는 PUT 또는 PATCH, Delete는 DELETE에 매핑한다. 모든 입출력이 이 형태로 정리되면서 서버 기능의 설계가 단순하고 일관되게 바뀌었다
프론트엔드 프레임워크가 장치 의존성을 해결했다
응답으로 받은 HTML을 화면에 출력할 때 또 다른 문제가 있다. HTML 출력은 장치의 영향을 받는다. 모니터 해상도는 Full HD부터 4K까지 제각각이고, 스마트폰과 태블릿은 기종마다 화면이 다르다. 장치 특성에 따라 HTML이 달라져야 한다
초기에는 정적 파일을 장치마다 여러 벌 만들어 두고, 클라이언트가 요청할 때 장치 정보를 확인해 맞는 것을 내려줬다. 같은 내용을 여러 번 개발하는 낭비가 생긴다
발상을 바꾼다. 어차피 본질이 I/O라면, 서버는 장치에 맞춘 HTML 대신 데이터만 보내면 된다. JSON이나 XML 형식으로 데이터만 내려보내고, 클라이언트의 자바스크립트가 그 데이터를 받아 장치에 맞는 HTML을 즉시 생성한다
[기존] 서버가 장치별 HTML을 만들어 전송 → 장치 수만큼 중복 개발 [개선] 서버는 JSON 데이터만 전송 → 클라이언트 JS가 장치에 맞는 HTML 생성
이 클라이언트 측 생성을 전문으로 돕는 것이 프론트엔드 프레임워크다. React.js, Vue.js가 대표적이다. 이들을 활용해 HTML을 동적으로 생성하는 방식으로 장치 의존성 문제를 풀었다
3-tier 구조와 성능 모니터링까지가 한 그림이다
여기까지 오면 서버 관점에서 구성 요소가 세 덩어리로 나뉜다. 송수신을 담당하는 웹 서버, 처리를 담당하는 WAS, 자료를 담당하는 데이터베이스다. 이 셋을 합치면 3-tier 구조가 된다
[클라이언트(React/Vue)]
│ HTTP Request / Response (JSON)
▼
┌─────────────┐
│ 웹 서버 │ 1-tier : 송수신
├─────────────┤
│ WAS │ 2-tier : 처리(로직)
├─────────────┤
│ 데이터베이스 │ 3-tier : 자료
└─────────────┘
마지막으로 성능을 짚는다. 어떤 요청이 가면 클라이언트는 그 응답이 올 때까지를 지연으로 느낀다. 국내는 네트워크 환경이 좋아 송수신에서 문제가 잘 생기지 않는다. 병목은 주로 두 곳에서 난다. DB에 질의를 보냈을 때의 응답 시간, 그리고 프로그램을 잘못 짰을 때 WAS의 처리 시간이다
이 두 곳을 모니터링하는 체계가 APM(Application Performance Monitoring)이다. 오픈소스인 스카우터(Scouter)가 대표적이다. 스카우터는 DB 응답 시간과 WAS의 JVM 상태를 모니터링해 시각화된 대시보드로 보여준다. 관리자는 대시보드만 보고 어디서 병목이 나는지 파악한다
병목의 핵심은 대개 DB다. DB 성능을 끌어올리는 대표적인 방법은 두 가지다. 읽기 부하를 분산하기 위해 노드를 병렬로 늘리는 수평 확장(scale-out), 그리고 조회 성능을 높이는 인덱스(index) 활용이다
[성능 개선 두 축] DB scale-out : 노드를 수평으로 늘려 처리량 분산 Index 활용 : 자주 조회하는 컬럼에 인덱스를 걸어 조회 속도 향상
정리
정리하면 웹 서비스 구조는 단방향 요청에서 양방향 상호작용으로, 다시 송수신·처리·자료를 분리한 3-tier 구조로 진화했다. 각 단계는 앞 단계의 한계를 푸는 과정이었다. 상태가 필요해서 세션이, 플랫폼 종속을 벗어나려 WAS가, 의존성을 낮추려 스프링이, 장치 의존성을 풀려고 프론트엔드 프레임워크가 등장했다. 도구의 이름보다 그 도구가 등장한 이유를 기억하면, 새로운 기술이 나와도 그것이 어떤 문제를 푸는지로 위치를 잡을 수 있다
이 글은 서비스 구조에 집중했다. 이어지는 2부에서는 이 구조 위에 반드시 얹어야 하는 웹 보안을 다룬다