[WEB 개발 기초] HTTP의 특징을 파헤치자 ① : 클라이언트-서버 아키텍처, 무상태성, 비연결성

이민선(Jasmine)·2023년 5월 3일
0

WEB 개발 기초

목록 보기
4/7
post-thumbnail

오늘은 HTTP의 특징 4가지 중 3가지를 정리해보려고 한다.

  • 클라이언트-서버 아키텍처 ✅
  • 무상태성(statelessness) ✅
  • 비연결성(connectionlessness) ✅
  • HTTP 메시지 ⏹️ 다음시간에~~

HTTP 메시지는 특히 설명해야 할 내용이 아주 많기 때문에(HTTP 메시지 구조, 헤더 내용, 상태 코드 등등) 다음 포스팅에서 별도로 다루려고 한다. 클라이언트-서버 아키텍처부터 고고싱~~~

1. 클라이언트-서버 아키텍처

웹 기반 애플리케이션 설계에 사용되는 기본적인 구조이다. 클라이언트, 서버, 네트워크 연결로 구성된 아키텍처이며, 서버와 클라이언트의 역할을 분리하여 독립적인 진화를 지향한다.

클라이언트: user interface를 담당. 서버에서 정보를 요청하고 응답을 받아와서 유저에게 보여주는 역할이다.
서버: 클라이언트의 요청에 응답을 생성하여 전송. 데이터와 비즈니스 로직은 대부분 서버 측에서 처리된다.

이렇게 역할을 분담하는 게 좋은 이유가 뭘까? 그냥 한 쪽에서 다하는 것보다 뭐가 좋다는 걸까?

  • 느슨한 결합(loose coupling)
    클라이언트-서버 간의 상호의존성을 줄이는 것이다. 서버의 성능을 향상시키고 싶을 떄는 서버만 향상시키면 되고, 클라이언트의 성능을 향상시키고 싶을 때는 클라이언트를 업그레이드하면 된다. 즉, 전체 시스템의 성능을 향상하려고 할 때 굳이 모든 요소를 다 업그레이드 할 필요가 없어지므로 유지보수 측면에서도 이점이 있다.

  • 보안성
    서버 측에서 중요한 데이터를 처리하고 클라이언트는 요청만 한다면, 클라이언트에서 중요 정보가 노출되는 일이 생기더라도 서버 내부에서는 다른 중요 정보들을 DB에 암호화하여 꽁꽁 숨겨 놓기 때문에 상대적으로 보안 위협이 크게 높아지지는 않는다.

이렇게 2개로 구분할 때는 2계층 아키텍처라고도 하고, DB를 아키텍처에 포함시켜 설명하면 3계층 아키텍처라고 한다. 아래 그림에서 3계층 아키텍처를 시각화해보자.


출처: 위키피디아
https://ko.wikipedia.org/wiki/%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4_%EB%A1%9C%EC%A7%81#/media/%ED%8C%8C%EC%9D%BC:Overview_of_a_three-tier_application_vectorVersion.svg

위의 사진에서 Presentation tier를 클라이언트(프론트엔드 개발자)가 담당, Logic tier를 서버(백엔드 개발자)가 담당, Data tier를 DB에서 담당한다고 생각하면 된다.

근데 서버가 담당한다는 비즈니스 로직이라는 단어가 정확히 뭘 의미하는 걸까? 자주 듣기도 하고 대충은 알 것 같으면서도 정확히 정리가 안된 개념이랄까? 그래서 비즈니스 로직의 정의를 찾아봤다.

비즈니스 로직

특정 비즈니스 목적을 달성하는 과정에서 데이터를 가지고 수행되는 계산, 검증, 규칙 적용 등의 처리 작업이다.
은행 웹 사이트로 예를 든다면, 고객 계좌 잔액 계산, 송금 시 계좌 비밀번호 검증 등의 로직을 떠올려볼 수 있다. 이런 비즈니스 로직은 일반적으로 서버 측에서 구현되고, 클라이언트는 유저의 요청을 서버로 보내고 응답을 받아 view로 보여주는 역할을 한다고 생각하면 되겠다.

이제 3계층 아키텍처와 비즈니스 로직의 의미까지 살펴봤으니, HTTP의 다음 특징인 무상태성에 대해 살펴보자.

