메서드와 상태코드 — 웹은 약속으로 굴러간다
Article
2강 끝에서 이렇게 말했다. 지금 우리 서버는 없는 책을 달라고 해도 200 OK 라고 답한다.
정말 그런지 확인해보자. 2강의 에코 서버를 켜두고, 메서드만 바꿔가며 같은 경로로 요청을 보낸다.
curl -X GET http://localhost:8080/books/1
curl -X POST http://localhost:8080/books/1
curl -X PUT http://localhost:8080/books/1
curl -X DELETE http://localhost:8080/books/1
curl -X PATCH http://localhost:8080/books/1
- GET /books/1 → 응답 200
- POST /books/1 → 응답 200
- PUT /books/1 → 응답 200
- DELETE /books/1 → 응답 200
- PATCH /books/1 → 응답 200
- GET /books/99999 → 응답 200
마지막 줄을 보라. 존재하지도 않는 99999번 책을 달라고 했는데 **"성공했다"**고 답했다.
우리 서버는 거짓말쟁이는 아니다. 애초에 아무 약속도 안 했으니 어길 것도 없다. 다만 아무도 이 서버와 대화하고 싶어 하지 않을 것이다. 무엇을 물어봐도 똑같은 대답이 오는 상대와는 일할 수 없다.
이번 강의는 그 약속을 배운다. 문법이 아니라 관습이다. 어겨도 컴파일 에러는 안 난다. 대신 아무도 안 쓴다.
경로는 명사, 메서드는 동사
/books/1 은 무엇을 가리킨다. 1번 책이다. 이걸 자원이라고 부른다.
그런데 그 책을 어쩌겠다는 건지는 안 적혀 있다. 가져가겠다는 건지, 지우겠다는 건지, 고치겠다는 건지.
그 동사가 메서드다.
잘못된 설계
동사를 경로에 넣는다
GET /getBook?id=1GET /deleteBook?id=1POST /createBookPOST /updateBookPrice- 주소마다 동사를 새로 지어내야 한다
약속을 지킨 설계
동사는 메서드가 맡는다
GET /books/1DELETE /books/1POST /booksPUT /books/1- 주소는 명사 하나면 된다
오른쪽처럼 짜면 /books 라는 주소 하나와 메서드 네 개로 책에 관한 모든 일을 할 수 있다. 이게 REST 라고 부르는 방식의 뼈대다.
| 메서드 | 하려는 일 | 본문 | 예시 |
|---|---|---|---|
GET | 가져간다 | 없다 | GET /books · GET /books/1 |
POST | 새로 만든다 | 있다 | POST /books |
PUT | 통째로 바꾼다 | 있다 | PUT /books/1 |
DELETE | 지운다 | 없다 | DELETE /books/1 |
새로 만들 때만 /books 이고 나머지는 /books/1 인 것에 주목할 것. 아직 만들지 않은 책에는 번호가 없기 때문이다. 번호는 서버가 붙여준다. 이 이야기는 15강에서 크게 다룬다.
두 번 보내면 어떻게 되나
메서드를 구분하는 진짜 이유가 여기 있다. 말로 하면 와닿지 않으니 직접 해봤다.
POST 는 책을 하나 늘리고, PUT 은 첫 칸을 덮어쓰고, DELETE 는 첫 칸을 지우는 서버를 만들었다. 그리고 같은 요청을 두 번씩 보냈다.
- POST 한 번 → 책 1권
- POST 두 번 → 책 2권
- PUT 한 번 → 책 1권
- PUT 두 번 → 책 1권
- POST 뒤 DELETE 한 번 → 책 0권
- POST 뒤 DELETE 두 번 → 책 0권
- GET 세 번 → 책 0권
POST 만 결과가 달라졌다. 두 번 보내면 책이 두 권이 된다.
PUT 은 몇 번을 보내도 한 권이다. 같은 자리를 덮어쓰기 때문이다. DELETE 도 마찬가지다. 이미 지워진 것을 또 지우라고 해도 결과는 똑같이 "없음"이다.
몇 번을 보내도 결과가 같은 성질을 멱등하다고 한다.
| 메서드 | 서버를 바꾸나 | 두 번 보내도 같은가 | 부르는 이름 |
|---|---|---|---|
GET | 안 바꾼다 | 같다 | 안전하다 · 멱등하다 |
PUT | 바꾼다 | 같다 | 멱등하다 |
DELETE | 바꾼다 | 같다 | 멱등하다 |
POST | 바꾼다 | 다르다 | 둘 다 아니다 |
이게 왜 중요한가. 네트워크는 자주 끊기기 때문이다.
요청을 보냈는데 응답이 안 온다. 서버가 못 받은 걸까, 받아서 처리했는데 응답만 유실된 걸까. 알 수가 없다. 이때 그냥 다시 보내도 되는 요청과, 다시 보내면 큰일 나는 요청이 갈린다.
상태코드는 앞자리로 읽는다
이제 응답 차례다. 2강에서 본 상태줄을 다시 보자.
HTTP/1.1 200 OK
200 이 상태코드다. 외울 필요 없다. 앞자리 하나만 알면 절반은 끝난다.
| 앞자리 | 뜻 | 누구 잘못인가 |
|---|---|---|
2xx | 잘 됐다 | — |
3xx | 다른 데로 가봐라 | — |
4xx | 요청한 쪽이 잘못했다 | 클라이언트 |
5xx | 서버가 잘못했다 | 서버 |
이 시리즈에서 실제로 쓸 것은 여섯 개뿐이다.
| 코드 | 이름 | 언제 | 어디서 배우나 |
|---|---|---|---|
200 | OK | 잘 가져왔다 · 잘 고쳤다 | 9강 |
201 | Created | 새로 만들었다 | 13강 |
204 | No Content | 잘 지웠다. 돌려줄 게 없다 | 13강 |
400 | Bad Request | 네가 보낸 값이 이상하다 | 20강 |
404 | Not Found | 그런 것 없다 | 21강 |
500 | Internal Server Error | 내 코드가 터졌다 | 21강 |
상태코드를 바꿔가며 던져봤다
경로에 적은 숫자를 그대로 상태코드로 돌려주는 서버를 만들어서, curl 이 어떻게 반응하는지 봤다.
curl http://localhost:8080/404
- /200 → 종료코드 0 · 본문 [상태코드 200 로 답했다]
- /201 → 종료코드 0 · 본문 [상태코드 201 로 답했다]
- /400 → 종료코드 0 · 본문 [상태코드 400 로 답했다]
- /404 → 종료코드 0 · 본문 [상태코드 404 로 답했다]
- /500 → 종료코드 0 · 본문 [상태코드 500 로 답했다]
404 를 받았는데도 curl 은 아무렇지 않게 종료코드 0(성공)으로 끝났다. 상태코드는 HTTP 의 약속일 뿐, 프로그램의 성공·실패와는 다른 층위다.
-f 를 붙이면 그제서야 4xx·5xx 를 실패로 취급한다.
curl -f http://localhost:8080/404
- /200 → 종료코드 0
- /201 → 종료코드 0
- /400 → 종료코드 22 · curl: (22) The requested URL returned error: 400
- /404 → 종료코드 22 · curl: (22) The requested URL returned error: 404
- /500 → 종료코드 22 · curl: (22) The requested URL returned error: 500
옆에 적힌 영어는 아무래도 좋다
200 OK 의 OK, 404 Not Found 의 Not Found. 이걸 이유 문구라고 한다.
이 문구는 사람이 읽으라고 있는 것이고, 아무렇게나 적어도 된다. 정말 그런지 확인하려고 404 옆에 엉뚱한 말을 적어 보냈다.
- HTTP/1.1 404 Yeah Nah Mate
- Content-Type: text/plain; charset=utf-8
- Content-Length: 30
그리고 curl 에게 상태코드만 물어봤다.
curl -o /dev/null -w '%{http_code}' http://localhost:8080/nope
- 404
curl 은 눈 하나 깜짝 안 하고 404 로 알아들었다. 계약은 숫자에 있다. 옆의 글자는 장식이다.
만들었을 때와 지웠을 때
200 말고 두 개를 더 보자. 둘 다 13강에서 실제로 쓴다.
201 Created 는 새로 만들었을 때다. 그냥 "됐다"가 아니라 **"만들었고, 그건 여기 있다"**까지 알려준다.
- HTTP/1.1 201 Created
- Content-Type: text/plain; charset=utf-8
- Location: /books/1
- Content-Length: 30
Location 헤더에 새로 생긴 것의 주소가 적혀 있다. 방금 만든 책이 몇 번인지 클라이언트는 몰랐는데, 이걸로 알게 된다.
204 No Content 는 잘 됐지만 돌려줄 내용이 없을 때다. 지웠을 때가 그렇다.
- HTTP/1.1 204 No Content
- Content-Type: text/plain; charset=utf-8
헤더 뒤로 아무것도 없다. 본문을 받아서 길이를 세어보면 정말 0 바이트다. 지운 책을 돌려줄 수는 없으니 당연하다.
그래서 REST 란
지금까지의 이야기를 한 줄로 묶으면 이렇다.
- 경로무엇에 대해 — /books/1
- 메서드무엇을 하려고 — DELETE
- 상태코드어떻게 됐는지 — 204
REST 는 규격이 아니라 스타일이다. 강제하는 컴파일러가 없다. GET /deleteBook?id=1 이라고 써도 잘 돌아간다.
다만 그렇게 쓰면, 그 API 를 쓰는 사람마다 문서를 읽어야 한다. 약속을 지키면 처음 보는 주소도 추측할 수 있다. DELETE /books/3 이 무슨 뜻인지 아무도 설명해주지 않아도 안다.
정리하면
경로는 명사, 메서드는 동사, 상태코드는 결과다. 셋 다 약속이고, 어겨도 프로그램은 돌아간다.
- 01
동사를 주소에 넣지 않는다
/books/1하나에 네 메서드를 걸면getBook·deleteBook을 지어낼 필요가 없다. - 02
POST 만 멱등하지 않다
두 번 보내면 두 개가 생긴다. PUT · DELETE · GET 은 몇 번을 보내도 결과가 같다. 네트워크가 끊겼을 때 재시도해도 되는지가 여기서 갈린다.
- 03
상태코드는 앞자리로 읽는다
2xx 성공 · 4xx 요청한 쪽 잘못 · 5xx 서버 잘못. 옆에 적힌 영어는 장식이라 마음대로 적어도 curl 은 숫자만 본다.
- 04
200 에 에러를 담지 않는다
없으면 404 다. 지금 우리 서버는 없는 책도 200 으로 답한다.
- 05
만들었으면 201 과 Location, 지웠으면 204
새로 생긴 것의 주소를 알려주고, 지운 것은 돌려줄 내용이 없다.
HTTP/1.1 201 Created Location: /books/1
여기까지가 HTTP 다. 1강에서 서버가 죽지 않는 이유를, 2강에서 오가는 글자의 생김새를, 3강에서 그 글자에 담긴 약속을 봤다.
이제 이 약속을 손으로 지키려면 어떻게 해야 하는지 상상해보자. 시작줄을 잘라 메서드와 경로를 꺼내고, 경로를 조각내 숫자를 뽑고, 메서드마다 if 를 세우고, 상태코드에 맞춰 상태줄을 조립하고, Content-Length 를 세고…
아무도 그렇게 하지 않는다. 다음 강의에서 start.spring.io 에 들어가 프로젝트를 하나 내려받는다. 그리고 지금까지 손으로 짠 것이 전부 사라지는 것을 본다.