Spring 예제와 함께 REST API 알아보자

예원·2022년 12월 28일
3

Spring 글 모아보기

목록 보기
14/17

REST란?

REST의 정의

"REpresentational State Transfer"의 약자로,

2000년도에 로이 필딩(Roy Fielding)의 박사학위 논문에서 최소로 소개되었다.

당시 웹(HTTP) 설계의 우수성이 제대로 활용되지 못하는 점을 안타까워하며 웹의 장점을 최대한으로 활용할 수 있는 아키텍처로써 REST를 발표했다.

REST는 자원을 이름으로 구분해 해당 자원의 상태를 주고 받는 모든 것을 의미한다.

다음의 3가지로 구성되어 있다.

1. 자원(Resource) - URI

  • 모든 자원은 고유한 ID를 가지고 ID는 서버에 존재하며 클라이언트는 각자원의 상태를 조작하기 위해 요청을 보낸다.
  • 이러한 자원을 구별하는 ID는 'user/:user_id', 'user/1' 과 같은 HTTP URI이다.

2. 행위(Verb) - Method

  • 클라이언트는 URI를 이용해 자원을 지정하고 조작하기 위해 Method를 사용한다.
  • HTTP 프로토콜은 GET, POST, PUT, PATCH, DELETE의 Method를 제공한다.
    Method정의CRUD
    GET리소스를 조회하고 정보를 가져옵니다.READ
    POST리소스를 생성합니다.CREATE
    PUT해당 리소스를 수정합니다.CREATE or UPDATE
    PATCH해당 리소스를 수정합니다.(일부만 수정)UPDATE
    DELETE해당 리소스를 삭제합니다.DELETE

3. 표현(Representation of Resource)

  • 클라이언트가 서버로 요청을 보냈을 때 서버가 응답으로 보내주는 자원의 상태를 의미한다.
  • JOSN, XML, TEXT 등 여러 형태로 나타낼 수 있다.

REST의 특징

  1. 클라이언트 - 서버 구조(Client - Server)
    • 자원이 있는 Server, 자원을 요청하는 Client의 구조를 가진다.
  2. 무상태(Stateless)
    • HTTP 프로토콜은 Stateless Protocol이므로 REST 역시 무상태성을 갖는다.
  3. 캐시 처리 기능(Cacheable)
    • 웹 표준 HTTP 프로토콜을 그대로 사용하므로 웹에서 사용하는 기존의 인프라를 그대로 활용 할 수 있다.
  4. 계층 구조(Layered System)
    • API 서버는 순수 비즈니스 로직을 수행하고 그 앞단에 사용자 인증, 암호화, 로드밸런싱 등을 하는 계층을 추가하여 구조상의 유연성을 줄 수 있다.
  5. 인터페이스 일관성(Uniform Interface)
    • URI로 지정한 자원에 대한 조작을 통일되고 한정적인 인터페이스로 수행한다.
    • HTTP 표준에만 따른다면 모든 플랫폼에 사용이 가능하다.
  6. 자체 표현 구조
    • 동사(Method) + 명사(URI)로 이루어져 있다.
    • 따라서 어떤 메서드에 무슨 행위를 하는지 메시지 자체만 보고도 쉽게 이해할 수 있다.

REST API란?

REST API의 정의

REST의 특징을 기반으로 서비스 API를 구현한 것이다.
Open API, 웹 개발 등에서 REST API를 많이 사용하고 있다.

대표적으로 Kakao API가 있습니다.

REST API의 특징

REST API는 각 요청이 어떤 동작이나 정보를 위한 것 인지를 요청 그 자체로 추론이 가능한 것이다.

예를 들어보겠다.
아래의 API가 어떤 동작을 수행할까?

GET /boards

Method와 URI를 통해 "게시물(boards)를 조회한다"라고 유추할 수 있다.
이와 같이 API만을 보고 추론할 수 있다.

REST API 디자인 가이드

REST API 설계 시 가장 중요한 항목은 2가지이다.

  • URI는 정보의 자원을 표현해야 한다.
  • 자원에 대한 행위는 HTTP Method(GET, POST, PUT, PATCH, DELETE)로 표현한다.

이 두가지는 꼭 기억해야 한다.

URI의 설계 규칙

1. URI는 소문자로만 구성한다.

  • 대문자는 때로 문제를 일으키는 경우가 있어 소문자를 선호한다.

2. URI에는 행위를 포함하지 않는다.

  • URI는 명사만 사용하며, 행위는 Method로 표시해야 한다.
    ❌ GET /getUser/:id
    ⭕ GET /user/:id

3. 언더바(_) 대신 하이픈(-)을 사용한다.

  • 언더바는 글자 폰트에 따라 부분적으로 가려지거나 숨겨질 수 있다.
  • 가독성을 위해 긴 단어는 하이픈으로 구분하는 것이 좋다.
    ❌ <http://api.example.com/students/course_ranking>
    ⭕ <http://api.example.com/students/course-ranking>

4. 슬래시(/)는 계층 관계를 표현한다.

  • 슬래시는 URI 경로 부분에서 자원 간의 계층적 관계를 나타내기 위해 사용한다.
    <http://api.example.com/students/3> -> 3번 학생
    

5. URI의 마지막 문자로 슬래시(/)를 포함하지 않는다.

  • 마지막 슬래시는 의미가 없으나 혼란을 가져올 수 있다.
    ❌ <http://api.example.com/students/>
    ⭕ <http://api.example.com/students>

6. 파일 확장자는 URI에 포함 시키지 않는다.

  • 파일 확장자는 URI 대신 Content-Type 헤더를 통해 전달 되는대로 미디어 타입을 사용하여 body 콘텐츠를 처리하여야 한다.
  • Accept Header를 통해 응답으로 받고 싶은 미디어 타입을 명시한다.
    ❌ <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 vs RESTful API

REST API와 RESTful API의 차이는 뭘까?

RESTful API는 REST의 설계규칙을 잘 지켜서 설계된 API이다.

RESTful하게 만든 API는 요청을 보내는 주소만으로도 어떤 것을 요청하는지 파악할 수 있다.

즉, RESTful API는 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것을 목적으로 한다.


Spring 예제

학생을 조회하는 예제를 통해 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;
    }
}

Controller

컨트롤러를 구현한다.

클라이언트가 요청한 ID를 가진 학생을 조회하여 결과를 반환하도록 할 것이다.

REST API를 구현하기 위해서는 URI 설계가 중요하다.

이번 예제에서는 아래와 같이 설계할 수 있다.

  • 조회한다 => GET Method
  • 학생 => students(복수형 작성)
  • 요청한 ID => students/:id(계층 관계 표현)

설계된 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());
}

TEST

테스트 코드를 통해 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 실습을 이였다.

전체 코드가 궁금하다면 여기서 확인할 수 있다.


reference

0개의 댓글