우아한테크코스 레벨2 에서 매트가 주관한 스터디로, 인프런 김영한님의 강의 모든 개발자를 위한 HTTP 웹 기본 지식 스터디를 진행중이다. 이 포스팅에서는 스터디에 PR을 날릴 내용과 함께 스터디 시간에 얻어간 내용을 적을 예정이다.
1. HTTP 기본
우리는 대부분의 통신을 Http에 넣어서 통신하고 있다. Html, Text, Json, Xml 등. 데이터를 주고받을 때에 Http를 사용하기 때문에 개발자라면 Http 지식은 기본 교양으로 가지고 있어야 한다.
현재 우리가 가장 많이 사용하고 있는 것은 Http/1.1 버전이다. Http/2, Http/3 버전이 있음에도 Http/1.1 버전을 사용하는 이유는 우리가 필요로 하는 기본 스펙이 충분히 존재하기 때문이다. Http/2, Http/3 버전은 아직 성능 개선에 초점이 맞춰져 있어서 1.1 버전을 사용해도 충분하다고 한다. 참고로 ~ Http/2 버전까지는 TCP 기반, Http/3 버전은 TCP 대신 UDP를 사용하는 방안으로 개선 중이라 한다. 즉, Http 프로토콜도 TCP 프로토콜을 기반으로 작동하는 것이기 때문에 1주차에서 배운 TCP/UDP 프로토콜 내용도 잘 복습해놔야 한다.
클라이언트 서버 구조
1주차에서도 배웠지만, Http 통신은 클라이언트에서 요청을 보내면 서버에서 응답을 보내는 방식으로 이루어진다. 우리는 사용자에게 요청에 대한 응답을 처리해주기 위해 서버와 유저 에이전트(클라이언트)를 개발한다. 이 때의 유저 에이전트, 즉 클라이언트는 웹 브라우저가 될 수도 있고, 안드로이드, ios 등 여러가지가 될 수 있다. 서버와 클라이언트가 분리돼있을 때의 장점은 여기서 나타나게 된다. 비즈니스 로직은 서버 개발에서 맡고, 클라이언트의 종류에 따라 UI 처리, api 응답 처리는 클라이언트 쪽에서 담당하면 된다. 즉, 역할을 분배하여 생산성이 높아진다.
Stateful, Stateless
Http는 Stateless하다. Stateless는 무엇이고 Stateful하다는 것은 무엇일까?
Stateful은 상태를 유지한다는 것을 뜻하고, Stateless는 무상태를 뜻한다. 이건 예시를 보면 이해하기 편한데, 강의자료의 예시가 정말 기똥차다. 먼저, Stateful (상태유지) 예시를 보자.
요청: 이 과자는 얼마입니까?
응답: 2,000원입니다.
요청: 3개 구매하겠습니다.
응답: 6,000원입니다. 할인카드 있으신가요?
요청: 멤버십 할인 있습니다.
응답: 10% 할인돼서 5,400원입니다. 감사합니다.
강의 자료 내용을 각색해서 적어보았다. 응답에서 이전에 요청한 상태를 가지고 있기 때문에 대화가 술술 이어지는 것을 확인할 수 있다.
만약 Stateless(무상태) 예시라면 아래와 같이 된다.
요청: 이 과자는 얼마입니까?
응답: 2,000원입니다.
요청: 3개 구매하겠습니다.
응답: ? 무엇을 3개 구매하실 건가요?
요청: 멤버십 할인 있습니다.
응답: ? 아 네 ^^.... (어쩌라는거지?)
응답이 요청으로 받은 상태를 받지 않기 때문에 매 요청을 새로운 요청으로 받아들인다. 현실 세계였다면 이 바보같은 응답을 해고했겠지만, Http 통신에선 오히려 Stateless를 사용하는 이유가 있다. 바로, 응답이 바뀔 때 장애가 발생하지 않게 하기 위해서이다! 만약 응답이 응답A, 응답B, 응답C와 같이 3교대로 진행된다면? 또는, 응답A가 갑자기 몇초만에 사고가 나서 응답B, 또는 응답C로 교체됐다면? Stateful 상태일 때는 상태를 유지하면서 그때그때 필요한 요청만 말하는 것이므로 이 경우에 응답의 입장에선 무얼 원하는지 알 수가 없다. 하지만 Stateless 상태는 받은 응답을 바탕으로 필요정보를 모두 포함해서 요청을 보내주면 응답이 중간에 바뀌어도 통신이 가능해진다.
- Stateless (무상태)
요청: 이 과자는 얼마입니까?
응답: 2,000원입니다.
요청: 이 과자를 3개 구매하겠습니다.
응답: 과자 3개는 6,000원입니다. 할인카드 있으신가요?
요청: 이 과자를 3개 구매하겠습니다. 멤버십 할인 있습니다.
응답: 과자 3개는 6,000원인데, 10% 할인돼서 5,400원입니다. 감사합니다.
Stateless의 장점은 아래와 같다.
- 클라이언트 요청이 증가해도 서버를 대거 투입할 수 있다. stateful은 항상 같은 서버가 유지돼야 하지만, stateless는 서버 교체가 쉽기 때문이다.
- 중간에 특정 서버 장애가 발생해도 대체하기 쉽다.
물론 Stateless에도 한계가 존재한다. 만약 로그인 권한이 필요한 부분까지 Stateless하다면 우리는 매 요청을 할 때마다 새롭게 로그인을 해주어야 할 것이다. 이렇게 일부 최소한의 Stateful 상태유지는 필요하다. 또한, 요청 측에서 데이터를 너무 많이 보내주어야 한다는 단점도 존재한다. 이 과자를 3개 구매하는데, 멤버십 할인이 있고, 기프티콘이 있고, 적립 카드가 있는데 과자 2개에만 적립 적용할 것이고, 신용카드로 6개월 할부로 구매한다는 정보가 전달됐다고 하자. 엄청나게 많은 정보가 한번에 몰려와서 꽤나 당황스럽지 않은가? 이 또한 Stateless의 단점에 해당된다.
비연결성(Connectionless)
HTTP 프로토콜은 TCP 프로토콜을 기반으로 한다. 그런데 TCP는 연결지향성 프로토콜이다. 이쯤되면 의문이 하나 들 것이다. HTTP 프로토콜은 왜 Connectionless하다는 것일까?
바로 TCP 연결을 요청-응답 처리 후에 끊어버리기 때문이다. 이렇게 하면 서버의 자원을 효율적으로 관리할 수 있게 된다. 클라이언트의 요청이 엄청나게 많은데도 Connection이 계속 이어진다고 생각해보자. 엄청난 부하가 걸리게 될 것이다.
물론, 비연결성으로 진행했을 때 한계도 존재한다. 매번 TCP/IP 연결을 새로 맺어야하므로 3 way handshake 시간이 그만큼 추가된다. 또, 요즘은 이미지파일, js 파일 등 굉장히 많은 파일들이 존재하기 때문에 매번 연결을 끊고 다시 연결하는 경우엔 시간이 더더욱 오래 걸린다. 이러한 단점을 보완하기 위한 HTTP 지속 연결이 생겨나게 됐다! 기존 HTML 연결은 특정 파일을 받고 연결을 바로 끊었다가 재연결하면서 시간이 소요됐다면, 지속연결은 모든 파일을 요청하자마자 바로 응답해주고, 완전히 처리가 완료됐으면 연결을 끊는 것이다.
HTTP 메시지
HTTP 메시지 구조를 살펴보자.
메시지 구조의 공통점으로 start-line, header, body로 이루어져있다.
우리가 e2e test로 api 호출 테스트를 진행할 때, assertJ 문법으로 statuscode가 200인지, Http method가 GET, POST 중에 무엇인지, header가 비어있는지, body에 객체가 잘 담겨있는지 확인해주는 것을 생각하면 더 편하게 다가올 것이다. header와 body 사이에는 공백라인 (CRLF)가 존재한다. body는 생략될 수 있다.
시작 라인에는 아래 정보가 들어가있다.
- 요청: HTTP method, 요청대상(절대경로[?쿼리]), Http Version
- 응답: Http Version, Http StatusCode, 이유문구
헤더에는 아래 정보가 들어가있다.
- 요청: URI
- 응답: Content-Type, Content-Length
HTTP 전송에 필요한 부가정보 (html, css, xml인지, body의 크기라든지, 캐시 정보라든지, 압축 여부 등)들이 헤더에 들어가있다고 한다.
바디에는 실제 전송할 데이터들이 들어가있다.
2. HTTP 메서드
HTTP API
좋은 API 설계는 어떻게 해야 할까? '/read-member-by-id', 'create-member' 과 같이 길지만 명확한 API를 좋다고 할 수 있을까? 지금은 기능이 적기 때문에 간단하지만, 수많은 기능들을 관리해야 한다면 URI가 지나치게 길어져 가독성이 떨어지고 유지보수에도 어려움을 겪게될 것이다.
API 설계에서 가장 중요한 것은 리소스 식별이다. 회원 관리에서 가장 중요한 리소스는 '회원'이다. 따라서 '/member' URI와 매핑시켜주면 된다.
그런데, 리소스 중심으로 짜다보면 중복되는 URI가 존재하게 된다. '/member', '/members'와 같이 복수형으로 구분해준다 해도 회원 조회 기능, 회원 등록 기능, 회원 수정 기능 등등. 수많은 기능들에서 중복된 URI가 발생하게 된다. 이렇게 리소스가 같지만 행위가 다른 기능들을 분리해줄 때엔 HTTP Method를 이용해주면 된다. 자주 쓰이는 GET, POST, PUT, PATCH, DELETE 외에도 HEAD, OPTIONS 등이 존재하긴 하지만, 사실상 앞의 5개를 거의 많이 쓴다고 한다.
참고로 '/member/{id}'와 같이 path에 id값을 넣어줘도 가능하고, '/member?id=20'과 같이 query에 id값을 넣어줘도 가능은 하다. 하지만 보통 계층정보로 된 구조나 리소스 식별자는 path에, 검색조건과 같은 비계층정보는 query에 넣어주기 때문에 '/member/{id}' 구조의 URI가 더 범용적으로 많이 쓰이는 것이다.
GET
- 리소스 조회
- 서버에 전달하고 싶은 데이터는 body에 담기보다는 쿼리 (@PathVariable, @RequestParam 등) 를 통해서 전달.
body 에 데이터를 담는 것을 지원하지 않는 브라우저가 많다고 한다.
POST
- 요청 데이터 처리 (주로 리소스 등록에 사용)
- 서버에 전달하고 싶은 데이터는 body에 담아줌
- 다른 메서드로 쓰이기 애매한 경우
신규 리소스 등록에 자주 쓰이는 method이다보니 응답 status code가 201 (Created)일 때가 많다. 이 때, 응답의 헤더 Location에는 /members/100 과 같이 식별자 path가 담기게 된다.
POST는 단순 등록에 해당되는 메서드가 아니다. 입력받은 데이터에 따른 프로세스 처리를 할 때엔 POST 메서드가 사용된다. 회원가입과 같은 등록에도 POST가 쓰이지만, 게시판 글쓰기/댓글쓰기, 기존 자원에 데이터 추가하는 것들도 POST를 사용하기에 적절한 행위들이다. 이처럼 POST일 때 데이터를 어떻게 쓸 지는 정답이 정해져 있지 않다. 리소스마다 적절하게 정해주도록 해야 한다.
POST의 결과로 새로운 리소스가 생성되지 않을 수도 있다. 이러한 경우에 리소스 자체만으로 구성돼있지는 않은 /members/start-study 와 같이 컨트롤 URI가 쓰이기도 한다. 실제로 실무에서는 완전히 리소스만으로 구성된 URI만을 사용하긴 어렵다고 한다. 수많은 URI들이 있고, 실제로 새로운 리소스가 생성되지 않는 기능들도 굉장히 많기 때문이다.
또한, 단순 변경이라 생각하여 PUT이나 PATCH를 사용했지만, 실제로는 수많은 프로세스의 변경이 일어났다면 POST가 적절하다. 예를 들어 주문상태를 '대기' -> '배달시작'으로 변경할 때, 주문상태뿐 아니라 배달기사님을 호출하고, 음식조리를 완료처리로 변경하고, 주문서를 변경하는 여러 작업이 들어갈 땐 POST가 적절할 수 있다.
PUT
- 리소스를 완전히 대체, 해당 리소스가 없으면 생성 (덮어쓰기)
클라이언트가 리소스 위치를 알고 있을 때 보통 PUT을 사용한다. 예를 들어 '/members' 인 경우, 리소스는 알지만 리소스 식별자 (리소스 위치)는 모른다. 이러한 경우는 POST 또는 GET일 확률이 높다. 그러나 '/members/1'에 기능을 하는 URI라면 PUT이 사용될 확률이 높다. 해당 리소스를 수정하거나 대체하거나, 없으면 생성해주는 작업을 해줄 수 있기 때문이다.
PATCH
- 리소스 부분 변경
PUT과 다른 점이 바로 '부분 변경' 이다. 해당 리소스를 완전히 대체하는 것은 PUT, 부분 변경은 PATCH이다. 즉, 리소스를 대체하거나 갈아끼우는 것은 PUT, 리소스 수정은 PATCH라 생각하면 된다.
예를 들어, {name: kth990303, age: 23} 의 데이터가 존재한다고 해보자. 이 때, {name: kth990303, age: 24}로 수정하길 원한다면 Http Method는 PATCH가 적절하다고 볼 수 있는 것이다. PATCH가 최근에 등장했기 때문에 PATCH를 지원하지 않는 브라우저가 일부 존재한다고 한다. 이 때엔 PATCH 대신 POST를 사용해주면 된다.
DELETE
- 리소스 삭제
주로 삭제를 하기 때문에 응답 Http Status code로 204 (No Content)를 많이 보내준다. 실제로 @DeleteMapping을 사용한 URI에서 ResponseEntity<Void> 형으로 응답객체를 보내주곤 했다.
HTTP 메서드의 속성
- 안전: 호출해도 리소스를 변경하지 않는다. (GET)
- 멱등: 여러번 호출해도 동일한 결과를 보장한다.(GET, PUT, DELETE)
- 캐시가능: 응답 결과 리소스를 캐싱해서 사용해도 된다. (GET, POST, PATCH)
PUT은 리소스를 대체한다. 덮어쓰는 행위이기 때문에 멱등성이 보장된다.
PATCH는 리소스를 수정한다. 만약 해당 리소스의 int값에 +=2를 하는 행위를 여러 번 한다면 결과는 매번 달라진다. 따라서 PATCH는 멱등성이 보장되지 않는다. 이는 POST도 마찬가지이다.
서버가 렉이 걸려서 순간적으로 같은 행위가 여러번 요청되고 응답됐을 때, GET, PUT, DELETE는 문제가 되지 않는다. 사용자가 A라는 값을 지우려고 했기 때문에, A를 지우는 행위를 여러 번 입력한다고 해서 문제되지 않는다. 하지만 만약 사용자가 자신의 돈을 3000원 기부하는 행위를 순간적인 렉으로 1000번 반복요청됐다면? 사용자의 돈 3,000,000원이 순식간에 증발돼버린다. 멱등성이 보장되지 않은 행위는 조심할 필요가 있다. 이렇게 여러번 요청됐을 때 문제가 되는 행위를 기준으로 생각해보는 것도 좋다.
실제로는 GET 관련에서만 캐싱을 사용한다고 한다. POST, PATCH는 응답 바디까지 캐싱 키로 고려를 해야 하는데, 이 과정이 쉽지 않다고 한다. 실제로 GET은 리소스 조회에 해당되므로 캐싱된 VO를 정적 팩토리 메서드로 가져오기 쉬우나, POST나 PATCH는 캐싱값 이외에 다른 값들이 존재할 확률이 높은 것도 한몫할 듯하다.
예전에 공부했을 때엔 크게 와닿지 않았는데, 확실히 우아한테크코스에서 스프링으로 API 설계를 연습하다보니 그만큼 많이 와닿는 듯하다. 앞으로 더 많은 경험을 쌓고 다시 보게 된다면 또 그 때는 다르게 느낄 것이라 생각하니 설렌다.
이론과 실전 병행의 중요성을 깨닫게 된다.
'CS > Http, Network' 카테고리의 다른 글
[매트 스터디] 5주차 HTTP 헤더2 - 캐시와 조건부 요청 (0) | 2022.06.01 |
---|---|
[매트 스터디] 4주차 HTTP 헤더 1 - 일반 헤더 (0) | 2022.05.22 |
[매트 스터디] 3주차 HTTP 메서드 활용 & HTTP 상태코드 (2) | 2022.05.15 |
[매트 스터디] 1주차 인터넷 네트워크 & URI와 웹 브라우저 요청 흐름 (2) | 2022.04.30 |
[Network] 인터넷 프로토콜 요약본 (0) | 2021.04.25 |