2. 무상태성(stateless)

클라이언트와 서버 간의 통신에서 이전에 주고받았던 정보나 상태를 유지하지 않는 것을 의미한다. 즉, 각 요청 사이에는 서버 측에서 상태를 보존하지 않는 것이 무상태성의 개념이다. 서버 입장에서는 클라이언트에서 오는 각각의 요청이 독립적이고 새롭다.

무상태성과 상태성을 비교하기 위해 자주 제시되는 예시가 있다. 커피를 마시러 카페에 갔다고 생각해보자.

웰컴투 stateful cafe

나: 블랙밀크티 한 잔 주세요
점원: 4000원입니다. 결제는 어떻게 하시겠어요?
나: 카카오페이 결제로 할게요. (휴대폰을 내밀며) 여기 있습니다.
점원: 계산 됐습니다. 포인트 적립 하시나요?
나: 아니요 괜찮습니다.
점원: 네 잠시만 기다려주세요.

인간들의 세계는 stateful하다. 내가 카카오페이 결제하겠다고 휴대폰을 내밀면 점원은 내가 블랙밀크티를 주문했던 사실을 기억하고 포스에 찍고 결제를 도와준다.

반면 HTTP 요청은 무상태적이다.

웰컴투 stateless cafe

나: 블랙밀크티 한 잔 주세요
점원: 4000원입니다. 결제는 어떻게 하시겠어요?
나: 카카오페이 결제로 할게요. (휴대폰을 내밀며) 여기 있습니다.
점원: (갑자기 왜 바코드를 보여주지...???) 뭘 결제하실 건데요?
나: (..?? 뭐지..? 그냥 집에서 타먹어야지. 퇴장한다.)
.
.
다음 손님: 자몽허니블랙티 한 잔 주세요.
점원: 4300원입니다. 결제는 어떻게 하시겠어요?
다음 손님: 자몽허니블랙티 한 잔을 카카오페이로 결제할게요.
점원: 결제되었습니다. 포인트 적립하시나요?
다음 손님: 자몽허니블랙티 한 잔 카카오페이로 결제했으니 포인트 적립해주세요.
점원: 네 적립되었습니다. 음료 나올 때까지 대기해주세요.

이렇게 내가 이전에 요청한 걸 기억하지 못하면, 서버(카페 사장님)는 무상태성을 가진 것이다. 인간의 입장에서 stateless cafe는 어색하기 그지 없다. 그런데 HTTP 요청/응답에서 클라이언트의 이전 상태를 왜 보존하지 않는걸까?
그건 다 stateless할 때의 장점이 있기 때문이다.

장점: 서버의 확장성

일단 서버 scale-out(수평 확장)에 유리하다. 클라이언트의 이전 상태를 서버에서 유지할 필요가 없기 때문에 서버의 부하를 분산시키기가 쉽다. 또한 서버의 장애가 발생하더라도 다른 서버가 클라이언트의 요청을 처리할 수가 있어서 시스템의 가용성이 향상된다.

이해를 돕기 위해 stateful한 게 수평 확장에 왜 불리한지를 느껴보자.

  • 중간에 서버에 문제라도 생긴다면?

나: 블랙밀크티 한 잔 주세요
점원: 4000원입니다. 결제는 어떻게 하시겠어요? 윽 갑자기 배가 아프다!
(화장실로 도망)
사장님 등장.
나: 카카오페이 결제로 할게요. (휴대폰을 내밀며) 여기 있습니다.
사장님: ..? 음료 뭐 주문 하셨었죠?

점원이 고객의 주문 정보를 사장님께 공유하지 않고 개인적인 사정이 생겨 화장실로 뛰어가버렸다. (서버 불능 상태) 이 때 만약 stateless하다면, 나는 내 주문을 받는 사람이 점원인지 사장님인지는 상관없고 '블랙밀크티 한잔 카카오페이로 결제'라는 상태를 모든 요청에 매번 언급한다면 아무런 문제 없이 주문이 잘 될 것이다.

  • 점원 수를 늘린다면?

블랙밀크티를 마시면서 열심히 블로깅을 하다가 포인트 적립하는 걸 까먹었다는 사실을 깨달았다. 그런데 알고보니 이 카페에는 직원이 3명이 있었고, 내 주문을 받았던 사장님은 점심을 드시러 가셨다.
나: 아까 포인트 적립하는 걸 까먹어서 그런데 지금 적립 되나요?
방금 출근한 점원 : ...? 어떤 음료를 몇 시 쯤에 주문하셨는데요?

