Spring의 Handler에 대해 알아보도록 하겠습니다. 우선, 가장 기본적인 Handler Mapping 작성법을 알아본 뒤, 요청 본문으로부터 데이터를 받는법, 응답을 본문에 작성하는법, 확장자에대한 얘기 등등을 하도록 하겠습니다. 개인 공부의 목적이므로 이번 포스팅에서는 많은 내용들이 생략될 수 있는점 양해 부탁드리겠습니다. 더 자세한 내용을 원하신다면 질문 주시면 답변드리겠습니다.
1. HandlerMapping 작성법
1.1 @RequestMapping
@Controllerpublic
class SampleController {
@RequestMapping("/hello")
public String hello(){
return "hello.html";
}
}
모두 아시다시피 위처럼 간단히 Controller를 작성한 뒤 view
이름과 같은 문자열을 return 하게 되면 classpath에 존재하는 view를 찾아 그것을 사용자에게 응답하게 됩니다.
이처럼 핸들러에 @ReponseBody 어노테이션을 부여하면 응답 본문에 데이터를 실어서 응답할 수도 있습니다. 이제 부터는 테스트를 용이하게 하기 위해(app실행, 브라우저 켜서 url입력하기 귀찮습니다..) @ResponseBody 어노테이션을 핸들러에 부여하도록 하겠습니다.
Test 작성
@RunWith(SpringRunner.class)
@WebMvcTest
public class SampleControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
}
방금 생성한 handler를 테스트 하기 위해 간단한 testcode를 작성했습니다.
@RequestMapping은 모든 메소드를 지원
이 @ReuqestMapping 어노테이션은 별도의 속성값을 주지 않는다면 기본적으로 모든 HTTP 메소드를 지원합니다. 테스트를 해보겠습니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
controller는 그대로 둔 상태로 TEST 코드를 위와같이 작성합니다. 바뀐 부분은 perform(post())
입니다.
성공하는 것을 볼 수 있습니다.
특정 메소드만 지원하도록 하기
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello(){
return "hello";
}
value 속성에는 매핑될 url을 입력하고 method에 지원할 method를 입력해주면 됩니다.
이번에는 테스트에 실패하는 것을 볼 수 있습니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello"))
.andDo(print())
.andExpect(status().isMethodNotAllowed());
}
테스트 코드를 위와같이 변경 하면, 응답으로 405(지원되지 않는 메소드 요청)를 기대하겠다는 의미입니다.
테스트에 성공하게 됩니다.
1.2 @GepMapping, @PostMapping
위 처럼 작성하기 불편한것 때문에 Spring 4.3 이후 부터 @GetMapping, @PostMapping 이 도입되었습니다.
@Controller
public class SampleController {
@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}
}
앞서서 작성한 핸들러를 위와 같이 변경할 수 있습니다. 이름만 봐도 알 수 있듯이 @GetMapping("")
는 @RequestMapping(value = "", method = RequestMethod.GET)
을 대신하는 코드입니다.
x
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk());
mockMvc.perform(post("/hello"))
.andExpect(status().isMethodNotAllowed());
}
위처럼 테스트 코드를 작성합니다. get메소드에 대해서만 응답을 하는지 확인하는 코드입니다.
성공입니다.
1.3 요청 패턴(?, *, **, /{key:정규식})
사용자의 요청패턴을 지정하여 핸들러에 매핑하는 방법도 존재합니다. 각각의 패턴에 대해 간단히 설명드리겠습니다.
?
?
는 사용자의 요청중 아무것이나 한글자를 요한다는 의미입니다.
@GetMapping("/hello/?")
@ResponseBody
public String hello(){
return "hello";
}
예를 들어 위와같이 핸들러를 작성한다면,
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello/a"))
.andExpect(content().string("hello"))
.andExpect(status().isOk());
}
통과하는 테스트 코드는 위와 같을것입니다. 즉 /hello/
이후 한글자를 더 필요로 한다는 것입니다.
*
*(Asterisk)
는 여러글자를 요할때 사용합니다.
@GetMapping("/hello/*")
@ResponseBody
public String hello(){
return "hello";
}
예를 들어 위와같이 핸들러를 작성한다면,
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello/asdasd"))
.andExpect(status().isOk());
mockMvc.perform(get("/hello/cccccccccccc"))
.andExpect(status().isOk());
mockMvc.perform(get("/hello/a123asda"))
.andExpect(status().isOk());
}
위와 같이 /hello/
이후의 모든 요청을 매핑하게됩니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello/a/b"))
.andExpect(status().isOk());
}
하지만 위와같이 1개의 경로가 아닌 2개의 경로 이상인 경우 테스트 코드에 실패하게 됩니다.
**
**
아스테리스크 기호 2개를 연달아 이용한다면, 이하 모든 요청 경로에대해 매핑하겠다는 의미입니다. 즉, 앞서 실패한 2단계 이상에 대한 요청까지 매핑을 하게 됩니다.
@GetMapping("/hello/**")
@ResponseBodypublic String hello(){
return "hello";
}
핸들러를 위와 같이 수정합니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello/a/b"))
.andExpect(status().isOk());
}
앞서 실패했던 테스트 코드를 통과합니다.
{key : 정규식}
요청에 대해 매핑시 정규표현식으로 매핑이 가능합니다. (정규표현식은 다음 링크를 참고해주세요.https://galid1.tistory.com/546 )
@GetMapping("/hello/{name:[a-z]+}")
@ResponseBodypublic
String hello(@PathVariable String name){
return "hello";
}
위와 같이 url에 name:정규표현식
을 입력합니다. name
의 경우 해당 url을 핸들러의 매개변수로 받아들이기 위한 일종의 key라고 생각하면 됩니다. 이 name과 같은 이름의 매개변수가 핸들러에 존재한다면 자동으로 값을 매핑시켜 줍니다.
*이때 중요한것은 @GetMapping("")안에 적는 url은 말그대로 url이기 때문에 구별하기 쉽게하기 위해서 공백
을 사용하면 안된다는 것입니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello/abcdefg"))
.andExpect(status().isOk());
}
앞서 작성한 핸들러에 대한 테스트 코드입니다.
2. 확장자 지원(사용자가 원하는 응답을 받도록)
이전 Spring MVC 또는 Servlet을 사용할 때에는 확장자를 이용하여 요청을 하도록 핸들러를 만들어놓는 경우가 있었습니다. 이는 요청할때 자신이 응답받고자하는 데이터에대한 타입을 구별하기 위해서
였습니다.
SpringMVC에서는 위의 이유로 기본적으로 .*
을 자동으로 작성을 해줍니다. 예를 들면 위와 같은 매핑을 자동으로 추가해줍니다. 하지만, spring boot에서는 기본적으로 모든 확장자를 지원하지 않습니다
. 바로 RFD 공격
이란 것 때문입니다.
Spring Boot 확장자 지원 테스트
위와 같은 이유 때문에 Spring boot에서는 모든 확장자를 기본적으로 지원하지 않습니다.
@GetMapping("/hello")
@ResponseBodypublic String hello(){
return "hello";
}
Spring Boot에서 핸들러에 위와 같이 작성합니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(get("/hello.zip"))
.andExpect(status().isOk());
mockMvc.perform(get("/hello.xml"))
.andExpect(status().isOk());
}
그후 자동으로 확장자가 추가되는지를 테스트합니다. 실패하는 것을 볼 수 있습니다.
3. 요청헤더 사용하기
최근의 추세는 요청 URI를 통해 응답할 데이터를 정하는 것이 아니라, 요청시 Accept header
를 이용해 사용자가 응답받고자 하는 데이터타입을 요청 헤더에
적도록 하고, Content-Type
이란 헤더를 통해서 사용자가 Request Body에 담은 내용을 알리도록 하고 있습니다.
3.1 요청 페이로드 타입 제한하기(consumes)
@*Mapping(GetMapping, PostMapping 등등)
어노테이션의 속성으로 consumes
라는 속성이 존재하는데요, 이 속성을 이용하면 사용자가 Request Body에 담는 타입을 제한할 수 있습니다. 즉, 이 핸들러에서는 body에 담긴 데이터의 타입이 APPLICATION_JSON_UTF8일 경우의 요청만을 처리한다는 의미입니다. 따라서 요청시 헤더에 꼭 application/json 이 존재해야합니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello"))
.andExpect(status().isOk());
}
위와 같이 테스트코드를 작성한뒤 테스트를 진행하면 415에러(not Supported MediaType)
가 나타납니다. 즉, 지원하지않는 body 데이터 형식을 header에 설정해 요청했기 때문입니다. Headers를 확인하면 아무런 header가 들어있지 않는것을 볼 수 있습니다.
contentType
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(print())
.andExpect(status().isOk());
}
위와 같이 contentType()
을 이용하여 테스트 요청에 header를 추가할 수 있습니다. 여기에 MediayType.Type
을 이용해 원하는 타입의 헤더를 넣어 테스트를 할 수 있습니다. 또 andDo(print())
를 추가하면 요청, 응답 결과를 console에서 확인할 수 있습니다.
테스트가 성공했습니다. 요청 Headers를 확인하면 핸들러에서 제한한 타입의 header가 담겨있는것을 볼 수 있습니다.
MediaType
MediaType은 header에 기입되는 application/json;charset=UTF-8
과 같은 content Type을 상수로써 기술할 수 있도록 도와주는 api입니다. 이것을 사용하므로써 문자열을 적는것 보다 type safe한 효과를 얻을 수 있습니다.
consumes의 값으로는 문자열이 와야하기 때문에 MediaType.APPLICATION_JSON_UTF8_VALUE
를 사용한 것을 볼 수 있습니다. 상수의 맨 뒤에 _VALUE가 붙어있는 경우에는 객체가 아닌 문자열로된 값을 얻어오게 됩니다.
MediaType객체를 매개변수로 원하는 곳에서는 _VALUE
가 붙지 않은 상수를 사용하면 됩니다.
3.2 응답 데이터 제한하기(produces, headers)
3.2.1 produces
@PostMapping(
value = "/hello",
produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBodypublic String hello(){
return "hello";
}
위 코드와 같이 @Mapping 어노테이션의 속성으로 produces
를 추가하고 data Type을 지정하면 해당 데이터타입으로만 사용자에게 응답하겠다는 의미입니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello")
.accept(MediaType.APPLICATION_JSON_UTF8))
.andDo(print())
.andExpect(status().isOk());
}
테스트 코드에서 accept()
를 이용해 요청 header에 accept를 추가합니다. JSON 응답을 원한는 요청을 만들어 테스트를 해보겠습니다. 406ERROR(Not Supported)
가 나타납니다.
요청에 accept 헤더가 없다면?
그런데 이 제한에는 약간 이상한 점이 있습니다. 바로 요청 헤더에 accept 헤더를 포함하지 않는 경우 입니다.
@Test
public void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello"))
.andDo(print())
.andExpect(status().isOk());
}
이런 경우에도 테스트가 성공 되는 것을 볼 수 있습니다.
3.2.2 headers (HttpHeaders)
이번에는 headers 속성을 사용하는 방법을 알아보도록 하겠습니다. headers라는 속성은 사용자가 요청하는 header에 특정 header가 존재하도록 제한할 때 사용할 수 있습니다.
@PostMapping( value = "/hello", headers = {HttpHeaders.FROM})@ResponseBodypublic String hello(){ return "hello";}
위와 같이 headers에는 배열로 값을 줄 수도있습니다.(하나만 주어도 가능)
@Testpublic void testHelloHandler() throws Exception { mockMvc.perform(post("/hello") .header(HttpHeaders.FROM, "localhost")) .andDo(print()) .andExpect(status().isOk());}
테스트 코드를 다음과같이 작성한 후 실행하면 통과하게 됩니다.
연산기능?
headers에는 문자열을 이용해 값이 존재할 때, 아닐때(!), ~와 같을때(=)
등의 경우를 만들 수도 있습니다.
=
@PostMapping(
value = "/hello",
headers = {HttpHeaders.AUTHORIZATION + "=" + "111"})
@ResponseBodypublic String hello(){
return "hello";
}
위의 경우에는 Authorization 값이 111로 세팅되어 요청이 왔을때 응답을 하겠다는 의미입니다.
@Testpublic
void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello")
.header(HttpHeaders.AUTHORIZATION, 111))
.andDo(print())
.andExpect(status().isOk());
}
위와 같이 AUTHORIZATION값을 111로 세팅하여 테스트코드를 작성하면 테스트에 통과하게 됩니다.
!
@PostMapping(
value = "/hello",
headers = {"!" + HttpHeaders.AUTHORIZATION})
@ResponseBodypublic String hello(){
return "hello";
}
이번에는 header에 Authorization 헤더가 존재하지 않는경우에 응답을 받겠다는 의미입니다.
@Testpublic
void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello"))
.andDo(print())
.andExpect(status().isOk());
}
위와 같이 header를 없앱니다.
4. @RequestParam, @PathVariable
4.1 @RequestParam
@RequestParam 어노테이션은 핸들러의 매개변수에 사용자가 입력한 파라미터값을 매핑할 때 사용합니다.
@PostMapping("/hello")
@ResponseBodypublic String hello(@RequestParam String name, @RequestParam int age){
return "hello " + name + " " + age;
}
핸들러를 위와 같이 작성합니다. 각각의 매개변수 앞에 @RequestParam을 입력합니다.
@Testpublic
void testHelloHandler() throws Exception {
mockMvc.perform(post("/hello?name=jjy&age=10"))
.andDo(print())
.andExpect(status().isOk());
}
위와 같이 테스트 코드를 작성하여 실행합니다. 성공입니다.
4.1.1 @RequestParam 생략?
@PostMapping("/hello")
@ResponseBodypublic String hello(String name, int age){
return "hello " + name + " " + age;
}
사실 @RequestParam은 별도로 입력하지 않아도 됩니다.
테스트에 성공하게 됩니다.
4.1.2 @RequestParam 사용 이유
그렇다면 @RequestParam은 왜 존재할까요? 바로 사용자가 원하는 매개변수에 맵핑하기 위해서 입니다. 사실 Spring은 @RequestParam을 사용하지 않아도 사용자가 입력한 Parameter의 key값과 매개변수의 이름을 비교
하여 값을 적절히 넣어줍니다.
@PostMapping("/hello")
@ResponseBodypublic String hello(String myName, int age){
return "hello " + myName + " " + age;
}
그렇다면 이런 생각이 드셨을 것입니다. 한번 매개변수의 이름을 바꾸어보자. 바로 해보겠습니다. handler의 배개변수의 name을 myName으로 변경합니다.
테스트 코드를 실행합니다. 성공하셨다구요? 값을 잘 보셔야합니다. name값에 null이 들어있는것을 볼 수 있습니다.
@PostMapping("/hello")
@ResponseBodypublic String hello(@RequestParam ("name") String myName, int age){
return "hello " + myName + " " + age;
}
이번에는 @RequestParam을 이용해보겠습니다. 매개변수의 이름은 마찬가지로 개발자가 원하는 대로 작성합니다. 그 후()
안에는 name으로 사용자가 입력해주는 parameter의 key를 입력합니다.
4.2 @PathVariable
https://galid1.tistory.com/505 Pathvariable 의 경우 이전포스팅을 참고해주세요.
4.3 Parameter 제한하기 (params)
4.3.1 요청파라미터에 특정 키값이 존재하게 제한
@PostMapping(
value = "/hello",
params = {"name", "age"})
@ResponseBodypublic String hello(@RequestParam ("name") String myName, int age){
return "hello " + myName + " " + age;
}
사용자 요청 파라미터에 특정 키들이 존재하도록 제한하기 위해서는 params
속성을 이용하면 됩니다. 여러 키들을 가지도록 제한하고 싶을때에는 위와 같이 배열을 사용하면 됩니다.
4.3.2 특정 키값이 존재하지 않도록 제한
@PostMapping(
value = "/hello",
params = {"!name", "!age"})
@ResponseBodypublic String hello(){
return "hello";
}
!
만 키값 앞에 붙혀주면 됩니다. 또한 파라미터가 존재하지 않는 경우이므로, hello의 매개변수로 받던 @RequestParam
어노테이션이 부여된 매개변수를 지워주어야 합니다.
출처: https://galid1.tistory.com/556 [배움이 즐거운 개발자:티스토리]
'백엔드 > Spring' 카테고리의 다른 글
[Spring] Spring Validation을 이용한 유효성 검사 (0) | 2024.03.05 |
---|---|
[Swagger] 스웨거(Swagger) Annotation, Validation (0) | 2024.02.28 |
[Spring Security] CSRF 토큰 (0) | 2024.02.26 |
[Spring] Spring IoC 컨테이너, Bean (0) | 2024.01.16 |
[Spring] 필터(Filter)와 인터셉터(Interceptor) (1) | 2024.01.10 |