부스트코스 강의를 듣고 정리한 내용.

REST API

API란?

응용 프로그램에서 사용할 수 있도록, OS나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스. 주로 파일 제어, 창 제어, 화상 처리, 문자 제어 등을 위한 인터페이스를 제공한다.

REST API란?

REST(REpresentational State Transfer) API란 REST API 형식의 API를 말한다. REST API는 핵심 컨텐츠 및 기능을 외부 사이트에서 활용할 수 있도록 제공되는 인터페이스이다. 요즘 대다수의 웹서비스들은 REST API 형태로 서비스를 제공한다.

REST API의 구성 요소에는 자원, 행위, 그리고 표현이 있다.

  • 자원 (Resource): 서비스를 제공하는 시스템의 자원. 각 자원은 URI로 표시되며, 유일한 식별자로써 표현된다.
  • 행위 (Verb): HTTP METHOD
  • 표현 (Representation)

REST의 특징

REST API는 반드시 아래의 스타일(제약 조건의 집합)을 지켜야 한다.

  1. Client - Server구조
    REST 서버는 API 제공, 클라이언트는 사용자 인증이나 컨텍스트(세션, 로그인 정보)등을 직접 관리하는 구조로 각각의 역할이 확실히 구분되기 때문에 클라이언트와 서버에서 개발해야 할 내용이 명확해지고 서로간 의존성이 줄어들게 된다.

  2. Stateless (무상태성)
    REST는 작업을 위한 상태 정보를 따로 저장하고 관리하지 않는다. 세션 정보나 쿠키 정보를 별도로 저장하고 관리하지 않기 때문에 API 서버는 들어오는 요청만을 단순히 처리하면 된다. 따라서 서비스의 자유도가 높아지고 서버에서 불필요한 정보를 관리하지 않음으로써 구현이 단순해진다.

  3. Cacheable (캐시 가능)
    REST의 가장 큰 특징 중 하나는 HTTP라는 기존 웹표준을 그대로 사용하기 때문에 웹에서 사용하는 기존 인프라를 그대로 활용할 수 있다는 것이다. 따라서 HTTP 프로토콜 표준에서 사용하는 Last-Modified 태그나 E-Tag를 이용하면 캐싱을 구현할 수 있다.

  4. Self-descriptiveness (자체 표현 구조)
    REST API 메시지만 보고도 이를 쉽게 이해할 수 있는 자체 표현 구조로 되어있다.

  5. Layered System (계층형 구조)
    REST API는 다중 계층으로 구성될 수 있으며 보안, 로드 밸런싱, 암호화 계층을 추가해 구조상의 유연성을 둘 수 있고 PROXY, 게이트웨이 같은 네트워크 기반의 중간 매체를 사용할 수 있게 한다.

  6. Uniform Interface
    HTTP 표준을 따르기만 한다면 특정 언어나 기술에 종속되지 않고 모든 플랫폼에 사용할 수 있으며 URI로 지정한 리소스에 대한 조작이 가능한 아키텍처 스타일을 의미한다.

HTTP 프로토콜을 사용하면 client-server, stateless, cache, layered system, code-on-demand 등에 대해서는 모두 쉽게 구현이 가능하다. 문제는 uniform interface이다.

Uniform Interface의 스타일

  • 리소스가 URI로 식별돼야 한다.
  • 리소스를 생성, 수정, 추가하고자 할 때 HTTP 메시지에 표현을 해서 전송해야 한다.
  • 메시지는 스스로 설명할 수 있어야 한다. (self-descriptive message)
  • 어플리케이션의 상태를 하이퍼링크를 이용해 전이되어야 한다. (HATEOAS)

첫 번째와 두 번째 항목은 지키기 어렵지 않으나, 메시지가 스스로 설명할 수 있어야 하는 부분과 HATEOAS를 지원하는 것은 웹과는 다르게 API로는 쉽지 않다.

