[Spring] Spring REST
[Spring] Spring REST
REST API
- 클라이언트-서버간 데이터를 주고받기 위한 Architecture
- 제공할 데이터를 resource로 추출하고 resource에 대한 CRUD를 제공하기 위해 REST API 사용
- CRUD : Create, Read, Update, Delete
ROA(Resource Oriented Architecture)
- RESTful Web 구축을 위한 architecture 정의
1. 리소스 제공
- web에서 클라이언트에게 제공할 데이터를 resource로 공개
2. URI를 통한 resource 식별
- resource에 uri를 할당해 resource 접근 허용
3. HTTP 메소드를 통한 CRUD
- GET : Read
- POST : Create
- PUT : Update
- DELETE : Delete
4. 포맷
- Json 포맷이나 xml 포맷 사용
5. 응답 상태 코드
- 1xx : 처리 중임을 알리는 상태 코드
- 2xx : 처리 완료를 알리는 상태 코드
- 3xx : 추가적인 처리가 필요함을 알리는 상태 코드
- 4xx : 클라이언트 측 요청의 문제가 있음을 알리는 상태 코드
- 5xx : 서버 측 처리에 문제가 있음을 알리는 상태 코드
6. Stateless 통신
- 서버가 세션을 생성하지 않고 데이터만으로 resource 조작
7. HATEOAS
- resource에 사용 가능한 uri를 전달하는 방식
- 클라이언트가 미리 uri를 알 필요가 없음 → 클라이언트-서버 결합도 낮춤
Spring Rest Api
- View를 사용하지 않고 HttpMessageConverter를 통해 request를 해석하고 response를 생성
1. HttpMessageConverter
- Request body를 Java 객체로 변환하고 Java 객체를 response body로 변환
2. Resource 클래스
- Java 클래스 형태로 생성
1 2 3 4 5 6 | public class TestResource { private String id; private String msg; } | cs |
Rest Controller
- Spring에서 제공하는 @RestController 사용
- @Controller와 @ResponseBody를 사용할 수도 있음
설정
- pom.xml 추가
1 2 3 4 5 6 7 8 9 10 | <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.9.6</version> </dependency> | cs |
- json<->객체로 변환하기 위해 추가(databind)
- LocalDate를 json으로 사용하기 위해 추가(jsr310)
- json 데이터에 대해 405 error가 뜨는 경우.
1 2 3 4 5 | <properties> <java-version>11</java-version> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> </properties> | cs |
- properties에 java version, source, target을 맞춰주면 해결
기능
1. 선언형
- 요청 매핑
- 요청 데이터 취득
- 입력 값 검증
2. 프로그래밍형
- 비즈니스 로직 호출
- Response 반환
Controller와 차이점
- 요청, 응답 데이터는 HttpMessageConverter를 통해 획득, 반환
- 입력 값 검증 예외는 예외 핸들러에서 공통으로 수행
→ Handler인자로 BindingResult를 받으면, 해당 핸들러안으로 들어와서 로직 수행
RestController CRUD Example
1. Model(TestModel)
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 | public class TestModel { private Long id; private String name; @JsonFormat(pattern = "yyyyMMdd") private LocalDate localDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public LocalDate getLocalDate() { return localDate; } public void setLocalDate(LocalDate localDate) { this.localDate = localDate; } } | cs |
- Long형 id와 String형 name을 갖는 class 생성
- 날짜를 기록할 수 있는 LocalDate 변수 생성
- @JsonFormat을 사용하여 "yyyyMMdd"형식으로 된 값만 받음
2. Create(POST)
1 2 3 4 5 6 7 8 9 10 11 12 | private Map<Long, TestModel> infoList = new HashMap<>(); @RequestMapping(value = "/", method = RequestMethod.POST) public void createInfo(@Validated @RequestBody TestModel testModel) { if(testModel.getId() <= 0) { throw new TestException(HttpStatus.BAD_REQUEST, "잘못된 ID 입니다."); } if(infoList.get(testModel.getId()) != null) { throw new TestException(HttpStatus.CONFLICT, "이미 존재하는 ID 입니다."); } infoList.put(testModel.getId(),testModel); } | cs |
- 해당 URI로 POST 요청 시, Create 수행
- 값을 저장할 Map을 생성
- POST 요청이 올 시, Map에 값을 저장
- 추가하려는 id가 존재할 경우, exception을 throw
- 잘못된 id가 들어올 경우, exception을 throw
- 결과(성공)
- 결과(실패)
3. Read(GET)
1) 전체 읽어오기
1 2 3 4 | @RequestMapping(value = "/", method = RequestMethod.GET) public List<TestModel> getAllInfo() { return new ArrayList<>(infoList.values()); } | cs |
- 해당 URI로 GET요청을 보낼 시, Map을 List로 바꿔 전달(Read)
- 결과
2) 특정 id 정보 읽어오기
1 2 3 4 5 6 7 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) public TestModel getInfo(@PathVariable Long id) { if(infoList.get(id) == null) { throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다."); } return infoList.get(id); } | cs |
- 해당 URI로 GET 요청 시, Read 수행
- @PathVariable을 사용하여 id 값을 받음
- map에 해당 id가 존재하지 않을 경우 exception을 throw
- 있다면, 해당 id를 가진 객체 정보를 return
- 결과(성공)
- 결과(실패)
4. Update(PUT)
1 2 3 4 5 6 7 | @RequestMapping(value = "/", method = RequestMethod.PUT) public void updateInfo(@Validated @RequestBody TestModel testModel) { if(infoList.get(testModel.getId()) == null) { throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다."); } infoList.put(testModel.getId(),testModel); } | cs |
- 해당 URI로 PUT 요청 시, Update 수행
- map에 해당 id가 존재하지 않을 경우, exception을 throw
- 존재한다면 해당 id의 정보를 변경
- 결과(성공)
- 결과(실패)
5. Delete(DELETE)
1 2 3 4 5 6 7 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public void deleteInfo(@PathVariable Long id) { if(infoList.get(id) == null){ throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다."); } infoList.remove(id); } | cs |
- 해당 URI로 DELETE 요청 시, delete 수행
- @PathVariable을 사용하여 id 값을 받음
- map에 해당 id가 존재하지 않을 경우, exception을 throw
- 존재한다면 해당 id의 정보를 삭제
- 결과(성공)
- 결과(실패)
6. Exception 처리
1) TestException
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class TestException extends RuntimeException { private HttpStatus httpStatus; private String msg; public HttpStatus getHttpStatus() { return httpStatus; } public void setHttpStatus(HttpStatus httpStatus) { this.httpStatus = httpStatus; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public TestException(HttpStatus httpStatus, String msg) { this.msg = msg; this.httpStatus = httpStatus; } } | cs |
- 예외 처리를 위해 RuntimeException을 상속받아 생성
- HttpStatus와 msg를 가진 클래스 생성
2) TestExceptionHandler
1 2 3 4 5 6 7 8 | @RestControllerAdvice public class TestExceptionHandler { @ExceptionHandler(value = TestException.class) public ResponseEntity handleException(TestException e) { return ResponseEntity.status(e.getHttpStatus()) .contentType(MediaType.APPLICATION_JSON_UTF8).body(e.getMsg()); } } | cs |
- @RestControllerAdivce를 사용하여 전역에서 처리할 Exception Handler 지정
- @ExceptionHandler를 통해 TestException이 발생하는 경우, 해당 handler에서 모두 잡음
- @ExceptionHandler의 value에 해당 핸들러에서 처리하고 싶은 Exception class를 지정
- 전달된 Exception의 Httpstatus와 msg를 전달
- 일반 String으로 body가 전달되기 때문에, 한글이 깨져 content-type에 "application/json;charset=UTF-8" 추가
Controller Code
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 | @RestController @RequestMapping(value = "/test") public class TestController { private Map<Long, TestModel> infoList = new HashMap<>(); @RequestMapping(value = "/", method = RequestMethod.POST) public void createInfo(@Validated @RequestBody TestModel testModel) { if(testModel.getId() <= 0) { throw new TestException(HttpStatus.BAD_REQUEST, "잘못된 ID 입니다."); } if(infoList.get(testModel.getId()) != null) { throw new TestException(HttpStatus.CONFLICT, "이미 존재하는 ID 입니다."); } infoList.put(testModel.getId(),testModel); } @RequestMapping(value = "/", method = RequestMethod.GET) public List<TestModel> getAllInfo() { return new ArrayList<>(infoList.values()); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public TestModel getInfo(@PathVariable Long id) { if(infoList.get(id) == null) { throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다."); } return infoList.get(id); } @RequestMapping(value = "/", method = RequestMethod.PUT) public void updateInfo(@Validated @RequestBody TestModel testModel) { if(infoList.get(testModel.getId()) == null) { throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다."); } infoList.put(testModel.getId(),testModel); } @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public void deleteInfo(@PathVariable Long id) { if(infoList.get(id) == null){ throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다."); } infoList.remove(id); } } | cs |
- @RestController를 사용하여 REST로 사용할 Controller 지정
- @RequestMapping을 사용하여 기본 URI를 "/test"로 설정
CORS(Cross Origin Resource Sharing)
- 웹 페이지에서 요청 시, 해당 페이지의 요청을 받은 서버가 아닌 다른 도메인 서버 resource에 접근하기 위한 메커니즘
1. Annotation
- Controller, handler 단위
- Controller, handler에 @CrossOrigin 사용
1) 종류
- exposedHeaders : Browser에서 Client가 받을 수 있는 response header 지정
- maxAge : preflight 요청에 대한 응답을 캐시할 시간 지정(default = 1800초)
- allowCredentials : 인증 정보를 취급할지에 대한 여부(default = true)
- allowedHeaders : 실제 요청에서 사용될 수 있는 header 지정(preflight 요청 시, 해당 값으로 검증)
- methods : 지원 가능한 request method 지정(default = requestmapping에 지정된 method들)
- origins(value) : 요청을 허용할 주소:포트 지정
2. Config
- Application 단위
- 설정 파일에 추가
1 2 3 4 5 6 7 8 9 | @Configuration @EnableWebMvc @ComponentScan("com") public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("path"); } } | cs |
URIBuilder
- Spring에서 제공하는 URI 생성을 위한 컴포넌트
1. UriConentsBuilder
- URI를 생성하기 위한 Component
1 2 3 4 5 6 | @RequestMapping(value = "/test", method = RequestMethod.GET) public URI test() { return UriComponentsBuilder.newInstance().path("/aaaa/bb/{c}") .buildAndExpand(10) .toUri(); } | cs |
- path로 URI 템플릿 생성
- buildAndExpand로 path variable에 들어갈 값 지정
- 결과
2. MvcUriComponentsBuilder
- URI를 생성하기위한 Component
1 2 3 4 5 | @RequestMapping(value = "/test", method = RequestMethod.GET) public URI test() { return MvcUriComponentsBuilder.fromController(TestController.class) .path("/test/{a}").buildAndExpand(123).toUri(); } | cs |
- fromController로 Controller에 지정된 기본 URL 가져옴
- path로 URI 생성
- 결과
Jackson
- JSON 포맷을 제어하기 위한 라이브러리
- 포맷 제어를 위한 annotation 제공
ObjectMapper
- Jackson이 제공하는 클래스로 JSON과 자바 객체를 서로 변환
- 기본적으로 지원되지만 커스텀하여 사용할 수 있음
- dateformat 등 JSON을 다루기 위해 필요한 것들을 제공
ex)
1 2 3 4 5 6 7 8 9 10 11 | @Bean @Primary public ObjectMapper objectMapper() { SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); simpleModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyyMMdd"))); return Jackson2ObjectMapperBuilder.json().modules(simpleModule).build(); } | cs |
- Deserializer를 "yyyyMMdd"형식으로 등록(Json → Object)
- Serializer를 "yyyy-MM-dd"형식으로 등록(Object → Json)
- WebMvcConfig 추가
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Configuration @EnableWebMvc @ComponentScan("com") public class WebMvcConfig implements WebMvcConfigurer { @Autowired private ObjectMapper objectMapper; @Bean public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); return converter; } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(jackson2HttpMessageConverter()); } } | cs |
- ObjectMapper를 converter로 등록
- 결과
REST Client
- Spring Application 내에서 REST Api를 호출하기 위한 방법
- Spring 3.0 이후에 제공되는 RestTemplate 이용
RestTemplate
- REST API를 호출하기 위한 클래스
- HTTP Client의 역할도 함
1. 사용법
1 2 3 4 5 | @RequestMapping(value="/template", method = RequestMethod.GET) public TestModel templateGet() { RestTemplate restTemplate = new RestTemplate(); return restTemplate.getForObject("http://localhost:8080/test/{id}", TestModel.class, 1); } | cs |
- RestTemplate 클래스 이용
- getForObject로 해당 URL에서 객체를 얻어올 수 있음
- post, get, delete, put 등 다양한 http method를 method로 제공
- 결과
2. RestTemplate Component
1) HttpMessageConverter
- Java 객체 <-> Json 간 서로 변환해주는 역할
- RestTemplate method 호출 시, 가장 먼저 거침
2) ClientHttpRequestFactory
- 요청을 전송할 객체를 만들기 위한 인터페이스
- ClientHttpRequest가 메시지 전송 역할 담당
3) ClientHttpRequestInterceptor
- HTTP 통신 전후에 공통 처리를 하기 위한 인터페이스
4) ResponseErrorHandler
- 에러 처리를 위한 인터페이스
3. RequestEntity/ResponseEntity
1) RequestEntity
- Request Header 설정이나 body 설정 등 통신에서 필요한 것을 제공
ex)
1 2 3 4 5 6 7 8 9 10 11 12 | @RequestMapping(value="/template", method = RequestMethod.POST) public void templatePostTest() { TestModel testModel = new TestModel(); testModel.setId(Long.valueOf(123)); testModel.setName("test"); testModel.setLocalDate(LocalDate.now()); RequestEntity<TestModel> requestEntity = RequestEntity .post(URI.create("http://localhost:8080/test/")) .contentType(MediaType.APPLICATION_JSON) .body(testModel); } | cs |
- RequestEntity를 생성
- POST 요청 Method를 사용하게 함
- Content-Type을 "application/json"으로 지정
- body에 TestModel 객체를 삽입
2) ResponseEntity
- Response header나 http 통신 정보를 얻기 위해 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @RequestMapping(value="/template", method = RequestMethod.POST) public void templatePostTest() { TestModel testModel = new TestModel(); testModel.setId(Long.valueOf(123)); testModel.setName("test"); testModel.setLocalDate(LocalDate.now()); RequestEntity<TestModel> requestEntity = RequestEntity .post(URI.create("http://localhost:8080/test/")) .contentType(MediaType.APPLICATION_JSON) .body(testModel); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Void> responseEntity = restTemplate .exchange(requestEntity, Void.class); } | cs |
- 생성했던 RequestEntity를 RestTemplate를 이용해 실행
- 응답으로 ResponseEntity를 받음
- ResponseEntity로 HttpStatus, Header 등을 조회
Timeout
- RestTemplate 사용 시, 타임아웃을 지정할 수 있음
1 2 3 4 5 6 7 | @RequestMapping(value="/template", method = RequestMethod.POST) public void templatePostTest() { SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory(); simpleClientHttpRequestFactory.setConnectTimeout(1000); simpleClientHttpRequestFactory.setReadTimeout(1000); RestTemplate restTemplate = new RestTemplate(simpleClientHttpRequestFactory); } | cs |
- SimpleClientHttpRequestFactory 클래스를 이용하여 Connection Timeout과 Read Timeout 설정
- RestTemplate의 생성자 인자로 넘겨줌