RestAPI

API를 공부하고 난 이후에 봐야합니다. 사실 Rest API를 공부하기 전에 SOAP API를 공부하는 것이 더 좋을 것 같지만 여기서 대충 역사를 설명하자면

기존에는 사람이 읽는 문서만 주고받었는데 시간이 지날수록 서버-클라이언트가 데이터를 주고 받을 일이 많아지게 됩니다.

어떤 누군가가 네트워크 망을 새롭게 만들면 번거로움 -> 기존 HTTP랑 HTML을 이용해서 Object-Model? 이런 생각을 했는데

2000년, Microsoft에서 XML-RPC 문법을 발표 -> 이후 SOAP(Simple Object Access Protocal)로 개명했습니다.

후에 Roy T Fielding이라는 사람이 설계한 새로운 아키텍처인 REST가 나왔는데 초창기에는 주목받지 못 했으나 SOAP방식이 지저분하기 때문에 많은 사람들이 REST를 선택합니다.

RestAPI란

REST API : 클라이언트와 서버 간의 두 컴퓨터 시스템이 인터넷을 통해 정보를 안전하게 교환하기 위해 사용하는 인터페이스

myAvatar

REST(Representational State Transfer)의 약자

  1. 자원을 이름으로 구분

자원 = 문서, 사진, 그림, 데이터 등 소프트웨어가 관리하는 모든 것을 HTTP URI(Uniform Resource Identifier)를 통해 명시

  1. 해당 자원의 상태를 주고받는 모든 것을 의미 (요청 -> 응답)

클라이언트는 데이터가 요청되어지는 시점에서 자원의 상태(정보= payload)를 전달한다.

Client가 자원의 상태(정보)에 대한 조작을 요청하면 Server는 이에 적절한 응답(Representation)을 보낸다. => 전달 방식으로는 JSON 혹은 XML를 통해 데이터를 주고 받는 것이 일반적

‘HTTP URI(Uniform Resource Identifier)’를 통해 자원(Resource)을 명시하고,

‘HTTP Method(POST, GET, PUT, DELETE, PATCH 등)’를 통해 해당 자원(URI)에 대한 ‘CRUD Operation’을 적용


CRUD Operation 설명 (HTTP Method)
CREATE 데이터 생성(POST)
READ 데이터 조회(GET)
UPDATE 데이터 수정(PUT, PATCH)
DELETE 데이터 삭제(DELETE)

REST의 구성 요소

  1. 자원(Resource) : HTTP URI
  • 모든 자원에 고유한 ID가 존재하고, 이 자원은 Server에 존재한다.
  • 자원을 구별하는 ID는 ‘/groups/:group_id’와 같은 HTTP URI Client는 URI를 이용해서 자원을 지정하고 해당 자원의 상태(정보)에 대한 조작을 Server에 요청한다.
  1. 행위(Verb): HTTP Method

HTTP 프로토콜의 Method를 사용한다. HTTP 프로토콜은 GET, POST, PUT, DELETE 와 같은 메서드를 제공한다.

  1. 표현(Representation of Resource)

Client가 자원의 상태(정보)에 대한 조작을 요청하면 Server는 이에 적절한 응답(Representation)을 보낸다. REST에서 하나의 자원은 JSON, XML, TEXT, RSS 등 여러 형태의 응답을 받을 수 있다. JSON 혹은 XML를 통해 데이터를 주고 받는 것이 일반적이다.

REST API 동작

{JSON} Placeholder 사이트에 접속하면 각종 테스트를 해볼 수 있다.

아래는 사이트 화면이다.

json placeholder

초반에 나오는 예시 코드를 실행또한 가능하다.

1
2
3
4
# Getting a resource
fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(response => response.json())
      .then(json => console.log(json))

결과

1
2
3
4
5
6
{
  id: 1,
  title: '...',
  body: '...',
  userId: 1
}

이제는 실습을 해볼 차례 인데 python의 RestAPI 구현은 다음과 URL을 통해 이동해서 확인할 수 있다.

URL : https://probationer070.github.io/python/2024/05/11/Python-RestAPI/

실습을 위해선 Postman [ 앱 ]이나 Talend API Tester [ 확장 프로그램 ] 같은 보조 도구 설치가 필요하다.

Postman

Postman

talend API tester

talend API tester

  • HTTP 상태 코드
상태코드 설명
1XX(정보) 요청이 수신돼 처리 중입니다.
2XX(성공) 요청이 정상적으로 처리됐습니다.
3XX(리다이렉션 메세지) 요청을 완료하려면 추가 행동이 필요합니다.
4XX(클라이언트 요청 오류) 클라이언트의 요청이 잘못돼 서버가 요청을 수행할 수 없습니다.
5XX(서버 응답 오류) 서버 내부에 에러 발생해 클라이언트 요청에 대해 적절히 수행하지 못했습니다.
  • 404 Error 예시