응답 결과에 보통 JSON 메시지를 사용하게 되는데, 이 JSON 메시지가 어디에 전달되는지, 그리고 JSON 메시지를 구성하는 것이 어떤 의미를 표현해야만 메시지 스스로 설명할 수 있다고 말할 수 있는데 그게 쉽지 않기 때문이다.

또 HATEOAS(Hypermedia as the Engine of Application State)를 API에서 제공하는 것도 쉽지 않다. HATEOAS란 하이퍼미디어를 어플리케이션의 상태를 관리하기 위한 메커니즘으로 사용한다는 아이디어이다. HATEOAS란 REST API를 사용하는 클라이언트가 전적으로 서버와 동적인 상호작용이 가능하도록 하는 것을 의미한다. 이러한 방법은 클라이언트가 서버로부터 어떤 요청을 할 때, 요청에 필요한 URI를 응답에 포함시켜 반환하는 것으로 가능하게 할 수 있다.

REST의 uniform interface를 지원하는 것이 쉽지 않기 때문에 , 많은 서비스는 REST에서 바라는 것을 모두 지원하지 않고 API를 만들게 된다. REST의 모든 것을 제공하지 않으면서 REST API라 말하는 경우도 있는 반면 REST API의 모든 것을 제공하지 않으면 Web API 또는 HTTP API라 부르는 경우는 경우도 있다.

REST API의 장단점

장점

  • HTTP 프로토콜의 인프라를 그대로 사용하므로 REST API 사용을 위한 별도의 인프라를 구축할 필요가 없음
  • HTTP 프로토콜의 표준을 최대한 활용해 여러 추가적인 장점을 함께 가져갈 수 있게 해줌
  • HTTP 표준 프로토콜에 따르는 모든 플랫폼에서 사용 가능
  • Hypermedia API의 기본을 충실히 지키면서 범용성 보장
  • REST API 메시지가 의도하는 바를 명확하게 나타내므로 의도하는 바 쉽게 파악 가능
  • 여러 가지 서비스 디자인에서 생길 수 있는 문제 최소화
  • 서버와 클라이언트의 역할을 명확히 분리

단점

  • 표준 자체가 존재하지 않음
  • 사용할 수 있는 메소드가 4가지밖에 없음
  • 제한된 HTTP Method 형태
  • 브라우저를 통해 테스트할 일이 많은 서비스라면 쉽게 고칠 수 있는 URL보다 Header 정보의 값을 처리해야 하므로 전문성이 요구됨
  • 구형 브라우저(IE)에서 호환이 되지 않아 지원해주지 못하는 동작이 많음

Reference & 읽어보면 좋은 글

Web API

REST API의 모든 스타일을 구현하지 못할 경우는 Web API나 HTTP API라 부른다. Web API를 만들 때에는 몇 가지 중요한 원칙이 있다.

WEB API 디자인 가이드

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

URI는 정보의 자원을 표현해야 한다

  • GET /members 🙆🏻‍♀️
    • 멤버의 모든 정보를 달라는 요청
    • 명사는 보통 집합을 나타내기 때문에 복수형으로 사용하는 것이 좋다.
  • GET /members/delete/1 🙅🏻‍♀️
    • GET은 정보를 요청할 때 사용. 그래서 이렇게 동사로 삭제를 표현하면 안 됨
  • GET /members/get/1 🙅🏻‍♀️
  • GET /members/add 🙅🏻‍♀️
  • GET /members/update/1 🙅🏻‍♀️
  • POST /members 🙆🏻‍♀️
    • members라는 리소스를 생성
  • PUT /members/1 🙆🏻‍♀️
    • members 중에 1에 해당하는 것을 갱신
  • DELETE /members/1 🙆🏻‍♀️
    • 삭제 표현
    • “members 중에 1에 해당하는 것을 삭제해주세요”라는 의미

자원에 대한 행위는 HTTP Method(GET, POST, PUT, DELETE)로 표현한다

HTTP Method 종류

  • POST: POST를 통해 해당 URI를 요청하면 리소스 생성
  • GET: 해당 리소스를 조회하고 도큐먼트에 대한 자세한 정보 가져옴
  • PUT: 리소스 수정
  • DELETE: 리소스 삭제

