Prologue
-
@PathVariable Annotation을 사용하는
-
다양한 경우로 테스트를 진행하였다.
-
이 글에서는 @PathVariable를 1개만 사용하였을 경우를 살펴보고
-
@PathVariable를 2개 이상 사용하는 경우에 대해서는
-
Spring Controller에서 사용하는 Annotation 분석하기 : Multi @PathVariable에서 알아본다.
-
각 테스트마다의 연관성은 낮다.
-
하지만 순서대로 보는걸 추천한다.
-
또한 각 테스트마다
-
어떤 이유로
-
그러한 결과가 나왔는지
-
반드시 이해하고
-
이해가 가지 않는다면 직접 테스트해보길 추천한다.
-
필자는 다양한 경우에 대해
-
경우의 수를 생각하면서
-
테스트를 진행하였고
-
그 내용을 정리하여 공유하고 싶은 마음에
-
오랜 시간을 할애하여 글을 작성하였다.
-
누군가에겐 도움이 되길 바란다.
Domain
Person1
@Getter
@Setter
public class Person1 {
private Long age1_Long; // Age를 뜻하는 필드
private String name;
private Boolean isConvert;
}
Person2
@Getter
@Setter
public class Person2 {
private Long age2_Long; // Age를 뜻하는 필드
private String name;
private Boolean isConvert;
}
-
Person1과 Person2의 차이
-
Age를 뜻하는 필드의
-
Type은 같지만 필드명이 다름
-
Person3
@Getter
@Setter
public class Person3 {
private String age3_String; // Age를 뜻하는 필드
private String name;
private Boolean isConvert;
}
Person4
@Getter
@Setter
public class Person4 {
private String age4_String; // Age를 뜻하는 필드
private String name;
private Boolean isConvert;
}
-
Person3과 Person4의 차이
-
Age를 뜻하는 필드의
-
Type은 같지만 필드명이 다름
-
Person5
@Getter
@Setter
public class Person5 {
private Long age1_Long;
private String nickName;
private Boolean isConvert;
}
-
Person5는
-
다른 Person 도메인과 다르게
-
2가지 차이가 있다.
-
Person1에서 Age를 뜻하는 필드와 동일한 필드명 사용
-
name이 아닌 nickName을 사용
-
2가지 차이를 둔 이유에 대해서는
-
아래 Example을 통해 알아본다.
Domain Summary
-
(Person1, Person2)과 (Person3, Person4)의 차이
-
Age를 뜻하는 필드의 Type
-
Person1, Person2 ==> Long
-
Person3, Person4 ==> String
-
Converter
Person1 Converter
public class Person1Converter {
@Component
public static class StringToPersonConverter implements Converter<String, Person1> {
@Override
public Person1 convert(String s) {
System.out.println("[Person`1` Converter] StringToPersonConverter Working");
Person1 person1 = new Person1();
person1.setIsConvert(true); // Converter 호출했음을 표기하기 위한 값 설정
return person1;
}
}
}
Example
Case 1
Controller
@RestController
public class SpringController {
@GetMapping("/path/variable/{name}")
public Person1 pathVariable_Name(@PathVariable("name") Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
}
Test Code
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class SpringControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Test
public void pathVariable_Name() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/path/variable/goodgid"))
.andDo(print())
.andExpect(status().isOk());
}
}
Log
[Person`1` Converter] StringToPersonConverter Working
Person1.Age : null
Person1.Name : null
Person1.IsConvert : true
MockHttpServletRequest:
HTTP Method = GET
Request URI = /path/variable/goodgid
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
...
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"age1_Long":null,"name":null,"isConvert":true}
Forwarded URL = null
Redirected URL = null
Cookies = []
Comment
-
Log를 통해
-
Converter가 동작함을 확인 할 수 있다.
[Person`1` Converter] StringToPersonConverter Working
Person1.IsConvert : true
-
그런데
-
Converter에서 값을 세팅해주지 않았기 때문에
-
age1_Long과 name에는 null이 담겨온다.
-
이걸로 알 수 있는 점은
-
Converter를 사용한다면
-
Converter 안에서
-
객체의 필드 값을 세팅해주는 작업이 필요하다는 점이다.
-
하지만 Converter같은 경우엔
-
1개의 Type만을 받을 수 있다.
@FunctionalInterface
public interface Converter<S, T> {
/**
* Convert the source object of type {@code S} to target type {@code T}.
* @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
* @return the converted object, which must be an instance of {@code T} (potentially {@code null})
* @throws IllegalArgumentException if the source cannot be converted to the desired target type
*/
@Nullable
T convert(S source);
}
/***
* StringToPersonConverter 같은 경우엔
* String Type만 받는다.
*/
@Component
public static class StringToPersonConverter implements Converter<String, Person1> {
@Override
public Person1 convert(String s) {
...
return person1;
}
}
-
그렇기 때문에
-
다양한 Type으로
-
객체를 생성할 필요가 있다면
-
Converter가 아닌
-
@ModelAttribute Annotation을 사용해야한다.
Case 2
Controller
@RestController
public class SpringController {
/**
* Case 1
*/
@GetMapping("/path/variable/{name}")
public Person1 pathVariable_Name(@PathVariable("name") Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
/**
* Case 2
* @PathVariable 어노테이션 선언 삭제
*/
@GetMapping("/path/variable/{name}/model/attribute")
public Person1 pathVariable_Name_Model_Attribute(Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
}
-
Case 1과 같은 조건에서
-
@PathVariable을 삭제해보자.
Test Code
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class SpringControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Test
public void pathVariable_Name_Model_Attribute() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/path/variable/goodgid/model/attribute"))
.andDo(print())
.andExpect(status().isOk());
}
}
Log
Person1.Age : null
Person1.Name : goodgid
Person1.IsConvert : null
MockHttpServletRequest:
HTTP Method = GET
Request URI = /path/variable/goodgid/model/attribute
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
...
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"age1_Long":null,"name":"goodgid","isConvert":null}
Forwarded URL = null
Redirected URL = null
Cookies = []
Comment
-
이번 경우엔
-
Person1 Converter가 사용되지 않았다.
-
대신에 ModelAttribute에 의해서
-
Person1 객체에 Data Binding이 이뤄졌다.
-
URL Path에 {name}를 사용하였고
"/path/variable/{name}/model/attribute"
-
Person1 클래스에
-
name이라는 필드가 있기 때문에
private String name;
- Data Binding이 되었다.
-
여기서 알 수 있는 점은
-
@PathVariable를 사용해야
-
Converter가 호출된다는 점이다.
Case 3
Controller
@RestController
public class SpringController {
/**
* Case 1
*/
@GetMapping("/path/variable/{name}")
public Person1 pathVariable_Name(@PathVariable("name") Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
@GetMapping("/path/variable/2/{name2}")
public Person1 pathVariable_Name2(@PathVariable("name2") Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
}
-
Case 1과 같은 환경에서
-
변수명만 변경하여 테스트를 진행한다.
-
name -> name2
Test Code
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class SpringControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Test
public void pathVariable_Name2() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/path/variable/2/goodgid"))
.andDo(print())
.andExpect(status().isOk());
}
}
Log
[Person`1` Converter] StringToPersonConverter Working
Person1.Age : null
Person1.Name : null
Person1.IsConvert : true
MockHttpServletRequest:
HTTP Method = GET
Request URI = /path/variable/2/goodgid
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
...
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"age1_Long":null,"name":null,"isConvert":true}
Forwarded URL = null
Redirected URL = null
Cookies = []
Comment
-
Case 1과 마찬가지로
-
Log를 보면 Converter 호출이 되었음을 알 수 있다.
[Person`1` Converter] StringToPersonConverter Working
Person1.IsConvert : true
-
또한 Case 1과 동일하게
-
Converter에서 Data Binding 작업이 이뤄지지 않았기 때문에
-
Person1 객체에는
-
Data Binding은 되지 않았다.
Body = {"age1_Long":null,"name":null,"isConvert":true}
-
이 테스트를 통해 확인하고 싶었던 점은
-
URL Path에 있는 값과
-
( = name2 )
@GetMapping("/path/variable/2/{name2}") // {name2}
-
Handelr Argument에 있는
-
@PathVariable Annotation의 Key값과
-
( = @PathVariable(“name2”) –> 여기서 Key 값으로 지정한 “name2” )
@PathVariable("name2")
- Arguemnts에 Object의 관계를 알고 싶었다.
Person1 person1
-
그 결과는 다음과 같다.
-
URL Path에 있는 값과
-
@PathVarialbe의 Key 값은 상관 관계가 있다.
-
URL Path와 Key 값이 같았기 때문에
-
Converter가 호출되었다.
-
만약 Controller를 다음과 같이 수정 후
@GetMapping("/path/variable/2/{name2}")
public Person1 pathVariable_Name2(@PathVariable("name3") Person1 person1) {
-
다시 Test Code를 실행할 경우
-
다음과 같은 Error Log를 볼 수 있다.
2020-01-27 15:26:48.761 WARN 34819 --- [ main] .w.s.m.s.DefaultHandlerExceptionResolver :
Resolved [org.springframework.web.bind.MissingPathVariableException:
Missing URI template variable 'name3' for method parameter of type Person1]
-
그리고 다음으로 알 수 있었던 점은
-
Handler Argument의 Type은
-
(= Handler Argument = Person1)
-
URL Path와 @PathVariable의 Key 값과
-
관련이 없음을 알 수 있었다.
-
Case 3를 정리해보자.
-
Case 3의 Controller에서는
-
@PathVariable Annotation을 사용하였기 때문에
-
Model Attribute 방법으로 Data Binding이 이뤄지지 않았고
-
대신에 Converter가 호출되었다.
-
그리고
-
URL Path / @PathVariable의 Key 값 / Handler Argument Type 간의
-
상관 관계에 대해서도 알아보았다.
Case 4
Controller
@RestController
public class SpringController {
/**
* Case 3
*/
@GetMapping("/path/variable/2/{name2}")
public Person1 pathVariable_Name2(@PathVariable("name2") Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
/**
* Case 4
*/
@GetMapping("/path/variable/2/{name2}/model/attribute")
public Person1 pathVariable_Name2_Model_Attribute(Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
}
-
Case 3과 같은 조건에서
-
@PathVariable을 삭제해보자.
Test Code
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class SpringControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Test
public void pathVariable_Name2_Model_Attribute() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/path/variable/2/goodgid/model/attribute"))
.andDo(print())
.andExpect(status().isOk());
}
}
Log
Person1.Age : null
Person1.Name : null
Person1.IsConvert : null
MockHttpServletRequest:
HTTP Method = GET
Request URI = /path/variable/2/goodgid/model/attribute
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
...
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"age1_Long":null,"name":null,"isConvert":null}
Forwarded URL = null
Redirected URL = null
Cookies = []
Comment
-
우선 Case 4에서는
-
@PathVariable Annotation을 사용하지 않았기 때문에
-
Spring은 @ModelAttribute 방식으로
-
Data Binding을 시도하였다.
-
하지만 Person1 객체에는
-
name2라는 필드가 없기 때문에
-
Person1 객체에는
-
어떠한 Data Binding이 이뤄지지 않았다.
-
이와 관련해서는 Case 2와 비교하여 살펴보자.
-
다음으로 알 수 있었던 점은
-
@ModelAttribute 방식으로
-
Data Binding을 하였기 때문에
-
Converter는 호출되지 않았다.
-
또한 이번 테스트를 통해
-
알 수 있었던 흥미로웠던 점은
-
Case 2와 같은 상황에서
-
URL Path에
-
name을 사용하느냐
-
name2를 사용하느냐 차이에 따른 결과이다.
/**
* Case 2
*/
@GetMapping("/path/variable/{name}/model/attribute")
public Person1 pathVariable_Name_Model_Attribute(Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
/**
* Case 4
*/
@GetMapping("/path/variable/2/{name2}/model/attribute")
public Person1 pathVariable_Name2_Model_Attribute(Person1 person1) {
System.out.println("Person1.Age : " + person1.getAge1_Long());
System.out.println("Person1.Name : " + person1.getName());
System.out.println("Person1.IsConvert : " + person1.getIsConvert());
return person1;
}
-
그리고 그 차이는
-
Person1 객체에
-
정확히 일치하는 필드명이 있느냐의 유무에 따라
-
Person1 객체에 Data Binding 결과에도 영향을 끼쳤다.
일치할 경우 : Person1 객체에 Data Binding 발생 O
일치하지 않을 경우 : Person1 객체에 Data Binding 발생 X
Summary
-
이 글에서는
-
@PathVariable을 1개만 사용하는 경우에 대해서 알아보았다.
-
그리고 @PathVariable 유무에 따른
-
Spring의 Data Binding 방법의 차이에 대해 알아보았다.
-
각 Test를
-
꼼꼼히 살펴보면서
-
확실하게 이해하고
-
부족한 개념에 대해서는
-
추가적인 학습을 통해
-
보충하도록 하자.