talend API tester

REST API 설계 규칙

1. 슬래시 구분자(/ )는 계층 관계를 나타내는데 사용

http://restapi.example.com/houses/apartments

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

  • REST API는 분명한 URI를 만들어 통신을 해야 하기 때문에 혼동을 주지 않도록 URI 경로의 마지막에는 슬래시(/)를 사용하지 않는다.

http://example.com/posts/ (X)

3. 하이픈(-)은 URI 가독성을 높이는데 사용

  • 불가피하게 긴 URI경로를 사용하게 된다면 하이픈을 사용해 가독성을 높인다.

4. 밑줄(_)은 URI에 사용하지 않는다.

  • 밑줄은 보기 어렵거나 밑줄 때문에 문자가 가려지기도 하므로 가독성을 위해 밑줄은 사용하지 않는다.

5. URI 경로에는 소문자가 적합하다. 대문자 사용은 피하도록 한다.

  • RFC 3986(URI 문법 형식)은 URI 스키마와 호스트를 제외하고는 대소문자를 구별하도록 규정하기 때문 파일확장자는 URI에 포함하지 않는다.

6. REST API에서는 메시지 body 내용의 포맷을 나타내기 위한 파일 확장자를 URI 안에 포함시키지 않는다. Accept header를 사용한다.

` http://restapi.example.com/members/345/photo.jpg (X) GET / members/345/photo HTTP/1.1 Host: restapi.example.com Accept: image/jpg (O) `

7. 리소스 간에는 연관 관계가 있는 경우

=> /리소스명/리소스 ID/관계가 있는 다른 리소스명

GET : /users/{userid}/devices (일반적으로 소유 ‘has’의 관계를 표현할 때)

referrence : jiwondev



RESTAPI 구현 (feat. JAVA)

REST 컨트롤러

1
2
3
4
5
6
7
@RestController
public class FirstApiController {
    @GetMapping("/api/hello") # URL 요청 접수
    public String hello() {   # hello world! 문자열 반환
        return "hello world!";
    }
}
1
2
3
Response | raw

hello world!

일반 컨트롤러