/ 구분자는 계층을 나타낼 때 사용

  • URI 마지막 문자로 슬래시 구분자(/)를 포함하지 않는다.
  • 하이픈(-)은 URI가독성을 높일 때 사용한다.
  • 언더바(_)는 사용하지 않는다.
  • URI 경로는 소문자만 사용한다.
  • RFC 3986(URI 문법 형식)은 URI 스키마와 호스트를 제외하고는 대소문자를 구별한다.
  • 파일 확장자는 URI에 포함하지 않는다.
  • Accept Header를 사용한다.

Status Code (Success)

  • 200: 클라이언트의 요청을 정상적으로 수행함
  • 201: 클라이언트가 리소스 생성을 요청했고 해당 리소스가 성공적으로 생성됨 (PUT 메소드에 의해 원격지 서버에 파일이 생성된 경우)

Status Code (클라이언트로 인한 오류)

  • 400: 클라이언트의 요청이 부적절한 경우
  • 401: 클라이언트가 인증되지 않은 상태에서 보호된 리소스를 요청한 경우. 예를 들어 로그인 하지 않은 유저가 로그인 하거나 요청 가능한 리소스를 요청했을 때
  • 403: 유저 인증 상태와 관계 없이 응답하고 싶지 않은 리소스를 클라이언트가 요청했을 때 사용. 403 자체가 리소스가 존재한다는 뜻이기 때문에 403보다는 400이나 404를 사용할 것을 권고
  • 405: 클라이언트가 요청한 리소스에는 사용한 불가능한 메소드를 이용한 경우

Status Code (서버로 인한 오류)

  • 301: 클라이언트가 요청한 리소스에 대한 URI가 변경된 경우. 응답 시 Location Header에다가 변경된 URI를 적어줘야 함
  • 500: 서버에 문제가 있는 경우

실습

서블릿을 이용해 Web API를 작성해보자. maven-archetype-webapp 아키타입의 메이븐 프로젝트를 생성한다. artifact id 이름은 webapiexam

프로젝트를 생성하자마자 할 일은 pom.xml에서 JDK 1.8 버전으로 바꿔주는 것이다.

<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.8.0</version>
  <configuration>
    <source>1.8</source>
    <target>1.8</target>
  </configuration>
</plugin>

그리고 Web API를 실행하기 위해서는 몇 가지 라이브러리를 더 추가해줘야 한다.

<!-- 전에 썼던 connectdb 재활용 예정 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.45</version>
</dependency>

<!-- json 라이브러리 databind jackson-core, jackson-annotation에 의존성이 있음 -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.4</version>
</dependency>
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.1.0</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>jstl</artifactId>
  <version>1.2</version>
</dependency>

아직 수정할 것이 하나 더 남았는데, .setting 폴더의 org.eclipse.wst.common.project.facet.core.xml 파일을 열어 jst.web의 버전을 2.3에서 3.1으로 올려준다. 서블릿을 버전을 올려주는 것인데, 적용되기 위해서는 이클립스를 재시작한다.

재시작 후에는 Properties > Project Facets의 Dynamic Web Module이 3.1 버전으로 바뀌었는지 확인한다.

확인 후에는 src > main > webapp > WEB-INF의 web.xml 파일을 연다. 이 예제에서는 annotation을 이용해 서블릿을 설정할 것이기 때문에 web.xml이 필요없으므로 삭제한다.

web.xml 파일을 삭제했다면 pom.xml의 properties에 failOnMissingWebXml이라는 엘리먼트를 추가해야 오류가 발생하지 않는다.

<properties>
  <!-- web.xml 파일을 삭제해도 eclipse에서 오류가 발생하지 않도록 함 -->
  <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>

이제 src > main에 java라는 폴더를 생성한다. 이 폴더에는 자바 패키지와 클래스 등이 저장될 것이다. 그리고 그 안에 kr.or.connect.webapiexam.api라는 이름의 패키지를 생성한다. 생성 후에는 src/main/java에 kr.or.connect.jdbcexam.dao와 kr.or.connect.jdbcexam.dto 패키지를 복붙해준다.

