RESTful
의 주요 제약조건
RESTful~ 하다의 6대 제약조건
Client-Server / Stateless / Cacheable / Uniform Interface / Layered System / Code - On - Demand(optional)
1. Client - Server
클라이언트님의 서버가 아니고 Client와 Server의 역할을 분리하는거다 멍청아
클라이언트는 UI(유저 인터페이스)를 담당하고, 서버는 데이터 처리와 저장소(비즈니스 로직, DB)를 담당한다.
클라이언트와 서버가 관심사(Concern)를 분리해서 독립적 개발과 배포가 가능해지고, 유지보수도 수월해진다.
architecture\client(React, Flutter) | architecture\server(Spring Boot API)
뭐.. 이런식으로 폴더를 분리해서 개발하면 된다고 한다.
클라이언트는 서버 내부 구조를 몰라도 되는것이고? 서버 또한 클라이언트의 구조에 대해 관여하지 않아도 된다.
변화에 따른 코드 유지보스의 영향 예시
| 변화 | 클라이언트 영향 | 서버 영향 |
| UI 리뉴얼, UI를 바꿔주면 | API만 유지되면 영향 없음(있는것같음) | 없음 |
| DB 구조 변경 | 없음 | 내부 로직만 수정하면됨(있음) |
클라이언트측 예시코드(JavaScript - Axios) **** 이게 뭐냐?
axios.get("/api/v1/products/1") // axios가 뭐고..
.then(res => console.log(res.data));
서버측 예시코드(Java + Spring Boot)
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
GetMapping("/{id}")
public ProductDto find(@PathVariable Long id) {
return ProductService.find(id);
}
}
이걸 보니 마음이 편안해진다..
프론트엔드는 API를 호출하기만 한다.(axios.get("/api/v1/products/1"))
서버는 내 부로직으로부터 DTO를 생성해서 응답하기만 한다. (ProductDto 타입의 리턴값 생성)
개발구조 예시
project\frontend\src
project\frontend\package.json // 이 2개가 프론트
project\backend\src\main\java
project\backend\build\gradle // 이 2개가 백
* 백엔드와 프론트엔드가 각자의 개발흐름에 따라서 배포할 수 있도록 분리하는것이 핵심이다.
* Swagger, Postman을 통해 클라이언트-서버간 계약?(API 명세)을 미리 확정하고 개발을 병렬로 진행해야한다!
* 팀 규모가 커질수록 API 게이트웨이 또는 BFF(Backend for Frontend) 패턴을 함께 고려해야한다..
2. Stateless(무상태성) : 수평확장
모든 요청은 독립적이다 : 서버는 이정 요청 상태(Session)를 저장하지 않는다.
필요한 상태는 언제나 클라이언트가 매 요청마다 전달한다.(사용자 ID, 요청내용, 등등..)
서버는 매번 새로운 요청처럼 처리하므로 요청간 상태 정보를 공유하지 않는다.
저번 게시글에다가 들었던걸 적었다.. 카페 점원과 언제나 같은걸 사먹는 고객!
수평 확장이 가능한 이유
- 서버가 상태를 기억하지 않고 초기화되기 때문에, 새로운 서버를 자유롭게 추가해도 문제가 발생하지 않는다.
- 사용자가 어떤 서버로 요청을 보내든 항상 동일한 결과를 얻을 수 있기 때문에, 로드밸런서가 자유롭게 요청을 분산할 수 있다.
- 원하는만큼 수평 확장이 가능해지고, 트래픽이 갑자기 증가해도 서버만 늘려주면 안정적인 대응이 가능해진다.
Client\Load Balancer\ServerA
Client\Load Balancer\ServerB
Client\Load Balancer\ServerC
뭐 이런식으로 서버를 증설해도 서버가 상태를 저장하지 않으므로, 클라이언트는 어떤 서버로 가도 결과가 같다.
스프링부트 : 실무예시
@GetMapping("/mypage")
public ResponseEntity<UserInfo> myPage(
@AuthenticationParincipal JwtAuthentication auth) { // auth에는 사용자 ID가 있음
return ResponseEntity.ok(userService.findInfo(auth.userId()));
}
* 클라이언트는 사용자 정보를 헤더나 쿼리로 매번 전달해야한다.
그런데 이건 어떻게 보면 클라이언트에게 전가하는게 꽤 많은거 아닌가..
* 서버는 이 요청 하나만 보고도 처리가 가능해짐
디버깅 포인트.. 뭔소린지 모르겠다
- 서버를 재시작해도 세션이 끊이지 않음?
- 요청에 필요한 정보만 제대로 포함되어 있다면, 서버든 언제든지 처리가 가능하다.
* 로드밸런서를 사용할 때 Sticky Session 설정은 필요없다.
* 기존 시스템에 세션 상태를 들고 있던 서버는 확장했을 때,
- 특정 서버에 계속 요청이 가야하므로 로드밸런싱이 불균형해지고 장애 위험이 증가함
- 반면 무상태 구조에서는 서버를 매우 많이 확장해도 안정성 유지가 가능하다
3. Cacheable : 네트워크 부하가 감소함
서버 응답에는 반드시 캐시 가능 여부를 명시함
중간계층(CDN, 브라우저)이 응답을 저장하고 재사용할 수 있게 함
Spring Boot ETgs 구현예시
@GetMapping("/{id}") ID 경로에 대한 GET요청 (/products/5)
public ResponseEntity<ProductDto> getProduct(
@PathVariable Long id, // URL 경로로 전달받은 ID
@RequestHeader(value="If-None-Match", required=false) String inm
// 클라이언트의 캐시 ETag값, 없을수도 있다고 함
) {
ProductDto dto = service.find(id); // 서비스를 호출해서 전달받은 ID의 상품정보를 조회한다
// 서버기준 ETag값 생성, updatedAt을 UTC 초단위로 변환 후(EpochSecond) 문자열로 반환함(String)
String eTag = "\"" + dto.getUpdatedAt().toEpochSecond(ZoneOffset.UTC) + "\"";
if(eTag.equals(inm)) {
return ResponseEntity.status(304).eTag(etag).build();
// eTag랑 @ResponseHeader로 전달받은 inm이 같다면 304 not modified를 반환한다
// 그 때 eTag값을 다시 포함시켜서 응답한다는데..
// body() 없이 build()만 넣어서 응답을 생성한다
}
// if 조건문에 해당하지 않으면
return ResponseEntity.ok().eTag(eTag).cacheControl(CacheControl
.maxAge(60, TimeUnit.SECONDS)
.mustRevalidate())
.body(dto);
// 리턴하는데, 200 OK 상태코드로 eTag 헤더를 설정하고(eTag(eTag))
// cacheControl 헤더를 설정하는데
// 최대 60초동안 캐시를 허용한다.(maxAge)
// 그리고 캐시가 만료되고 서버 재검증을 한다고 한다. (mustRevalidate)
}
}
* 정적 리소스에는 maxAge를 길게, 동적 리소스에는 eTag + mustRevalidate 조합을 사용한다고 한다.
이건 프로그래밍에 깨달음을 얻으면 한번 읽어보자
- 브라우저 계층은 캐시 위치가 로컬 스토리지이며, UX개선을 할 수 있다고 한다
- CDN, 중간계층은 엣지 노드에 캐시가 있으며 글로벌 지연이 내려간다고..
4. Uniform Interface : 일관성있는 리소스 접근
Uniform Interface는 REST의 핵심 개념이다. 모든 리소스에 일관된 방식으로 접근할 수 있도록 규정한다.
이를 통해 서버 - 클라이언트의 결합도를 낮추고 확장성, 유지보수성을 높인다.
※※ REST는 URI로 자원을 명확하게 식별하고, HTTP 메서드로 행위를 구분한다.
즉 '무엇을' 과 '무엇을 할지??' 를 분리해서 설계해야만한다.
| 행위 | HTTP 메서드 | URI | |
| 생성 | POST | /orders | 주문을 생성함(서버가 ID를 부여함) |
| 조회 | GET | /orders/99 | ID가 99인 주문을 조회한다 |
| 수정 | PUT | /orders/99 | 주문을 전체 수정해서 모든 코드를 복붙한다 |
| 부분수정 | PATCH | /orders/99 | 일부 필드만 수정하고 나머지는 변경하지 않음 (ex : 커피 3잔에서 2잔으로 바꾸시겠어요?) |
| 삭제 | DELETE | /orders/99 | 해당 주문을 삭제한다 |
URI는 리소스 자체(무엇)를 나타내고 // HTTP 메서드는 행위(무엇을 할지)를 명확하게 표현한다.
이걸 모르면 넌 설계할 수 없어 알겠니?
4대구성 규칙
| 규칙 | 예시 | |
| URI기반 식별 | /orders/99 | 명사형, 복수형을 사용함 (/orders, /users, /members) |
| Representation 조작 | 리소스를 JSON 등으로 표현하고 클라이언트가 수정한다 |
PUT /orders/99 {quantity:2} |
| Self-descriptive Message | 요청, 응답만으로 의미를 파악할 수 있음 | Content-Type, 상태코드(200,300,400) 등 명확하게 알 수 있음 |
| HATEOAS | 응답 내 다음 행동 링크 제공? | {"next" : "/orders/99/pay"} |
Uniform Interface 예시 : HATEOAS 기반 응답 구성
// 주문정보 조회, GET
@GetMapping("{/id}") // ID로 식별함
public EntityModel<OrderDto> find(PathVariable Long id) {
// 서비스 계층에서 ID를 받은걸로 주문정보를 조회한다
OrderDto dto = orderService.find(id);
// 조회된 주문정보를 EntityModel?? 로 감싸서 반환한다
return EntityModel.of(dto)
.add(linkTo(methodOn(OrderController.class).pay(id))
.withRel("pay"));
// linkTo(methodOn(~~~~)) 은 pay(id) 메서드의 URI를 자동으로 생성해준다.
// withRel("pay")는 이 링크가 "pay", 즉 결제기능과 관련이 있음을 명시해준다.
}
* HATEOAS(Hypermedia As The Engine Of Application State)
클라이언트가 서버 응답 안의 링크,
즉 응답을 받은 클라이언트가 "응답 받고 뭘 해야하지?"를 링크를 보고 판단할 수 있게 한다고 함.. 아직 잘 모르겠다
* URI에는 동사대신 명사를 사용함
* 리소스간 계층 구도를 표현할때는 /users/99/orders 형태를 사용함
*버전 정보에 대해서는 URI가 아닌 헤더로 처리하는 방식도 고려되며, 실무에서는 보통 /v1/orders처럼 URI 버전을 포함시키는
방식도 많이 사용한다고 합니다....
리소스는 명사이고, 행위는 동사이다.
메뉴판을 URI라고 하자. 여기서 무엇을 주문할 수 있는지 보여준다.
나 : 카푸치노 하나 주세요 - > POST, /orders
나 : 아까 주문한거 확인해주세요 - > GET, /orders/345543
나 : 수량 변경할게요 - > PATCH, orders/345543
나 : 아 됐어요 주문 취소할게요 - > DELETE, orders/345543
내가 말하고 있는게 동사(HTTP 메서드)이고, 리소스가 명사(URI)이다.
5. Layerd System : 계층형 아키텍처
REST 시스템은 여러 계층으로 나눌 수 있는 구조를 기본으로 한다.
클라이언트는 중간 계층이 존재해도 최종서버와 통신하는 것처럼 동작할 수 있다고 한다.
보안, 로깅, 로드밸런싱, 캐싱 등 별도의 계층으로 전~부 분리해서 역할을 위임할 수 있도록 해준다고 한다.
layered-arch(폴더)
layered-arch\client-app (React,Flutter 등 클라이언트)
layered-arch\api-gateway (Spring Cloud Gateway, 인증/라우팅)
layered-arch\auth-service (인증?? 마이크로서비스?)
layered-arch\product-service (핵심 비즈니스 로직)
Spring Cloud Gateway 예시
spring:
cloud:
gateway:
routes:
- id: product
uri: lb://product-service
predicates:
- Path=/api/v1/products/**
// 이게.. 머고..
- 클라이언트 요청은 Gateway를 먼저 통과한다고 함
- Gateway는 요청 경로에 따라 적절한 마이크로서비스로 라우팅한다고 함..
- 이러한 구조를 통해 인증 로직을 분리하거나, 트래픽을 조절하는 캐시 계층?을 삽입할 수 있다고 한다.
모르겠으니 예시로 이해해보자
소중한 고객님께서(클라이언트) 마트에 물건을 사러간다고 하자.
1. 입구 보안 요원(API Gateway)이 니가 총을 들고 있는지, 위험한 사람은 아닌지 신분증(인증)을 확인할 수 있다
2. 카운터 직원(Business Service)이 계산을 해줄 것이다
3. 창고 직원(DB)이 상품을 가져다줄 것이다.
어..
각 계층은 서로가 어떤 방식으로 일하는진 몰라도 자기 자신의 역할만 충실히 수행하면 마트가 돌아가겠지???
* 로깅, 인증, 트래픽 제어, 캐시 등을 독립계층으로 분리하면 유지보수가 잘 되고 장애전파도 줄어든다고 함
* 각 계층은 스프링에서 별도 모듈 또는 마이크로서비스로 구성할 수 있다고 한다...
Client 계층 : 사용자 요청 전송 (React, Flutter)
Gateway 계층 : 인증, 라우팅 ,요청 필터링 (Spring Cloud Gateway)
Service 계층 : 핵심 비즈니스 로직 수행 (Spring Boot Service들)
DB 계층 : 데이터 저장 및 조회 (SQL, MongoDB, Oracle 등등)
정리
Client-Server : 역할 분리로 독립적 배포와 개발이 가능함
Stateless : 요청 독립성, 수평 확장성 업
Cacheable : 네트워크 비용을 줄이고 응답속도를 높인다
Uniform Interface : 일관된 API 설계로 유지보수를 경감시킨다
Layered System : 보안, 로깅, 스케일링 유연성이 높아진다 - > 중간계층을 자유롭게 삽입할 수 있음
하..
'Spring Boot' 카테고리의 다른 글
| RESTful 구현 기본 : Controller에서 요청 처리 (1) | 2025.07.18 |
|---|---|
| RESTful API 기본 : @RestController (6) | 2025.07.18 |
| REST : 탄생배경과 개념 (7) | 2025.07.10 |
| MVC : Spring Boot 시작하기 (0) | 2025.07.06 |
| Spring MVC : 요청 처리 흐름 (8) | 2025.07.06 |