REST(Representational State Transfer)는 HTTP 프로토콜을 기반으로 한 아키텍처 스타일로, HTTP/1.0과 1.1의 설계에 참여한 Roy Fielding이 2000년 논문에서 처음 제안했습니다. RESTful 서비스는 HTTP의 장점을 최대한 활용하여, 일관성 있고 확장 가능한 API를 제공합니다.
| 원칙 | 설명 |
|---|---|
| Uniform Interface (유니폼 인터페이스) | 리소스 식별, 리소스 조작, 자기 설명 메시지, HATEOAS 등의 일관된 인터페이스 제공 |
| Stateless (무상태성) | 각 요청은 독립적이며, 서버는 세션/쿠키 등 상태를 저장하지 않음 |
| Cacheable (캐시 가능) | HTTP 표준의 Last-Modified, ETag 등을 활용하여 클라이언트·프록시 캐싱 지원 |
| Self-Descriptive Messages (자기 표현 구조) | 요청/응답 메시지만으로 의미를 이해할 수 있도록 JSON/XML 기반의 명확한 구조 사용 |
| Client–Server 구조 | 역할 분리로 클라이언트와 서버의 독립적인 개발 및 확장 가능 |
| Layered System (계층화) | 프록시, 게이트웨이, 로드 밸런서 등 중간 계층 도입으로 유연성 확보 |
| 잘못된 예시 | 문제점 | 올바른 예시 |
|---|---|---|
GET /get-user_list.json | 1) 동사(get) 사용 2) 밑줄( _) 사용3) .json 확장자 사용 | GET /users |
user-list처럼 대시(-)를 씁니다.Accept: application/json 헤더로 JSON을 요청합니다.| 잘못된 예시 | 문제점 | 올바른 예시 |
|---|---|---|
GET /users/123/orders_list/ | 1) 언더스코어 사용 2) 트레일링 슬래시( /) 있음 | GET /users/123/orders |
/users/123/orders는 “ID가 123인 유저의 주문 목록”이라는 계층 관계를 그대로 보여줍니다./가 없으면 동일한 리소스를 중복 관리하지 않아도 됩니다.| 잘못된 예시 | 문제점 | 올바른 예시 |
|---|---|---|
GET /product/456.json | 1) 단수 리소스 사용 2) .json 확장자 사용 | GET /products/456 |
| 잘못된 예시 | 문제점 | 올바른 예시 |
|---|---|---|
GET /user_profiles | 언더스코어(_) 사용 | GET /user-profiles |
_)는 외형상 잘 안 보이고, 검색엔진도 단어 사이 구분자로 인식하지 않습니다.-)는 시각적으로 읽기 쉽고, SEO에도 더 유리하기 때문에 RESTful URI나 블로그 슬러그 등에 권장됩니다.
- SEO(검색엔진최적화)
구글 같은 검색엔진은 대시(-)를 ‘단어 구분자’로 인식합니다. 언더스코어(_)는 단어를 이어 붙이는 연결부로 인식해서, 실제로는 “userprofiles”처럼 한 덩어리로 처리할 수 있습니다. 따라서 “user-profiles”처럼 대시를 쓰면 검색 키워드 분리가 잘 돼서 검색노출에 유리합니다.- 표준 권장사항
[RFC 3986 (URI 표준)] 에도 언더스코어는 특별한 의미가 있는 문자가 아니어서 “안 쓰는 게 좋다”고만 명시돼 있고, 반면 하이픈(대시)은 가독성·SEO 양쪽에서 사실상 ‘사실상의 표준’으로 자리 잡고 있습니다.
| CRUD | HTTP 메서드 | 설명 |
|---|---|---|
| Create | POST | 리소스 생성 |
| Read | GET | 리소스 조회 |
| Update | PUT/PATCH | 리소스 갱신 |
| Delete | DELETE | 리소스 삭제 |
<div class="menu-test">
<h4>추천메뉴(GET)</h4>
<form id="menuRecommendationFrm">
<!-- 음식 타입 -->
<input type="radio" name="foodType" value="kr" checked> 한식
<input type="radio" name="foodType" value="ch"> 중식
<input type="radio" name="foodType" value="jp"> 일식
<!-- 매운맛/순한맛 -->
<input type="radio" name="taste" value="hot" checked> 매운맛
<input type="radio" name="taste" value="mild"> 순한맛
<button type="button" class="btn-send">전송</button>
</form>
<div id="menuRecommendation-result"></div>
</div>
<script>
$(function() {
$('.btn-send').click(function() {
$.ajax({
url: '/menu/menu',
type: 'GET',
data: $('#menuRecommendationFrm').serialize(),
dataType: 'json',
success: function(data) {
let html = '<table><tr><th>음식점</th><th>메뉴</th><th>가격</th></tr>';
data.forEach(item => {
html += `<tr><td>${item.restaurant}</td><td>${item.foodName}</td><td>${item.price}</td></tr>`;
});
html += '</table>';
$('#menuRecommendation-result').html(html);
},
error: (xhr, status, err) => console.error(err)
});
});
});
</script>
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private MenuService menuService;
@GetMapping("/menu")
public List<Menu> recommend(
@RequestParam String foodType,
@RequestParam String taste
) {
Map<String, String> params = Map.of(
"foodType", foodType,
"taste", taste
);
return menuService.selectMenuRecommendation(params);
}
}
@Service
public class MenuServiceImpl implements MenuService {
@Autowired
private MenuDAO menuDAO;
@Override
public List<Menu> selectMenuRecommendation(Map<String, String> map) {
return menuDAO.selectMenuRecommendation(map);
}
}
<!-- resources/mapper/menu/menu-mapper.xml -->
<mapper namespace="menu">
<select id="selectMenuRecommendation" resultType="menu">
SELECT * FROM menu
WHERE foodType = #{foodType}
AND taste = #{taste}
</select>
</mapper>
사용 예시: /menu/{foodType} 경로로 한식(kr), 중식(ch), 일식(jp) 메뉴 전체 조회
<select id="foodTypeSelector">
<option disabled selected>음식 타입 선택</option>
<option value="kr">한식</option>
<option value="ch">중식</option>
<option value="jp">일식</option>
</select>
<div id="foodType-result"></div>
<script>
$('#foodTypeSelector').change(function() {
const type = $(this).val();
$.getJSON(`/menu/${type}`, data => {
let html = '<table><tr><th>번호</th><th>음식점</th><th>메뉴</th><th>가격</th></tr>';
data.forEach(i => {
html += `<tr><td>${i.foodNo}</td><td>${i.restaurant}</td><td>${i.foodName}</td><td>${i.price}</td></tr>`;
});
$('#foodType-result').html(html);
});
});
</script>
@GetMapping("/{foodType}")
public List<Menu> byType(@PathVariable String foodType) {
return menuService.selectMenuByfoodType(Map.of("foodType", foodType));
}
1. pom.xml에 <packaging>jar</packaging> 설정
2. mvn package 실행 → target/*.jar 생성
1. <packaging>war</packaging>
2. SpringBootServletInitializer 상속
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder builder
) {
return builder.sources(Application.class);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. mvn package → target/*.war
4. 톰캣 webapps 폴더에 배포
Roy Fielding의 REST 아키텍처
https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
Spring MVC @RequestBody 정리
http://bluesky-devstudy.blogspot.com/2016/07/spring-mvc-requestbody.html