Spring

Spring REST - REST Controller

728x90

참조한 강의 : 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

 

MIME type이란?

MIME 이란?Multipurpose Internet Mail Extensions의 약자로 간단히 말하면 파일 변환을 의미한다.현재는 웹을 통해 여러 형태의 파일을 전달하는데 사용하고 있지만 이 용어가 생길 땐 이메일과 함께 동봉

velog.io

 

 

 

- 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 이다.

 

그림으로 나타내면 이런 모양이다.

출처 : https://www.udemy.com/course/spring-hibernate-tutorial/

 

이건 근데 생각해보면, 이전 포스팅에서 다뤘던 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

 

 

 

728x90

'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