대뜸 적립하겠다고 얘기하는 건 점원이 stateful할 거라고 기대하기 때문일 것이다. 만약 요청을 할 때 stateless할 거라고 기대하고 요청한다면, '제가 20분 전에 자몽허니블랙티를 주문했는데 포인트 적립을 안해서 지금 처리해주세요'라고 하면 문제가 없을 것이다.
이렇게 stateless한 상황에서 요청을 한다면, 손님이 100명 200명 많아져도 각 점원은 현재 요청 내용에만 집중하면 되기 때문에 점원 수를 10명으로 늘려도 문제가 없어진다.

클라이언트-서버 아키텍처에서는 위와 같은 stateful함의 문제 때문에 stateless를 지향한다. 동시 접속자 수가 많아져서 서버를 증설하고 싶을 때 모든 요청이 무상태적이어야만 서버끼리 고객의 요청 정보를 굳이 공유하며 작업을 처리할 필요도 없어진다.

단점: 불필요한 데이터 전송 초래

생각해보면 서버의 부담은 줄지만 매번 클라이언트가 모든 정보와 상태를 일일히 다 전송해야 하므로 클라이언트에게 부담이 되는 구조일 수 있다.

그리고 데이터 전송량이 너무 많이진다는 문제도 있다. 아까 카페 예시에서 본 것처럼, 포인트 적립할지 말지 대답하는데 음료 뭐 주문할지 또 얘기하려면 입 아플 것이다.

이에 대한 대안으로는 캐싱이 있다. 클라이언트와 서버 간의 데이터 요청과 응답을 캐싱하여, 서버의 부하를 줄이고 응답 속도를 향상시킬 수 있겠다.

100% 무상태성 과연 가능? 글쎄.

로그인 정보를 생각해보자. 유저가 로그인 시도 후 어떤 페이지 GET 요청을 했는데 무상태성 때문에 로그인한 상태가 저장되지 않는다면? 그럼 클라이언트는 번거롭게도 매번 유저 정보를 쿼리 파라미터에 담던가 해서 서버에 보내주어야 한다.

그래서 실무에서는 쿠키나 세션을 사용해서 로그인 상태를 stateful하게 유지한다.
이처럼 무상태성을 지향하기는 하지만, 로그인만 생각해봐도 100% 무상태성은 불가능에 가깝다. 로그인 정보 등 인증 정보는 상태를 유지해야하지만, 그외에는 서버 확장성을 고려하여 상태 유지는 최소한으로 사용하는 것이 바람직하다고 한다.

3. 비연결성(connectionless)

사실 HTTP의 비연결성은 버전 1.0의 큰 특징이었고, RTT(Round Trip Time, 패킷 왕복 시간) 증가 문제가 대두되자 버전이 업그레이드 되면서 점차 비연결성이라는 특징은 희미해져 왔다.

HTTP/1.0

한 번의 TCP 연결 당 하나의 요청을 처리하는 구조였다. TCP/IP모델을 떠올려보면 애플리케이션 계층에서 전송 계층으로 HTTP 메시지를 넘겨주고, 전송 계층에서 3-way-handshake를 비롯한 개념적 연결이 이루어지는데 이를 TCP 연결이라고 볼 수 있다.

클라이언트의 요청에 서버가 한 번 응답할 때마다 연결을 끊어버리기 때문에 지속 연결에 따른 자원의 소모를 최소화할 수 있다는 장점은 있었지만, 서버로부터 파일을 가져오려고 하기만 하면 3 way handshake를 계속 여는 등 새로운 연결을 또 하고 또하는 건 너무 번거로운 일이었다. 이와 같은 RTT 증가 문제를 해결하기 위해, HTTP 버전이 업그레이드 될 때마다 새로운 방법이 고안되었다.

HTTP/1.1

1) 지속 연결(persistent connection)의 표준화


출처: 위키피디아

좌측은 HTTP/1.0일 때의 비연결성을, 우측은 HTTP/1.1일 때의 지속 연결을 보여준다.

