참조한 강의 : www.udemy.com/course/spring-hibernate-tutorial/
이번에는 REST Controller 에 대해서 알아본다.
앞선 포스팅에서는 Jackson 을 이용해 자바에서 JSON 파일을 POJO 로 바꾸는 방법에 대해서만 알아봤지,
스프링을 기반으로 REST 의 기능이 들어간 것을 만들지는 않았다
스프링에서 REST 기능을 넣고 싶으면 REST Controller 를 이용해야한다.
그러나, REST Controller 를 알아보기 전에 HTTP 에 대해서 간단하게 개념을 짚고 갈 필요성이 있다.
- HTTP 구조
HTTP 는 HyperText Transfer Protocol 의 약자로, HTML 문서를 교환하기 위해 만들어진 통신 규약이다.
HTTP 는 Request 와 Response 구조로 구성되어 있으며,
클라이언트가 Request 를 보내면, 서버가 요청에 따른 적절한 Response 를 보내는 구조로 되어있다.
1) HTTP Request 의 구조
HTTP Request Message 는 크게 아래와 같이 3개의 부분으로 구성되어있다.
1-1) start line
: start line 에는 HTTP Method, Request Target, HTTP Version 으로 구성되어 있는데 예를들면 아래와 같다
예 : GET /api/students HTTP/1.1
1-2) headers
: header 에는 Request 에 대한 추가 정보를 담고 있는 부분이다.
예를들면 아래와 같은 정보들이 해당한다
쿠키값이 뭐가 들어가있고, 어떤 타입의 데이터 형식을 받아올 수 있는지 등등 이 적혀있는것을 볼 수 있다.
이 header 부분은 key : value 형식으로 값이 저장되어 있으며
자주 쓰이는 header 정보는 아래 같은것들이 있다.
Host : target url 의 host url (예를들면, google.com, tistory.com 같은것을 말한다)
User-Agent : 요청을 보내는 클라이언트의 정보 (클라이언트가 쓰는 웹브라우저가 무엇인지 등이 적혀있다)
Accept : 해당 요청이 받을 수 있는 response 의 데이터 타입을 말한다
Connection: 해당 요청이 끝난후에, 클라이언트와 서버가 계속 연결을 지속할건지 아닐건지를 알려준다
Content-Type : 해당 요청이 보내는 메세지의 body 타입
Content-Length : 메시지 body 의 길이
1-3) Body
해당 request 의 실제 메시지 나 내용 같은게 들어있는 부분이다.
body 값이 아예 없는 request 도 있긴하다
2) HTTP Response 의 구조
: Response 의 구조도 거의 Request 와 흡사하다 (마찬가지로, 3개의 부분으로 나뉜다)
2-1) Status Line
: Response 의 상태를 나타내는 부분으로
예를들면 이런식으로 되어 있다
HTTP/1.1 404 Not Found
: http 버전, status code, status text 로 구성됨
2-2, 3) Headers, Body
: Request 와 거의 똑같다
* 참고 : MIME Type
: 본래 HTTP 는 HTML 파일을 전송하는게 주 목적이었는데 (즉, 텍스트 파일을 전송하는게 목적) 웹이 발달하면서 텍스트 파일 뿐 아니라, 다른 타입의 데이터도 전송할 필요성이 생겼다.
이때 사용되는게 MIME (Multipurpose Internet Mail Extensions) 이며 이 MIME 에 대한 자세한 설명은 아래 참조
velog.io/@aerirang647/MIME-type%EC%9D%B4%EB%9E%80
- REST Controller
Spring 에서 제공하는 REST 는 Spring Web MVC 를 기반으로 제공되는데,
REST 기능을 구현하기 위해 쓰이는 어노테이션인 @RestController 의 경우
Spring MVC 가 제공하는 @Controller 의 확장판으로 볼 수 있다.
(그래서, maven 프로젝트에 의존성 주입할때, REST 를 위한 별도의 의존성을 주입해주는게 아니라
기존에 썼던 spring-webmvc 를 의존성 주입해주면 거기에 RestController 도 같이 딸려온다)
REST Controller 에 대한 기본 사용법을 아래의 몇가지 예제를 통해서 확인해보자
* 예제 1)
간단하게 @RestController 를 이용해서 Hello World 를 리턴값으로 보여주는 방법을 알아보자
먼저 pom.xml 에 의존성 주입을 해준다
- pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
|
cs |
다음으로 이 예제에서는 별도의 xml 파일에 스프링 환경설정을 하지 않고 오로지 자바 파일과 어노테이션만으로 설정할 것이기 때문에, 스프링 설정을 위한 자바 파일을 만든다
- DemoAppConfig.java
@Configuration
@EnableWebMvc
@ComponentScan("스캔할 ")
public class DemoAppConfig {
}
|
cs |
다음으로 MVC 기반의 프로젝트 이므로, Dispatcher Servlet 에 대한 초기화 작업이 필요하다
- SpringMvcDispatcherServletInitializer.java
public class SpringMvcDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { DemoAppConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
|
cs |
마지막으로 url 을 처리할 RestController 를 만든다
- DemoRestController.java
@RestController
@RequestMapping("/test")
public class DemoRestController {
@GetMapping("/hello")
public String sayHello() {
return "Hello World";
}
}
|
cs |
이를 실행하기 위해 Postman 을 이용해서 결과값을 확인하면 아래와 같다
* 예제 2)
이번에는 단순히 hello world 라는 글자만 가져오는것 말고, 학생들 정보 전체를 조회하는 GET method 를 작성해보자
url 구성은 /api/students 로 구성할 것이다.
먼저 간단하게 학생들에 대한 정보를 List 에 담아서 저장한뒤, 넘겨주는 방법을 생각해보자
이를 위해서 아래와 같이 Student 클래스를 만들어준다
- Student.java
public class Student {
private String firstName;
private String lastName;
// getter, setter, constructor 생략
}
|
cs |
다음으로 학생 정보를 조회하기 위한 RestController 를 만든다
- StudentRestController.java
@RestController
@RequestMapping("/api")
public class StudentRestController {
@GetMapping("/students")
public List<Student> getStudents() {
List<Student> students = new ArrayList<>();
students.add(new Student("jackson", "lee"));
students.add(new Student("mario", "kim"));
students.add(new Student("sonic", "joo"));
return students;
}
}
|
cs |
여기까지 작성한 뒤, postman 으로 실행을 시켜보면 아래와 같이 학생 리스트를 JSON 으로 보여주는것을 볼 수 있다.
그러나 위와 같이 RestController 를 구성하는 것은 좋지 못한 습관이다.
왜냐면 매번 클라이언트로 부터 요청을 받을 때마다, 매번 새로운 List 를 생성하기 때문이다.
메모리 낭비가 상당히 심해질 수 있는 코드이다.
이 부분은 밑의 예제 3) 에서 고쳐보도록 한다.
* 예제 3)
예제 2 처럼 전체 학생 정보를 얻어오는 것 말고, 특정한 학생만 조회하고 싶을땐 어떻게 할까?
이때는 @PathVariable 이라는 어노테이션을 이용한다
그리고 url 구성도 다음과 같이 되어야한다
/api/students/{studentId}
그리고 예제 2의 코드는 앞에서도 말했듯, 좋지 못한 코드 구조를 갖고 있다.
그러므로 다음과 같이 리팩토링 작업을 해주자
- StudentRestController.java
@RestController
@RequestMapping("/api")
public class StudentRestController {
private List<Student> students;
@PostConstruct
public void loadData() {
students = new ArrayList<>();
students.add(new Student("jackson", "lee"));
students.add(new Student("mario", "kim"));
students.add(new Student("sonic", "joo"));
}
@GetMapping("/students")
public List<Student> getStudents() {
return students;
}
@GetMapping("/students/{studentId}")
public Student getStudent(@PathVariable int studentId) {
return students.get(studentId);
}
}
|
cs |
해결책은 바로 위에 처럼 @PostConstruct 어노테이션을 써주는것이다.
@PostConstruct 는 StudentRestController 빈 인스턴스가 생성된 직후, (즉, 생성자가 호출된 직후)
단 한번만 수행되는 메소드를 정의할때 쓰는 어노테이션으로, 초기화 작업을 할때 유용하게 쓰인다.
이렇게 처음 Bean 이 생성되고 나서 정해지고, students 변수를 컨트롤러 클래스의 필드 변수로 선언했기 때문에
싱글톤 패턴을 따르는 구조로 리팩토링 되었다.
이제 테스트를 해보면 아래와 같이 잘 나오는 것을 볼 수 있다
그러나 이 코드 역시 또 다른 문제점을 포함하고 있다.
만약 path variable 값으로 1, 2, 3 이 아니라 이보다 더 크거나 작은 값이 들어가면 어떻게 될까
그러면 당연히 out of index 에러가 날 것이다. (500 Internal Server Error)
그러면 숫자가 아닌 다른 문자를 넣으면?
이때는 400 Bad Request 에러가 날 것이다.
즉, 위의 코드는 예외 처리에 대한 내용이 없는 것이다.
* 예제 4)
Apache Server 가 제공하는 못생긴 에러 메시지 화면 대신에, 자체적으로 만든 에러 JSON 값을 넘겨주는 작업을 해보자
먼저 Error JSON 값을 넘겨주기 위해서는 클래스를 하나 정의해야한다
(Error 메시지를 보내주기 위한 POJO 를 만들겠다는 의미)
- StudentErrorResponse.java
public class StudentErrorResponse {
private int status;
private String message;
private long timeStamp;
// constructor, getter, setter 생략...
}
|
cs |
다음으로 예외처리를 위한 Exception 클래스를 하나 만들어야 한다
- StudentNotFoundException.java
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(String message, Throwable cause) {
super(message, cause);
}
public StudentNotFoundException(String message) {
super(message);
}
public StudentNotFoundException(Throwable cause) {
super(cause);
}
}
|
cs |
: 여기서 의문점이 하나 생길 수 있다.
왜 여러개의 생성자를 만들었는지에 대한 의문이 생길 수 있다.
사실 저렇게 3개 전부를 다 써주는것이 의무는 아니다.
그리고 상위 클래스인 RuntimeException 으로 부터 상속 받아서 만들수 있는 생성자는 하나 또 있다.
즉, 총 4개가 있다는 소리인데,
4개를 전부 다 써줄 필요도 없고, 꼭 3개를 써야 하는 것도 아니다.
다만 개발 편의성을 위해서 3개의 생성자를 만든 것이다.
다음으로 RestController 에 예외처리 구문이 들어가도록 리팩토링 한다.
- StudentRestController.java
@RestController
@RequestMapping("/api")
public class StudentRestController {
// 나머지 생략...
@GetMapping("/students/{studentId}")
public Student getStudent(@PathVariable int studentId) {
if (studentId >= students.size() || studentId < 0) {
throw new StudentNotFoundException("Student id not found - " + studentId);
}
return students.get(studentId);
}
@ExceptionHandler
public ResponseEntity<StudentErrorResponse> handleException(StudentNotFoundException e) {
StudentErrorResponse error = new StudentErrorResponse();
error.setStatus(HttpStatus.NOT_FOUND.value());
error.setMessage(e.getMessage());
error.setTimeStamp(System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); // body, status code
}
}
|
cs |
@ExceptionHandler 는 Spring MVC 에서 예외처리를 하기 위해 사용되는 어노테이션으로,
ResponseEntity 를 리턴하는 어노테이션이다.
ResponseEntity 란 이름에서 짐작 가능하듯, HTTP Response 를 담는 객체 타입을 의미한다.
@ExceptionHandler 어노테이션을 통해서 예외처리 메소드를 정의할때는
메소드의 리턴타입을 ResponseEntity<T> 로 정의하고
T 에는 앞서 예외처리 JSON 값을 보내기 위해 만든 StudentErrorResponse 처럼 예외처리 내용을 전달하는데 쓰일 POJO 타입이 들어간다
그리고 이 메소드의 매개변수 값은 예외처리를 위해 RuntimeException 을 상속 받은 클래스가 들어간다.
(즉, StudentErrorResponse 는 HTTP Response 의 Body 부분에 해당하고, StudentNotFoundException 은 런타임시에 생긴 이 학생을 찾지 못해서 생기는 오류 처리를 위한 클래스인것)
그리고 HttpStatus.NOT_FOUND 는 그냥 딱 이름에서 알 수 있듯이 HTTP Status code 를 의미한다.
여기 까지해서 테스트를 해보면, 정수 값에 대해서는 처리가 된다.
근데, 다른 자료형에 대해서는 처리가 되지 않는다.
그러면 한가지 생각을 해보자
지금 위에 쓴 코드는 404 에러를 처리하기 위해 만든 코드이고, StudentRestController 라는 Rest Controller 에 정의한 예외처리 메소드를 기반으로 실행되었다.
만약 프로젝트의 규모가 커져서 /api/students 뿐 아니라 /api/professor , /api/staffs 등 다양해지면
ProfessorRestController 에 따로 예외처리 메소드 써주고, 또 다른 rest controller 에 예외처리 메소드를 작성해주고 .....
이렇게 작성하는 것은 너무 비효율적이다.
또한 예외사항이 404 에러만 있는건 아니다. 400 에러도 있고 또 다른 에러 코드도 있다.
그때마다 매번 서로다른 rest controller 에 예외처리 메소드를 작성할 수는 없는 것이다.
그래서 예외처리 부분만 따로 떼서 마치 전역변수 처럼 사용하게 만들어야 한다.
* 예제 5)
: 예외처리 구문만 전역 변수처럼 쓰기 위해서는 @ControllerAdvice 라는 어노테이션을 써야한다
여러개의 Rest Controller 에 직접 Request 가 닿는게 아니라, 사전에 미리 값에 대해서 검증을 하고 컨트롤러로 넘겨주고, 혹은 컨트롤러에서 작업한 이후 오류 사항이 있었는지 체크하기 위한 후처리 작업이 필요하다
그때 사용되는게 바로 @ControllerAdvice 이다.
그림으로 나타내면 이런 모양이다.
이건 근데 생각해보면, 이전 포스팅에서 다뤘던 AOP인 것을 알 수 있다.
예외처리라는 공통된 부분을 따로 Controller Advice 로 묶어서 처리 하기 때문이다.
(AOP 참조 : sdy-study.tistory.com/213)
그래서, RestController 에 작성한 예외처리 메소드를 제거하고
아래 처럼 별도의 @ControllerAdvice 를 만들어 주자
- StudentRestExceptionHandler.java
@ControllerAdvice
public class StudentRestExceptionHandler {
@ExceptionHandler
public ResponseEntity<StudentErrorResponse> handleException(StudentNotFoundException e) {
StudentErrorResponse error = new StudentErrorResponse();
error.setStatus(HttpStatus.NOT_FOUND.value());
error.setMessage("찾을 수 없는 요청 입니다, 유효한 값을 입력해주세요");
error.setTimeStamp(System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); // body, status code
}
@ExceptionHandler
public ResponseEntity<StudentErrorResponse> handleException(Exception e) {
StudentErrorResponse error = new StudentErrorResponse();
error.setStatus(HttpStatus.BAD_REQUEST.value());
error.setMessage("잘못된 요청입니다, 유효한 값을 입력해주세요");
error.setTimeStamp(System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); // body, status code
}
}
|
cs |
위는 404 에러, 아래는 400 에러 이다.
* REST API Design 에 대한 짧은 고찰
: 여기까지 쭉 Spring REST 에 대한 간단한 튜토리얼 정도로 살펴봤는데, 한가지 더 생각해볼만한 주제가 있다.
바로, REST API 작성시에, url 디자인을 어떻게 정할 것인가 이다.
아래의 두 url 묶음들 중 뭐가 더 적절한 url 일까?
- 1)
/api/customers
/api/customers/1
- 2)
/api/customerList
/api/addCustomer
큰 기업들이 제공하는 서비스의 REST API 를 살펴보면 대략 유추 할 수 있다.
1) Github
: docs.github.com/en/rest/reference/repos#repositories
위의 주소는 github 가 제공하는 repo 관련 api 문서로
아래 그림과 같이 전부 일정한 url 구조를 보여주고 있다.
/orgs/{org}/repos 이런식으로 일정한 구조를 갖고 있다.
2) Paypal
: developer.paypal.com/docs/api/invoicing/v2/
다음은 paypal 이 제공하는 api 인데 여기서도 어떤 일정한 구조를 갖고 url 이 구성되어 있다
하나 확실한 것은
/api/customerList
/api/addCustomer
이런식의 url 구조는 갖지 않는 다는 것이다.
더 자세히 말하면, url 에 행위에 대한 내용은 작성하지 않는다 이다.
addCustomer 같이 쓰는게 아니라
그냥 /api/customers 라고 하고 위의 paypal 예제 처럼 행위에 대한 부분은 HTTP Method 로만 표현하는 것이다.
또한 가급적이면, url 에 복수 명사로 쓰는 것을 권장한다
/api/customerList 가 아니라
/api/customers 처럼 쓰는게 적절하다.
위의 깃헙 이나 페이팔도 /orgs/{org}/repos , /v2/invoicing/invoices 같이 복수 명사로 표현하지
customerList 이런식으로 표현하지 않는다는 뜻이다.
따라서 url 을 구성할때, 가급적이면 명사로 표현해 놓고, 동사 같이 행위를 나타내는 어휘는 url 에 작성하는것을 지양하는게 좋다.
- References)
1. HTTP Structure : velog.io/@teddybearjung/HTTP-%EA%B5%AC%EC%A1%B0-%EB%B0%8F-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%86%8C
2. MIME Type : velog.io/@aerirang647/MIME-type%EC%9D%B4%EB%9E%80
'Spring' 카테고리의 다른 글
Spring Boot - Overview (0) | 2021.03.22 |
---|---|
Intellij 에 Spring Framework 설정하기 (0) | 2021.03.14 |
Spring REST - Overview (0) | 2021.03.07 |
Spring Security - User Registration (0) | 2021.03.07 |
Spring Security - User Roles (0) | 2021.03.06 |