일단은 모든 role의 정보를 보여주는 rolesServlet.java 클래스를 작성해보자.

package kr.or.connect.webapiexam.api;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.databind.ObjectMapper;

import kr.or.connect.jdbcexam.dao.RoleDao;
import kr.or.connect.jdbcexam.dto.Role;

@WebServlet("/roles")
public class RolesServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    public RolesServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setCharacterEncoding("utf-8");
		response.setContentType("application/json");
		
		RoleDao dao = new RoleDao();
		
		List<Role> list = dao.getRoles();
		
		// JSON 사용하려고 추가했던 라이브러리가 제공하는는 객체
		// JSON 문자열로 바꾸거나 JSON 문자열을 객체로 바꿔줌
		ObjectMapper objectMapper=  new ObjectMapper();
		// 파라미터로 list를 넣어주면 list가 JSON 문자로 바뀌어 리턴해줌
		String json = objectMapper.writeValueAsString(list);
		
		PrintWriter out = response.getWriter();
		out.println(json);
		out.close();
	}
}

실행시키면 roles.json 파일이 다운받아진다.

[{"roleId":102,"description":"Project manager"},{"roleId":101,"description":"Researcher"},{"roleId":100,"description":"Developer"}]

setContentType는 클라이언트에게 보낼 때 어떤 형식으로 보낼지를 지정하는데, 이 부분에 오타가 생기거나 제대로 인식할 수 없게 되면 클라이언트는 어떤 방식으로 보여줘야 될 지 결정할 수 없기 때문에 파일 다운로드 형식을 보여준다. IE 버전 8이랑 9에서는 파일 다운로드 형식을 보여주므로 크롬으로 바꿔서 실행시키면 잘 된다.

+)

메이븐이 특정 라이브러리를 제대로 인식하지 못해 Maven Dependencies 폴더 내에 라이브러리들이 다 들어있는데도 import가 제대로 안 된다든지 하는 문제들이 종종 발생한다. C:/User/.m2/repository라는 곳에 사용하는 라이브러리들이 들어있는데, 이 부분을 제대로 못 읽어오는 등의 문제가 발생한 것이다. 그럴 때는 이클립스를 종료한 상태에서 .m2 디렉토리를 삭제하고 이클립스를 켜 실행시킨다.

그럼 이제 id 하나에 대해 role 정보를 읽어오는 서블릿을 작성해보자. url을 /roles/*라 설정했는데, 이는 path가 roles로 시작하지만 / 다음에는 어떤 문자든 모두 올 수 있다는 의미임.

package kr.or.connect.webapiexam.api;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.databind.ObjectMapper;

import kr.or.connect.jdbcexam.dao.RoleDao;
import kr.or.connect.jdbcexam.dto.Role;

@WebServlet("/roles/*")
public class RolebyIdServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    public RolebyIdServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setCharacterEncoding("utf-8");
		response.setContentType("application/json");
		
		String pathInfo = request.getPathInfo(); // /roles/{roleId}
		String[] pathParts = pathInfo.split("/");
		String idStr = pathParts[1];
		int id = Integer.parseInt(idStr);
		
		RoleDao dao = new RoleDao();
		
		Role role = dao.getRole(id);
		
		ObjectMapper objectMapper = new ObjectMapper();
		String json = objectMapper.writeValueAsString(role);
		
		PrintWriter out = response.getWriter();
		out.println(json);
		out.close();
	}
}

실행시키면 상태코드 500 에러가 뜬다. http://localhost:8080/webapiexam/roles/*에서 * 대신 실제 role_id를 넣어줘야 한다..!

role_id로 100을 넣어주면 100번에 해당하는 하나의 role 정보가 보여진다.

그런데 이렇게 서블릿을 이용해 API를 만드는 것은 좀 불편하지 않은가..? 이런 불편함을 해결하기 위해 여러 가지 기술들이 등장했고 그 중 대표적인 기술이 Spring 프레임워크이다 (오오).