HTTP/1.0에서는 keep-alive 옵션이 존재하기는 했지만 기본 option은 아니었다. HTTP/1.1에서 비로소 지속 연결이 표준화되었다. 이에 따라 한개의 TCP 연결을 유지하면서 여러개의 요청과 응답 메시지를 처리할 수 있게 되었다. 이를 지속 연결(persistent connection)이라고 한다. 비연결성으로 대표되었던 HTTP/1.0의 1 요청 1 연결 메커니즘이 사라졌기 때문에, HTTP/1.0에 비해 RTT 증가 문제가 많이 완화되었다.

하지만 여전히 지속 연결 하에서는 요청과 응답이 동기적으로(순차적으로) 처리되어 HOL(Head-of-Line) Blocking이 발생하여 성능 저하 문제가 있었다. HOL blocking이란, 같은 큐(queue)에 있는 패킷이 이전 패킷에 의해 지연되는 현상을 의미한다.

헤더에 메타 데이터가 많이 들어 있어서 압축이 되지 않아 무거운 구조였다는 한계점도 있었다.

HTTP/2

이에 HTTP/2.0에서는 많은 기술적인 개선이 이루어졌다.

1) 지속 연결 대신 멀티 플렉싱 방식 채택

멀티플렉싱 방식이란, 하나의 TCP 연결을 통해 여러개의 데이터 스트림을 동시에 전송하는 비동기적인 방식이다. 동시에 여러개의 요청과 응답을 처리할 수 있기 때문에 성능이 향상되고, 위에서 언급한 HOL blocking 문제도 해결이 가능하다.

2) 헤더 압축

헤더 압축 기술을 사용하여 위에서 언급한 큰 헤더 구조에 의한 성능 저하 문제를 완화할 수 있었다.

하지만 상술했듯이 HTTP/2.0도 물론 TCP 기반이었기에 근본적으로 TCP 연결 설정 과정에서의 지연 시간 문제를 완전히 해결할 수는 없었다.

HTTP/3

- TCP 대신 UDP 기반의 QUIC 프로토콜 사용

출처: https://peering.google.com/#/learn-more/quic

HTTP/3에서는 TCP 기반이 아니라 UDP 기반으로 돌아간다. 어라? TCP/IP 모델 공부할 때 UDP는 신뢰성 없는 프로토콜이라고 배웠는데? 대신 UDP는 TCP와 달리 3-hand-shake가 없어서 빠르기는 했었지. 그럼 HTTP/3는 빠르지만 신뢰성이 저하된 프로토콜인가욥??!!

정확히 얘기하면 UDP 프로토콜 기반의 'QUIC'이라는 프로토콜을 기반으로 하기 때문에 TCP와 비교해도 전송 속도와 신뢰성 2마리 토끼를 다 잡은 프로토콜이라고 할 수 있겠다. QUIC은 UDP 기반이기는 하지만, 비연결성 프로토콜인 UDP와 달리 연결 기반 프로토콜이다. 연결 설정 과정에서 패킷 손실을 감지하고 재전송하기 때문에, 패킷 전송에 대한 보장성을 높일 수 있다. 따라서 신뢰성 문제가 보완된 프로토콜이라고 할 수 있겠다.

게다가 HTTP/2의 장점이었던 멀티플렉싱도 지원하는 데다가, UDP 기반 프로토콜이어서 빠르기까지 하니, 데이터 전송 속도에 있어서는 최적화가 많이 된 버전이라고 할 수 있겠다.

출처: https://peering.google.com/#/learn-more/quic

현재 대부분의 최신 브라우저들은 HTTP/3를 지원하고 있으며, 이러한 지원은 더욱 늘어날 것으로 보인다.

여기까지 HTTP의 3가지 특징 정리해보았다! 파도파도 중요한 것이 넘쳐나는 HTTP 세상~~ 다음 시간에는 HTTP 메시지 구조와 헤더에 대해 포스팅하려고 한다.

출처: 위키피디아

참고:

https://mommoo.tistory.com/67
https://peering.google.com/#/learn-more/quic

profile
기록에 진심인 개발자 🌿

2개의 댓글

comment-user-thumbnail
2023년 5월 4일

인간들의 세계는 stateful하다. 여기 상태유지, 무상태성 설명 맛집이네요!!!👍🧚🏻‍♂️

1개의 답글