RESTful
탄생배경은 간단하게, 개념은 자세하게 보기
1. 웹의 확장성을 위한 고민
1990년대 말 이후 급속도로 웹이 성장했다. 그에 따라 TCP의 연결수가 한계에 달했고 서버당 메모리도 부족하고..
버전 호환성도 난립했다고 한다.
1994년 : Mosaic, Netscape 등장(정적 HTML 중심) / 페이지뷰 일일 10k ~ 100k
1996년 : CGI/Perl로 동적 페이지 시작, 병목 - > 프로세스 폭발 ? / 일일 1M+
1999 : 포털 및 전자상거래 증가 및 세션, 쿠키, 로드밸런서 도입 / 일일 10M+
CGI & 초기 RPC 방식의 한계
1996년 등장한 CGI(Common Gateway Interface)의 한계
- 요청마다 새로운 OS 프로세스 생성 : 요청이 많아질수록 OS레벨에서 프로세스가 쌓이게 되어 CPU 리소스를 과도하게 소모하고
컨텍스트 스위칭?? 비용이 급증한다고 한다.
- 상태(State) 서버 세션에 저장함 : 로그인 정보나 장바구니 상태 등을 서버측 세션(서버에다가)에 저장하니까,
요청이 항상 동일한 서버로 가야하는 문제가 발생함. - > Sticky - Session??이 필요하게 되었다고 한다.
- 강결합 API 구조 : 클라이언트와 서버가 서로 긴밀하게 결합되어 있어서 서버측 코드를 변경하면 클라이언트쪽도 함께 수정해야했다. 또한 명확한 버전 관리 체계도 없었다고 한다. (결합도는 낮게, 응집도는 높게) XML기반과 HTML기반이 섞이기도 했다고..
실제로 네이버 뉴스 댓글 사용자가 증가하자 CGI 기반 댓글 시스템이 느려지기 시작했고,
결국 Node 수평 확장 구조로 전환되면서 무상태 설계를 도입했다고 한다. 그랬구나..
확장성(Scalability)에 대해서
- Vertical Scaling(수직 확장) : 한 대의 서버에 CPU, 메모리 등 자원을 추가하여 성능을 높인다.
장점 : 구조의 변경 없이 빠르게 대응할 수 있다.
단점 : 비용이 매~우 높고 물리적인 한계가 존재한다. 64코어, 128코어짜리 CPU는 정말.. 정말 비쌀것 같다..
- Horizontal Scaling(수평 확장) : 서버 수를 늘려서 로드를 분산하는 방식이다. (웹 서비스의 표준이라고 함)
장점 : 트래픽이 증가하면 노드를 추가하는 것만으로 대응할 수 있음
단점 : 세션/상태 유지 전략이 필요하다. 이것을 REST가 해법을 제시해준다고 한다.
Amazon은 트래픽 증가에 대응하기 위해 EC2서버를 무한으로 확장하는 구조로 전환했다고 한다.
이를 위해 상태 저장 방식에서 벗어나 RESTful 구조를 채택했다고 함.
* 레거시 시스템에서 세션복제 + 로드밸런서 고정(URL sticky) 문제로 수평확장 장애가 자주 발생한다고 한다.
- 세션복제 + 로드밸런서 고정?
웹 서비스가 수평확장되면 여러대의 웹서버가 요청을 나눠받게 된다. 이 때 로그인 등 사용자의 상태정보를 서버 메모리에 저장하면
1) 세션복제(Session Replication) 문제
각 서버가 사용자의 로그인 정보나 장바구니 상태를 따로따로 저장하고 있다면, 사용자가 매 요청마다 다른 서버에 접속할 경우
로그아웃되거나 상태가 초기화되는 문제가 발생한다고 한다.
이를 해결하기 위해 모든 서버간 세션을 동기화(복제)해야 하지만, 서버가 많아지면 네트워크 트래픽과 지연이 증가한다고 한다.
2) 로드밸런서 고정(URL Sticky Session) 문제
세션을 동기화하는 대신 로드밸런서가 특정 사용자의 요청을 항상 같은 서버로만 보내도록 고정하는 방식이라고 한다.
단기적으로는 안정적이지만 특정 서버에 요청이 몰리거나 다운되면 사용자 세션이 유실된다고 한다.
또한 로드밸런서가 상태를 기억해야하기 때문에 무상태(Stateless) 구조가 깨지고, 장애 복원이나 서버 증설이 어려워진다고 함.
* 무상태(Stateless)가 왜 중요한지는 초기 CGI 병목역사를 보면 이해 가능하다고 한다.
* REST 구조는 서버 수평 확장을 가능하게 하는 설계 원칙으로, 실무에 매우매우 중요하다고 함.
2. 아무튼 위의 복잡한 문제들에 대해 Roy Fielding이 이의를 제기했다고 해야하나.. 아무튼 이 분덕에 REST가 탄생하게 되었다.
REST를 제안한 배경
1) 웹이 왜 이렇게 빨리 퍼지게 되었는가?
2) 어떤 아키텍처 제약이 웹의 확장성을 담보했는가? << 뭔소린지 모르겠다..
3) 아키텍처? 에 대한 원리를 패턴(Style)으로 일반화할 수 없을까?
이러한 고민끝에 웹의 성공비결을 아키텍처 스타일의 제약조건으로 정리했고, 이게 REST라고 한다.
REST 6대 제약조건
| 제약조건 | 설명 | 이걸 하면 뭐가 좋은가? |
| Client-Server | 클라이언트(UI)와 서버(데이터, 로직)을 분리하여 독립적으로 개발, 배포가능 |
사용자 인터페이스와 서비스 로직의 진화 독립성을 확보한다고 한다. |
| Stateless (무상태성) |
서버가 요청자의 상태를 저장하지 않는다. 매 요청마다 필요한 정보를 클라이언트가 함께 전달한다고 함. |
서버 확장성 향상, 요청 처리에 대해 간섭이 없음, 장애 복원성이 UP! |
| Cacheable | 서버의 응답에 캐시 가능 여부를 명시함 - > 클라이언트/중간 노드가 캐싱하여 불필요한 트래픽 방지 |
성능 향상, 네트워크 부담 감소 |
| Uniform InterFace | URI, HTTP 메서드등 일관된 방식으로 리소스 접근 및 조작 - > 규약이 명확하다 |
느슨한 결합구조, API 이해 및 유지보수가 쉬움 |
| Layered System | 클라이언트는 중간계층(프록시, 로드밸런서 등)이 있는지 모르는 채(몰라야한다)로 전송이 가능하다. |
보안 강화, 트래픽 관리, 인프라 유연성 확보 |
| Code-On-Demand (Optional, 선택?) |
서버가 클라이언트에게 스크립트(JS 등)를 전달하여 동적 기능을 수행하도록 허용한다. |
클라이언트의 기능 확장(사용은 선택적) |
* 무상태성(Stateless)에 대해서
내가 매일 똑같은 커피숍에 가서 똑같은 주문을 했다고 하자.
그럼 보통 종업원은 사람이기 때문에 그래도 내가 누군지 얼굴을 기억할 것이고 내가 어떤 주문만 했는지 알 것이다.
이걸 매~일 반복하다보면 종업원은 내가 눈에 잡힌다면 커피숍에 가서 주문하기도 전에 똑같은 음료를 준비해놓을 수도 있다.
그런데 만약 오늘은 기분이 영 좋지도 않고 새로운 시도도 해보고 싶어서 색다른 음료를 주문했다고 하자.
그 때 종업원은 마침 내가 길거리에서 커피숍으로 오는걸 눈치채고 여유롭게 내가 여태껏 주문해왔던 음료를 만들어둔 후에
"어서오세요! 오늘도 카푸치노에 시나몬 뺀거 맞으시죠?" 라고 한다면?
아.. 나는 물론 "네 부탁드립니다." 라고 하겠지만 뭔가 잘못된 상황이라는 것을 느끼고 있을 것이다.
이렇게 종업원이 내가 어떤 주문을 할지 "기억하고 있다면" 무상태성(Stateless)이 깨졌다고 말한다.
정리한다면 무상태성(Stateless)을 유지하고 있다는 의미는 클라이언트가 언제, 어떤 요청을 하던 서버쪽에선 그 요청하는 요청자의 상태를 전~혀 저장하지 않고 주어지는 요청에 대해 항상 일관된 응답을 한다는 것을 의미한다.. 고 생각하면 편할 것 같다.
Uniform Interface의 4가지 규칙
REST 6대 제약조건에 있던 그 Uniform Interface
1) Resource Indenfication in URIs(URI에서 리소스를 식별하기)
- 모든 리소스는 고유한 URI로 식별해야한다고 함(/users/1는 사용자의 ID가 1번인 리소스를 의미함)
2) Manipulation via Representations(표현을 통한 동작?)
- 리소스는 JSON, XML같은 표현(Representation) 형태로 전달된다.
- 클라이언트는 이 표현을 수정해서 다시 서버로 보낸다 - > PUT, PATCH, GET, DELETE, POST 등등..
3) Self-descriptive Message(자기 설명적 메시지?)
- 요청/응답 메시지는 자체적으로 설명력을 가져야 한다.(헤더, 상태코드, 콘텐츠타입 포함..)
- ex) Content-Type : application/json, 200 OK, 404 Not Found 등등...
4) Hypermedia as the Engine of Application State(HATEOAS, 애플리케이션 상태 엔진으로서의 하이퍼미디어?)
- 클라이언트가 서버로부터 받은 응답 내 링크(Hyperlink)를 통해 다음 행동을 결정한다.
- ex) /users/1을 요청하면 "edit" : "/users/1/edit"을 제공함..
* RESTful은 RESTful = CRUD 가 아니라, 6대 제약조건을 얼마나 만족했는지가 핵심이다.
* HATEOAS(Uniform Interface의 4가지 규칙 중 하나)는 대부분 실무에서 생략되지만,
버전리스(버전이 없는?) API를 원할 때 강력한 무기가 된다고 한다.
3. HTTP와 웹의 성공 요인 3가지 분석
단순성(Simplicity) : HTTP는 너무 단순해서 가장 많이 사용되는 프로토콜이 되었다.
- 단 한줄로 리소스를 요청할 수 있음
- 텍스트 기반 프로토콜로서 디버깅, 학습이 쉽다(브라우저에서 바로 테스트할 수 있다)
- 복잡한 바이너리 포맷이 없기 때문에 비개발자도 이해하기 쉬운 구조임.. 이거 진짜 맞는말이다 난 아직도 바이너리를 이해 못했다
- 개발 진입장벽이 낮고, 모든 플랫폼에서 쉽게 구현 및 테스트가 가능함
GET /articles/123 HTTP/1.1 // 단 한줄
Host: example.com
느슨한 결합(Loose Coupling)
- Self-descriptive Message
- HTTP 메시지 하나만으로도 요청의 의도, 구조를 완전하게 설명할 수 있다.
요청 메서드(GET), 경로(/users/1), 헤더(Content-Type), 본문(JSON 등)
POST /users HTTP/1.1 // POST니까 생성, 유저를 생성함
Content-Type: application/json // json 타입?
{
"name": "Alice",
"email": "alice@example.com"
}
- 하물며 내가 봐도 무엇을 요청했는지 명확하게 알 수 있다!
- 서버뿐만 아니라 중간 프록시, 캐시서버, API 게이트웨이도 이해하고 처리할 수 있다.
- 하위 호환성
- HTTP 구조는 수십년간 거의 바뀌지 않았음에도 잘 작동한다.
- GET/POST/PUT/DELETE/PATCH 등은 서버 버전이 달라도 같은 의미로 잘 작동한다.
- 덕분에 API가 수명주기 동안에도 느슨한 결합으로 확장이 가능하다.
캐시(Cache) : HTTP는 태생부터 캐시를 고려한 설계를 따른다.
| 계층 | 캐시 주체 | 예시 | 효과? |
| 브라우저 | 클라이언트 | HTML, JS, 이미지 | 사용자 체감 속도가 올라감 |
| CDN | 중간 캐시 계층 | 정적 자원, API GET 응답 | 서버부하가 내려감, 글로벌 응답속도가 올라감 |
| 프록시 서버 | 조직 내부/ISP | 문서, 이미지, 공통 API | 대역폭 절감, 병목 지점 완화 |
* 캐시 전략 예시
- E-Tag + If-None-Match 헤더로 리소스 변경 여부를 빠르게 확인함
- 응답이 바뀌지 않았을 경우, 304 에러로 대역폭을 절감할 수 있음(예외처리?)
GET /products/1 HTTP/1.1
If-None-Match: "v1-product"
//
HTTP/1.1 304 Not Modified
- Cache-Control(캐시 컨트롤) : max-age=60, must-revalidate - > 클라이언트가 60초동안 재요청없이 캐시를 사용함
뭔소린지 모르겠다..
예시코드
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(
@PathVariable Long id,
@RequestHeader(value="If-None-Match", required=false) String ifNoneMatchHeader) {
// 상품조회
Product product = productService.findById(id);
if(product == null) {
return ResponseEntity.notFound().build();
}
// ETag 생성(ex. 수정시간 기반으로 생성?)
String eTag = "\"" + product.getUpdatedAt().toEpochSecond(ZoneOffset.UTC) + "\"";
// 클라이언트 ETag와 비교함
if(eTag.equals(ifNoneMatchHeader)) { // ifNoneMatchHeader는 @RequestHeader에 있는 인자임
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(eTag) // eTag 메서드 안에 위에 ETag를 생성한 eTag를 넣나봄
.build();
}
// if문으로 빠져나가지 않는다면
// 새로운 ETag와 함께 정상 응답을 반환한다
return ResponseEntity.ok()
.eTag(eTag)
.cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).mustRevalidate())
.body(product);
} // end getProduct
}
* 정적 파일뿐만 아니라 REST API의 응답에도 캐시전략을 적용할 수 있다.
* 불변 리소스(프로필 이미지, 정책문서 등등..)는 max-age를 길게 설정한다.
* 자주 변경되는 리소스(알림, 실시간 목록 등...)들은 no-cache, ETag 조합을 사용한다.
정리
REST의 6대 제약조건
: Client-Server / Stateless / Cacheable / Uniform Interface / Layered System / Code-on-Demand(옵션)
REST는 단순 CRUD가 아니라 아키텍처 스타일 자체임
HTTP의 성공비결은 단순한 URI, 캐시, 느슨한 결합 - > API 설계의 가장 중요한 구성요소
'Spring Boot' 카테고리의 다른 글
| RESTful API 기본 : @RestController (6) | 2025.07.18 |
|---|---|
| REST : 제약조건 (0) | 2025.07.10 |
| MVC : Spring Boot 시작하기 (0) | 2025.07.06 |
| Spring MVC : 요청 처리 흐름 (8) | 2025.07.06 |
| Spring MVC의 컴포넌트 (0) | 2025.07.02 |