1
2
3
4
5
6
7
8
@Controller
public class FirstController {
    @GetMapping("/hi")
    public String niceToMeetYou(Model model) {
        model.addAttribute("username", "hongpark");
        return "greetings";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
Response | raw

<!doctype html>
<html lang="en">
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap CSS -->
  <link>
  ...

(a) REST 컨트롤러 : 데이터 반환 (b) 일반 컨트롤러 : 뷰페이지 반환

구현하기 (GET, POST, PATCH, DELETE)

일반적으로 웹을 처음 입문할 때 MVC구조니 3-tier Layer Architecture라느니 하는 것들을 모르는 채로 개발을 시작하려고 하면 파일구조가 어떻게 돼야하는지 모르는 경우가 허다하다. 그렇기에 아래 대충 구조도를 그려놓겠습니다.

물론 Spring Tool Suite 같은 IDE를 사용하면 자동으로 만들어주는 부분도 있지만 한번도 해보적인 없다면 구조가 너무 뒤죽박죽인것 같아서 나중에 바꾸고 싶어도 귀찮아진다. 그렇기 때문에 파일구조를 우선 제대로 만들어놓는게 맘에 편합니다.(물론, Service와 ServiceImpl처럼 굳이 세세하게 나눌 필요까지는 없습니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ProjectName/
    │
    ├── gradle/wrapper
    ├── src/
    │   ├── main/
    │   │   ├── java/com/example/ProjectName/
    │   │   │   ├── api/
    │   │   │   ├── controller/
    │   │   │   ├── dto/
    │   │   │   ├── entity/
    │   │   │   ├── repository/
    │   │   │   ├── service/
    │   │   │   └── ProjectnameApplication.java
    │   │   │   
    │   │   └── resources/
    │   │       ├── META-INF/
    │   │       │    ├── orm.xml  # Java 설정을 담고 있다.
    │   │       ├── static      # html, css, img 파일을 담는다.
    │   │       ├── templates   # 템플릿 - thymeleaf, mustache 같은 템플릿 엔진을 사용할때 생성
    │   │       └── application.properties    # 주로 Spring의 추가설정을 할 때 사용
    │   │       
    │   └── test/java/com/example/ProjectName # 해당 부분은 JUnit같은 테스트를 만들때 사용
    │
    ├── .gitignore
    ├── build.gradle
    ├── gradlew
    ├── gradlew.bat
    └── settings.gradle

위에 파일 구조를 보면 META-INF라는 파일이 있는데 이를 설명이 길 것 같아 아래와 같이 남긴다. META-INF 말고도 WEB-INF라는 것도 보게 될 텐데 설명을 아래 작성했다.

간혹 프로그래밍하다보면 아래와 같은 에러가 나는데

1
"Path with "WEB-INF" or "META-INF":

jsp 파일을 찾을 수 없다고 하는데, tomcat에는 jsp를 처리하는 서블릿이 없으니 build.gradle이나 pom.xml에 jsp 처리하는 서블릿을 아래와 같이 추가해줘야 한다.

  • Gradle : build.gradle

    1
      implementation "org.apache.tomcat.embed:tomcat-embed-jasper"
    
  • maven : pom.xml

    1
    2
    3
    4
    5
      <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <scope>provided</scope>
      </dependency>
    

WEB-INF와 META-INF 차이

WEB-INF

  • Web Information의 약자

  • WEB-INF폴더에는 브라우저에서 직접 접근할 수 없고 오직 서버내에서만 접근이 가능합니다. (중요한 파일들이 노출되지 않도록 만든 폴더)

META-INF

  • Java에서 설정관련 파일을 저장하는 폴더입니다.

  • Java 패키징 기술인 jar의 일부입니다.

  • jar 파일들을 풀어보면 META-INF 폴더 아래 MANIFEST.MF 라는 파일이 있습니다.

입문자라면 금방 넘어가도 좋지만 배워둬서 나쁠 것 없습니다.

META-INF는 자바 패키징 기술인 jar의 일부입니다. jar는 기본적으로 파일 포맷이 zip과 동일하지만 zip외에 부가적인 규약이 정해져 있고, 그 중 하나가 META-INF 디렉토리와 그 속 몇몇 파일의 포멧과 용도입니다.

반면에

WEB-INF는 웹 애플리케이션 용으로 (servlet 규격으로) 따로 만들어진 디렉터리죠. 스프링 설정 파일을 META-INF에 두는 이유는 해당 애플리케이션을 war가 아닌 jar로 패키징해서 배포할 수 있기 때문입니다. 보통 애플리케이션을 다중 계층 구조로 만들면서 계층별로 별도로 패키징할 계획이라면, 웹과 관련 없는 계층의 메타 데이터는 WEB-INF가 아닌 META-INF에 두는 것이 맞습니다.

설명이 길었는데 아무튼 실습을 해봐야하니 아래 코드는 GET, POST, PATCH, DELETE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Slf4j
@RestController
public class ArticleApiController {
    
  @Autowired  // 게시글 Repo 주입 (DI)
  private ArticleRepository articleRepository;

  // GET (전체 게시글 조회)
  @GetMapping("/api/articles")
  public List<Article> index() {
    return (List<Article>) articleRepository.findAll();
  }


  // GET (단일 게시글 조회) 
	@GetMapping("/api/articles/{id}")
	public Article show(@PathVariable("id") Long id) {
		return articleRepository.findById(id).orElse(null);
	}

	// POST
	@PostMapping("/api/articles")
	public Article create(@RequestBody ArticleForm dto) {
		Article article = dto.toEntity();
		return articleRepository.save(article);
	}

	// PATCH
	@PatchMapping("/api/articles/{id}")
	public ResponseEntity<Article> update(@PathVariable("id") Long id,
						  @RequestBody ArticleForm dto) {
		// DTO -> Entity 변환
		Article article = dto.toEntity();
		logger.info("id: {}, article: {}", id, article.toString());
		// target 조회
		Article target = articleRepository.findById(id).orElse(null);
		// 잘못된 요청 처리
		if (target == null || id != article.getId()) {
			logger.info("잘못된 요청! id: {}, article: {}", id, article.toString());
			return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
		}
		// update 및 정상 응답(200)으로 출력
		target.patch(article);
		Article updated = articleRepository.save(article);
		return ResponseEntity.status(HttpStatus.OK).body(updated);
	}

	// DELETE
	@DeleteMapping("api/articles/{id}")
	public ResponseEntity<Article> delete(@PathVariable("id") Long id) {
		// 대상 찾기
		Article target = articleRepository.findById(id).orElse(null);
		// 잘못된 요청 처리
		if (target == null) {
			return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
		}
		// 대상 삭제
		articleRepository.delete(target);
		return ResponseEntity.status(HttpStatus.OK).build();
	}
}

실제로 수행한 코드는 다음과 같이 남기겠다. 하지만 코드는 build.gradle이 가장 먼저 띄워질 예정이다

https://github.com/probationer070/jsp_learning/blob/Testo/WorkSpace6Spring/build.gradle