"REpresentational State Transfer"의 약자로,
2000년도에 로이 필딩(Roy Fielding)의 박사학위 논문에서 최소로 소개되었다.
당시 웹(HTTP) 설계의 우수성이 제대로 활용되지 못하는 점을 안타까워하며 웹의 장점을 최대한으로 활용할 수 있는 아키텍처로써 REST를 발표했다.
REST는 자원을 이름으로 구분해 해당 자원의 상태를 주고 받는 모든 것을 의미한다.
다음의 3가지로 구성되어 있다.
1. 자원(Resource) - URI
2. 행위(Verb) - Method
Method | 정의 | CRUD |
---|---|---|
GET | 리소스를 조회하고 정보를 가져옵니다. | READ |
POST | 리소스를 생성합니다. | CREATE |
PUT | 해당 리소스를 수정합니다. | CREATE or UPDATE |
PATCH | 해당 리소스를 수정합니다.(일부만 수정) | UPDATE |
DELETE | 해당 리소스를 삭제합니다. | DELETE |
3. 표현(Representation of Resource)
REST의 특징을 기반으로 서비스 API를 구현한 것이다.
Open API, 웹 개발 등에서 REST API를 많이 사용하고 있다.
대표적으로 Kakao API가 있습니다.
REST API는 각 요청이 어떤 동작이나 정보를 위한 것 인지를 요청 그 자체로 추론이 가능한 것이다.
예를 들어보겠다.
아래의 API가 어떤 동작을 수행할까?
GET /boards
Method와 URI를 통해 "게시물(boards)를 조회한다"라고 유추할 수 있다.
이와 같이 API만을 보고 추론할 수 있다.
REST API 설계 시 가장 중요한 항목은 2가지이다.
이 두가지는 꼭 기억해야 한다.
1. URI는 소문자로만 구성한다.
2. URI에는 행위를 포함하지 않는다.
❌ GET /getUser/:id
⭕ GET /user/:id
3. 언더바(_) 대신 하이픈(-)을 사용한다.
❌ <http://api.example.com/students/course_ranking>
⭕ <http://api.example.com/students/course-ranking>
4. 슬래시(/)는 계층 관계를 표현한다.
<http://api.example.com/students/3> -> 3번 학생
5. URI의 마지막 문자로 슬래시(/)를 포함하지 않는다.
❌ <http://api.example.com/students/>
⭕ <http://api.example.com/students>
6. 파일 확장자는 URI에 포함 시키지 않는다.
❌ <http://api.example.com/sutdents/photo.jpg>
⭕ <http://api.example.com/sutdents/photo>
HTTP/1.1
Host: api.example.com
Accept: image/jpg
7. URI에서 영어는 복수형으로 작성한다.
REST API와 RESTful API의 차이는 뭘까?
RESTful API는 REST의 설계규칙을 잘 지켜서 설계된 API이다.
RESTful하게 만든 API는 요청을 보내는 주소만으로도 어떤 것을 요청하는지 파악할 수 있다.
즉, RESTful API는 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것을 목적으로 한다.
학생을 조회하는 예제를 통해 REST API를 실습해본다.
우선 학생을 저장하고 조회하는 예제를 만들었다.
REST API를 공부하는 예제이기 때문에, 이 부분에 대한 설명은 생략한다.
Student
@Data
public class Student {
private Long id;
private String name;
private String dept;
public Student(String name, String dept) {
this.name = name;
this.dept = dept;
}
}
StudentRepository
@Repository
public class StudentRepository {
private static Map<Long, Student> store = new HashMap<>();
private static long sequence = 0l;
public void save(Student student) {
student.setId(++sequence);
store.put(student.getId(), student);
}
public Optional<Student> findById(Long studentId) {
return Optional.ofNullable(store.get(studentId));
}
}
StudentService
public interface StudentService {
void save(Student student);
Optional<Student> find(Long id);
}
@Service
public class StudentServiceImpl implements StudentService {
private final StudentRepository studentRepository;
@Autowired
public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
@Override
public void save(Student student) {
studentRepository.save(student);
}
@Override
public Optional<Student> find(Long id) {
return studentRepository.findById(id);
}
}
클라이언트에게 응답을 넘겨줄 때는 JSON 형태로 주는 것이 일반적이다.
주로 상태코드, 응답메시지, 데이터를 형태로 많이 구성한다.
이번 예제에서는 상태코드, 상태메시지, 응답메시지, 데이터로 구성해볼 것이다.
결과적으로 아래와 같은 형태가 된다.
{
"code": 200,
"httpStatus": "OK",
"message": "학생 조회 성공",
"data": {
"id":1,
"name":"yewon",
"dept":"computer-system"
}
RestResponse
형태에 맞는 응답을 전달하기 위한 DTO를 생성한다.
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RestResponse<T> {
private Integer code;
private HttpStatus httpStatus;
private String message;
private T data;
}
Message
사용할 응답메시지를 담은 클래스도 생성해준다.
public enum Message {
READ_STUDENTS("학생 조회 성공"),
NOT_FOUND_STUDENTS("학생 조회 실패");
private final String label;
Message(String label) {
this.label = label;
}
public String label() {
return label;
}
}
컨트롤러를 구현한다.
클라이언트가 요청한 ID를 가진 학생을 조회하여 결과를 반환하도록 할 것이다.
REST API를 구현하기 위해서는 URI 설계가 중요하다.
이번 예제에서는 아래와 같이 설계할 수 있다.
설계된 URI는 /students/{id}
이며
컨트롤러에 해당 요청에 대한 기능을 구현하였다.
@GetMapping("/students/{id}")
public ResponseEntity<RestResponse> find(@PathVariable Long id) {
// 응답 내용을 저장할 restResponse를 생성한다.
RestResponse<Object> restResponse = new RestResponse<>();
// ID를 이용해 학생 정보를 조회한다.
Optional<Student> student = studentService.find(id);
// 조회 결과가 존재한다면,
if (student.isPresent()) {
// 성공 응답과 조회 결과를 restResponse에 저장한다.
restResponse = RestResponse.builder()
.code(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.message(Message.READ_STUDENTS.label())
.data(student)
.build();
// 조회 결과가 존재하지 않는다면,
} else {
// NOT FOUND 응답을 restResponse에 저장한다.
restResponse = RestResponse.builder()
.code(HttpStatus.NOT_FOUND.value())
.httpStatus(HttpStatus.NOT_FOUND)
.message(Message.NOT_FOUND_STUDENTS.label())
.build();
}
// 응답 결과로 restResponse를 전달한다.
return new ResponseEntity<>(restResponse, restResponse.getHttpStatus());
}
테스트 코드를 통해 REST하게 응답이 오는지 확인해본다.
원활한 테스트를 위해 실행 전 임의의 데이터 한 개를 저장하도록 하였다.
@BeforeEach
void before() {
Student student = new Student("yewon", "computer-system");
// ID 값은 자동으로 1로 저장됩니다.
studentService.save(student);
}
조회 성공 테스트
ID가 1인 학생을 조회해본다.
/student/1
로 GET 요청을 보낸다.
@Test
@DisplayName("API 성공 테스트")
public void Api_200() throws Exception {
mockMvc.perform(get("/students/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("학생 조회 성공"))
.andExpect(jsonPath("$.data.name").value("yewon"))
.andExpect(jsonPath("$.data.dept").value("computer-system"))
.andDo(print());
}
테스트 결과에 초록불이 이쁘게 들어왔다.
body 부분을 살펴보면 200 OK와 함께 조회된 데이터가 있는 것을 확인할 수 있다.
{
"code":200,
"httpStatus":"OK",
"message":"학생 조회 성공",
"data":{
"id":1,
"name":"yewon",
"dept":"computer-system"
}
}
조회 실패 테스트
학생이 존재하지 않는 경우도 확인해본다.
@Test
@DisplayName("API 실패 테스트")
public void Api_400() throws Exception {
mockMvc.perform(get("/students/99"))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.message").value("학생 조회 실패"))
.andExpect(jsonPath("$.data").isEmpty())
.andDo(print());
}
마찬가지로 테스트 결과에 초록불이 이쁘게 들어왔다.
body 부분을 살펴보니 404 NOT FOUND 상태 코드와 '학생 조회 실패' 응답 메시지가 전달되었다.
{
"code":404,
"httpStatus":"NOT_FOUND",
"message":"학생 조회 실패",
"data":null
}
지금까지 간단한 예제를 통해 REST API 실습을 이였다.
전체 코드가 궁금하다면 여기서 확인할 수 있다.