WebClient๋ฅผ ์ฌ์ฉํด์ Github Api๋ก ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ WebClientService ํด๋์ค๊ฐ ์์๊ณ , ํ ์คํธ ์ฝ๋๋ฅผ ์ง๊ธฐ ์ํด ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด์์ต๋๋ค.
๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋, Mock ์น ์๋ฒ๋ฅผ ์ ์ํด์ ํ ์คํธ๋ฅผ ํ๋ ๋ฐฉ๋ฒ์ด ์์์ต๋๋ค.
-
์ค์ API ์๋ฒ๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ๋ฅผ ํ๋ฉด, ์๋ฒ ์ํ์ ๋ฐ๋ผ ํ ์คํธ์ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง ์ ์์ต๋๋ค.
-
Mock ์๋ฒ๋ก๋ ์ถฉ๋ถํ ์ธ๋ถ API๊ฐ ์ ์์ด๊ณ ์ ์์ ์ธ ๊ฐ(์์ํ ๊ฐ)์ด ๋ฐํ๋๋ค๋ฉด, ์ ์์ ์ผ๋ก ๋ก์ง์ด ์๋ํ๋ ๊ฒ์ ๋ณด์ฌ์ค ์ ์์ต๋๋ค.
-
๋ก์ปฌ์ ์๋ฒ๋ฅผ ๋์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์๋๋ ๋น ๋ฅด๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
์์ ๊ฐ์ ์ฅ์ ์ด ์๋ Mock ์น ์๋ฒ๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ํ์ ์ผ๋ก MockWebServer์ WireMock ์ด ์์ต๋๋ค.
stackOverflow์ ๋ต๋ณ์ ์ํ๋ฉด WireMock์ด ์๋๋ก์ด๋์์ ์ด์๊ฐ ์์ด์ ๋์จ ๊ฒ์ด MockWebServer๋ผ์
์๋๋ก์ด๋ ํ๊ฒฝ์ด ์๋๋ผ๋ฉด WireMock์ด ๋ ์ข๋ค๊ณ ํฉ๋๋ค.
์ ๋ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ ๋ชจ๋ ์ฌ์ฉํด์ ํ ์คํธ ์ฝ๋๋ฅผ ํ๋ฒ ์์ฑํด๋ณด์์ต๋๋ค.
๋ ๋ฐฉ์์ ๊ณตํต์ ์ ๋ก์ปฌ์ ๊ฐ์ง ์๋ฒ๋ฅผ ๋์ด ๋ค, ๊ทธ ์๋ฒ์ ํน์ ์์ฒญ์ ํ์ ๋์ ์๋ถ๊ฐ์ ๊ฐ์ ํ ๋ค, ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ ๋๋ค.
testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '5.0.0-alpha.11'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '5.0.0-alpha.11'
mockwebserver ์ okhttp ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฒ์ ์ ์ผ์นํด์ผ ํ๋ค๊ณ ํฉ๋๋ค.
@ExtendWith(MockitoExtension.class)
class WebClientServiceTest {
private WebClientService webClientService;
public static MockWebServer mockWebServer;
@BeforeAll
public static void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@BeforeEach
void init() {
String baseUrl = String.format("http://localhost:%s", mockWebServer.getPort());
webClientService = new WebClientService(WebClient.create(baseUrl));
}
@AfterAll
public static void shutdown() throws IOException {
mockWebServer.shutdown();
}
๋จผ์ , ์์ฑํ MockWebServer์ ์์ฒญ์ ๋ณด๋ด์ผ ์ง์ ์ ์ํ ์๋ต์ ๋ฐ์ ์ ์์ ๊ฒ์ ๋๋ค.
๋ฐ๋ผ์, WebClient์ ์ ๋ ฅํ๋ baseUrl์ MockServer ํฌํธ๋ก ๋ณ๊ฒฝํฉ๋๋ค.
@Test
@DisplayName("AccessToken ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต ํ
์คํธ")
public void getAccessTokenSuccess() {
String expectedToken = "token";
String expectedResponse = String.format("access_token=%s&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}", expectedToken);
mockWebServer.enqueue(new MockResponse()
.setBody(expectedResponse));
String code = "code";
String accessToken = webClientService.getAccessToken(code, "/login/oauth/access_token");
assertThat(accessToken).isEqualTo(expectedToken);
}
enqueue() ๋ฉ์๋๋ฅผ ํตํด์ mockWebServer๋ก ์์ฒญ์ด ๋ค์ด์์๋ ์๋ต ๊ฐ์ ์ง์ ํด์ค๋๋ค.
GitHub Api๋ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ํตํด ์ธ์ฆํ๋ฉด ๋ฐ๋ Code๋ฅผ https://github.com/login/oauth/access_token
๋ก Post ์์ฒญ์ ๋ณด๋ด๋ฉด
access_token={๊ฐ}&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}
์์ ๊ฐ์ ํ์์ผ๋ก ์๋ตํฉ๋๋ค.
๋ฐ๋ผ์, ์์ ๊ฐ์ ๊ฐ์ผ๋ก ๋ฐํํ๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ค์ ๋ก๋ https://github.com/login/oauth/access_token
๋ก ์์ฒญ์ ๋ณด๋ด์ง๋ง, ์ง์ ์์ฑํ ๊ฐ์ง ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด์ผ ํ๋ฏ๋ก uri๋ฅผ /login/oauth/access_token
๋ก ์
๋ ฅํ์ต๋๋ค.
ํ
์คํธ ์์ผ๋ก๋localhost:{mockWebServer์ port}/login/oauth/access_token
๋ก ์์ฒญ์ด ๋ณด๋ด์ง ๊ฒ์ด๊ณ , ์ค์ ํ ์๋ต๊ฐ์ ๊ธฐ๋ํ ์ ์์ต๋๋ค.
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-contract-stub-runner', version: '4.0.1'
WireMock์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
@AutoConfigureWireMock
class WebClientServiceTest2 {
private static WireMockServer wireMockServer;
@Autowired
private WebClientService webClientService;
@BeforeAll
public static void setUp() throws IOException {
wireMockServer = new WireMockServer();
wireMockServer.start();
}
@BeforeEach
void init(){
String baseUrl = String.format("http://localhost:%s", wireMockServer.port());
webClientService = new WebClientService(WebClient.create(baseUrl));
}
@AfterAll
public static void stop(){
wireMockServer.stop();
}
WireMock ๋ฐฉ์๋ MockWebServer ๋ฐฉ์๊ณผ ์ด๊ธฐ ์ธํ ์ ์ ์ฌํฉ๋๋ค.
@Test
@DisplayName("AccessToken ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต ํ
์คํธ")
public void Success(){
String expectedToken = "token";
String expectedResponse = String.format("access_token=%s&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}", expectedToken);
wireMockServer.stubFor(post(urlEqualTo("/login/oauth/access_token"))
.willReturn(
aResponse()
.withBody(expectedResponse)
));
String code = "code";
String accessToken = webClientService.getAccessToken(code, "/login/oauth/access_token");
System.out.println(accessToken);
assertThat(accessToken).isEqualTo(expectedToken);
}
ํ ์คํธ ์ฝ๋๋ ์ญ์ ๋น์ทํ์ง๋ง,
WireMock ๋ฐฉ์์ ๊ฒฝ์ฐ Http Method๋ ์ง์ ํ ์ ์๋ค๋ ๊ฒ์ด ํฐ ์ฐจ์ด์ ์ธ ๊ฒ ๊ฐ์ต๋๋ค.
์ค์ ๋ก ์ ์ฝ๋์์ stubFor(post(urlEqualTo("/login/oauth/access_token"))
์ post๋ฅผ stubFor(get(urlEqualTo("/login/oauth/access_token"))
get์ผ๋ก ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ ํ
์คํธ ์คํจ๋ฅผ ํ๊ฒ๋ฉ๋๋ค.
์๋ํ๋ฉด getAccessToken() ๋ฉ์๋์์๋, ํ๋ผ๋ฏธํฐ์ ์ ๋ ฅ๋ url์ post ์์ฒญ์ ๋ณด๋ด๊ธฐ ๋๋ฌธ์ ๋๋ค.
MockWebServer ๋ณด๋ค ๊ตฌ์ฒด์ ์ธ ์ํฉ์ผ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค๋ ๊ฒ์ด ์ฝ๋๊ฐ ์ด๋ค ์ญํ ์ธ์ง ๋ ์ ๋ณด์ฌ์ค ์ ์๋ ์ฅ์ ์ด ์๋ค๊ณ ์๊ฐ์ด ๋ค์์ต๋๋ค.
WebClient ๊ด๋ จ ํ ์คํธ ์ฝ๋๋ฅผ ์ง๋ ๋ฐฉ๋ฒ์ ์ดํดํ๋๋ฐ ์ด๋ ค์์ด ์์๊ณ ๋ง์ ์๊ฐ์ ํฌ์ํ์ต๋๋ค.
MockWebServer ์ WireMock์ ๋ํด ์๊ฒ ๋์ด ์ธ๋ถ API๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํ ์คํธ ์ฝ๋๋ฅผ ์งค ์ ์๊ฒ ๋์ด ๊ฐ์ง ์๊ฐ์ด์๋ ๊ฒ ๊ฐ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ค๋ค ๋ณด๋, ๋๋ถ์ WebClient ๊ฐ์ฒด๋ฅผ Bean์ผ๋ก ๋ฑ๋กํ ๋ค DI ๋ฐ๋๋ก ์์ ํ ์ ์์๊ณ ์์ฒญ์ ๋ณด๋ด๋ uri๋ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ ฅ๋ฐ๋๋ก ๋ฆฌํฉํ ๋งํ๊ฒ ๋๋ ๊ณ๊ธฐ๊ฐ ๋์์ต๋๋ค.
Postํ๋์๋ Enum์ผ๋ก ๊ด๋ฆฌ๋๋ Categoryํ๋๊ฐ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ ๋ ์นดํ ๊ณ ๋ฆฌ๋ฅผ Request Parameter๋ก ์ ๋ ฅ๋ฐ์ ์นดํ ๊ณ ๋ฆฌ์ ํด๋นํ๋ ๊ฒ์๊ธ๋ค์ ๋ฐํํ๋ GET ์์ฒญ API๋ฅผ ๋ง๋ค๋ ค๊ณ ํ์ต๋๋ค.
์์ํ๋ uri๋ /api/v1/posts/categories?category=qna
์ ๊ฐ์ ๊ฒ์
๋๋ค.
@RestController
@Slf4j
@RequestMapping("/api/v1/posts")
@RequiredArgsConstructor
public class PostApiController {
@GetMapping("/categories")
public ResponseEntity<Response<Page<PostGetListResponse>>> getAllByCategory(@RequestParam String requestCategory, Pageable pageable){
Category category = Category.valueOf(requestCategory)
Page<PostGetListResponse> response = postService.getAllPostsByCategory(category, pageable);
return ResponseEntity.ok(Response.success(response));
}
}
๋ฐ๋ผ์ ์ ์ฝ๋์ ๊ฐ์ด RequestParam์ String๊ฐ์ผ๋ก ์ ๋ ฅ๋ฐ๊ณ , ์ ๋ ฅ๋ฐ์ ๊ฐ์ Enumํ์ ์ผ๋ก ๋ณํ์์ผ์ผ ํ์ต๋๋ค.
์ด ๋ณํ ๊ณผ์ ๋๋ฌธ์ ์ฝ๋๊ฐ ๋ง์ด ์ถ๊ฐ๋์ด ๊ธธ์ด์ง๋ ๊ฒ์ ์๋์ง๋ง,
์ ๋ ฅ๋ฐ์๊ณผ ๋์์ ์ฒ๋ฆฌ๋๋ค๋ฉด ์ฝ๋ ๊ฐ๋ ์ฑ ํฅ์๊ณผ ํต์ฌ ๋ก์ง๋ง ๋ฉ์๋ ์์ ๋ด์ ์ ์์ ๊ฒ์ด๋ผ ์๊ฐ๋์ด ๋ฐฉ๋ฒ์ ์ฐพ์๋ดค์ต๋๋ค.
public enum Category {
QNA, COMMUNITY, STUDY, NOTICE;
public static Category create(String requestCategory) {
for (Category value : Category.values()) {
if (value.toString().equals(requestCategory)) {
return value;
}
}
throw new IllegalStateException("์ผ์นํ๋ ์นดํ
๊ณ ๋ฆฌ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.");
}
}
๋จผ์ Enumํ์ ์ธ Category์์ Stringํ์ ์ธ requestCategory๋ฅผ Category๋ก ๋ง๋ค์ด์ฃผ๋ ๋ฉ์๋๋ฅผ ์ ์ํฉ๋๋ค.
public class PostEnumConverter implements Converter<String, Category> {
@Override
public Category convert(String requestCategory) {
return Category.create(requestCategory.toUpperCase());
}
}
๊ทธ๋ฆฌ๊ณ import org.springframework.core.convert.converter.Converter;
์ ์๋ Converter ์ convert() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํฉ๋๋ค.
Request Parameter๋ก ์ ๋ ฅ๋๋ ๋ฌธ์์ด์ toUpperCase() ๋ฅผ ์ด์ฉํด์ ๋ชจ๋ ๋ฌธ์๋ฅผ ๋๋ฌธ์๋ก ๋ฐ๊ฟ์ค ํ ๋๊ฒจ์ค๋๋ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new PostEnumConverter());
}
}
๊ทธ๋ฆฌ๊ณ WebConfiguer์ addFormatters() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํ์ฌ ๋ง๋ Converter๋ฅผ ์ถ๊ฐํด์ค๋๋ค.
์ด์ ์ ๋์ํ๋์ง ํ์ธ์ ํด๋ด ๋๋ค.
/api/v1/posts/categories?category=QNA
๋ /api/v1/posts/categories?category=qna
๋ก ์์ฒญํ์ ๋, ์ ๋์ํ๋ ๊ฒ์ด ํ์ธ์ด ๋์ง๋ง,
/api/v1/posts/categories?category=
๋ผ๋์ง /api/v1/posts/categories?category=hi
์ ๊ฐ์ ์๋ชป๋ ์์ฒญ ์, ์๋ฌ ํธ๋ค๋ง์ด ๋์ง ์์์ ํ์ธํ์ต๋๋ค.
๋ก๊ทธ๋ฅผ ๋ณด๋ /api/v1/posts/categories?category=
์์ฒญ์ฒ๋ผ ์กด์ฌํ์ง ์๋ ๋ฌธ์์ด๋ก ์์ฒญ์ ํ๋ ๊ฒฝ์ฐ์๋ MissingServletRequestParameterException ์๋ฌ๊ฐ,
/api/v1/posts/categories?category=hi
์์ฒญ์ฒ๋ผ Enum ์ผ๋ก ์กด์ฌํ์ง ์๋ ๊ฐ์ ์์ฒญํ๋ ๊ฒฝ์ฐ์๋ MethodArgumentTypeMismatchException ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
@RestControllerAdvice
public class ExceptionManager {
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<?> converterExceptionHandler(MissingServletRequestParameterException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Response.error(ErrorCode.REQUEST_PARAM_NOT_MATCH.getMessage()));
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<?> converterSecondExceptionHandler(MethodArgumentTypeMismatchException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Response.error(ErrorCode.REQUEST_PARAM_NOT_MATCH.getMessage()));
}
}
๋ฐ๋ผ์ ์์ ๊ฐ์ด ์๋ฌ๋ฅผ ํธ๋ค๋งํ๋ ExceptionManager ๋ฅผ ํตํด ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์งํํ์ต๋๋ค.
์ ์์ ์ผ๋ก ์๋ฌ ์๋ต์ ํ์ธํ ์ ์๊ฒ ๋์์ต๋๋ค.
@GetMapping("/categories")
public ResponseEntity<Response<Page<PostGetListResponse>>> getAllByCategory(@RequestParam Category category, Pageable pageable) {
Page<PostGetListResponse> response = postService.getAllPostsByCategory(category, pageable);
return ResponseEntity.ok(Response.success(response));
}
๋๋ถ์ Controller์ ๋ฉ์๋๊ฐ ํจ์ฌ ๊น๋ํด์ ธ ๊ฐ๋ ์ฑ์ด ํฅ์๋์๊ณ , ํต์ฌ ๋ก์ง์๋ง ์ง์คํ ์ ์๊ฒ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
implementation 'org.springframework.boot:spring-boot-starter-validation'
์ validation ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด
Controller์์ @RequestBody ์ด๋ ธํ ์ด์ ์ผ๋ก ๋งคํํ๋ request dto์ ํ๋ ์ ํจ์ฑ์ ๊ฒ์ฌํ ์ ์์ต๋๋ค.
public class PostCreateRequest {
@NotBlank(message = "์ ๋ชฉ์ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.")
private String title;
@NotBlank(message = "๋ด์ฉ์ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.")
private String content;
@NotNull(message = "์ ํจํ์ง ์์ ์นดํ
๊ณ ๋ฆฌ๊ฐ ์
๋ ฅ๋์์ต๋๋ค.")
private Category category;
}
์์ ๊ฐ์ด ๋งคํ๋๋ ํ๋์ validation์์ ์ ๊ณตํ๋ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
์๋ฅผ ๋ค์ด, ์ Category ํ๋์ ์ฌ์ฉํ @NotNull ์ด๋ ธํ ์ด์ ์ ๊ฒฝ์ฐ null์ด ์ ๋ ฅ๋๋ ๊ฒ์ ํ์ฉํ์ง ์์ต๋๋ค.
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `com.growith.domain.post.Category` from String "category": not one of the values accepted for Enum class: [NOTICE, STUDY, QNA, COMMUNITY]
๊ทธ๋ฆฌ๊ณ BindingError๋ฅผ ํ์ธํ๊ธฐ ์ํด ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ๋ค, ์คํ์์ผ๋ณด๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
public enum Category {
QNA, COMMUNITY, STUDY, NOTICE;
}
}
์นดํ ๊ณ ๋ฆฌ์๋ ๋ค์๊ณผ ๊ฐ์ ์์๋ค์ด ์กด์ฌํ๋๋ฐ,
null ๊ฐ์ด ์ ๋ ฅ๋๋ ์ํฉ์ ์ฒ๋ฆฌํ ์ ์์์ง๋ง, Enum์ผ๋ก ์ ์๋์ง ์์ ๋ฌธ์์ด(์ ์์์์ category:"category")์ด ์ ๋ ฅ๋์ ๋๋ ์ฒ๋ฆฌ๊ฐ ๋์ง ์์ ์ํฉ์ด์์ต๋๋ค.
๋ํ, "qna" ์ ๊ฐ์ด ์๋ฌธ์๋ก ์ ๋ ฅ๋๋ ๊ฒฝ์ฐ์๋ ์ฒ๋ฆฌ๋์ง ์์์ต๋๋ค.
๊ทธ๋์ ๋์๋ฌธ์๋ ๊ตฌ๋ณํ์ง ์๊ณ ์ผ์นํ๋ฉด ์ ์์ ์ผ๋ก ๋ก์ง์ด ์คํ๋๊ณ ,
null์ด๊ฑฐ๋ enum์ผ๋ก ์ ์ํ์ง ์์ ๊ฐ์ด ๋ค์ด์ค๋ ๊ฒฝ์ฐ๋ง ์์ธ์ฒ๋ฆฌ๊ฐ ๋๋๋ก ๊ตฌํํ์์ต๋๋ค.
public enum Category {
QNA, COMMUNITY, STUDY, NOTICE;
}
@JsonCreator
public static Category parsing(String inputValue) {
return Stream.of(Category.values())
.filter(category -> category.toString().equals(inputValue.toUpperCase()))
.findFirst()
.orElse(null);
}
}
๋ฐฉ๋ฒ์ enum ๊ฐ์ฒด ์์ @JsonCreator ์ด๋ ธํ ์ด์ ๊ณผ ์์ฑ์๋ฅผ ๊ตฌํํ์ฌ Json๋ฐ์ดํฐ๋ฅผ ์ญ์ง๋ ฌํ ํ๋ ๊ณผ์ ์ ์๋ ์ค์ ํ๋ ๊ฒ์ ๋๋ค.
category ๋ฅผ ํค๊ฐ์ผ๋ก ํ๋ value๊ฐ์ ํ๋ผ๋ฏธํฐ๋ก์ inputValue๊ฐ์ผ๋ก ๋ฐ์์์ ๋ก์ง์ด ์ํ๋ฉ๋๋ค.
์ ๋ ฅ๋๋ inputValue๊ฐ์ toUpperCase() ๋ฉ์๋๋ก ๋๋ฌธ์๋ก ๋ณํ ํ,
์ผ์นํ๋ Category๊ฐ ์๋ค๋ฉด ๋ฐํํ๊ณ ์๋ค๋ฉด null์ ๋ฐํํ๋๋ก ํ์ต๋๋ค.
null์ด ๋ฐํ๋๋ฉด validation์ผ๋ก ์ค์ ํ @NotNull์ด๋ ธํ ์ด์ ์ ์ํด BindingError๊ฐ ๋ฐ์ํ ๊ฒ์ ๋๋ค.
๋ชฉํํ๋ ๋๋ก ์์ธ์ฒ๋ฆฌ๊ฐ ๋ ๊ฒ์ ํ์ธํ ์ ์์๊ณ , ์๋ฌ ์๋ต์ ํตํด ์ด๋ค ์๋ฌ๊ฐ ๋ฐ์ํ๋์ง ๋ช ์ํ ์ ์๊ฒ ๋์์ต๋๋ค.