๊ธฐ์กด ๋ฐฉ์
implementation 'org.springframework.boot:spring-boot-starter-validation'
์ validation ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด
Controller ์์ @RequestBody ์ด๋ ธํ ์ด์ ์ผ๋ก ๋งคํํ๋ Request Dto ์ ํ๋ ์ ํจ์ฑ์ ๊ฒ์ฌํ ์ ์๋ค.
public class UserCreateRequest {
@Email(message = "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค. '@' ๋ฅผ ํฌํจ์์ผ์ฃผ์ธ์.")
private String email;
@Length(min = 8,message = "๋น๋ฐ๋ฒํธ๋ ์ต์ 8์ ์ด์์
๋๋ค.")
private String password;
}
์์ ๊ฐ์ด ๋งคํ๋๋ ํ๋์ validation
์์ ์ ๊ณตํ๋ ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ๋ฉด ๋๋ค.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserApiController {
private final UserService userService;
@PostMapping
public ResponseEntity<Response<UserCreateResponse>> create(@RequestBody @Validated UserCreateRequest requestDto, BindingResult br) {
if (br.hasErrors()) {
throw new BindingException(br.getFieldError().getDefaultMessage());
}
UserCreateResponse response = userService.createUser(requestDto);
return ResponseEntity.ok(Response.success(response));
}
}
Controller์ ๋ฉ์๋์์ @Validated
๋ฅผ ์ ํจ์ฑ์ ๊ฒ์ฆํ ๊ฐ์ฒด ์์ ๋ถ์ด๊ณ
๋ฐ๋ก ๋ค์ BindingResult
๊ฐ์ฒด๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ์ถ๊ฐํด์ฃผ๋ฉด ์์ธ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋ค.
๐จ ๊ผญ, ๋ฐ๋ก ๋ค ํ๋ผ๋ฏธํฐ์ BindingResult ๊ฐ์ฒด๋ฅผ ์ถ๊ฐํด์ฃผ์ด์ผ ํ๋ค.
์ด๋ ๊ฒ ์ ์ฉํ๊ณ ๋๋, ๋ ๊ฐ์ ํ๊ณ ์ถ์ ๋ถ๋ถ์ด ๋ณด์๋ค.
if (br.hasErrors()) {
throw new BindingException(br.getFieldError().getDefaultMessage());
}
๐ก validation์ ํตํด ์์ธ์ฒ๋ฆฌ๋ฅผ ํ๋ค๋ณด๋ฉด, ์ด ๋ถ๋ถ์ด ๊ณตํต์ ์ผ๋ก ์ฌ์ฉ๋ ๊ฒ์ด ์์๋์๋ค.
AOP ๋ฅผ ํ์ฉํด ์ปค์คํ ์ด๋ ธํ ์ด์ ์ ๋ง๋ค์ด ๋ฐ์ธ๋ฉ ์ฒดํฌ๋ฅผ ํ๋ค๋ ๊ฒ์ ๋ช ์ํ๊ณ
๋ฉ์๋ ์์๋ ํต์ฌ ๋ก์ง์ผ๋ก๋ง ๊ตฌ์ฑํด ๊ฐ๋ ์ฑ์ ๋์ด๋ ๋ฐฉ์์ผ๋ก ๊ฐ์ ํด์ผ๊ฒ ๋ค ์๊ฐํ๋ค.
AOP ๋ฅผ ํ์ฉํด ๊ฐ์ ํด๋ณด์
// ์๋ฐ 17๋ฒ์ , ์คํ๋ง ๋ถํธ 3.1.2 ๊ธฐ์ค
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '3.1.2'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '3.1.2'
AOP๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํด์ค๋ค.
๊ทธ๋ฌ๋ฉด ์คํ๋ง ๋ถํธ ์๋ ์ค์ ์ผ๋ก AnnotationAwareAspectJAutoProxyCreator
๋ผ๋ ๋น ํ์ฒ๋ฆฌ๊ธฐ๊ฐ ์คํ๋ง ๋น์ ๋ฑ๋ก๋๋ค.
๊ทธ๋ฌ๋ฉด ์ด ๋น ํ์ฒ๋ฆฌ๊ธฐ๊ฐ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋ Advisor
๋ฅผ ์๋์ผ๋ก ์ฐพ์์ ํ์ํ ๊ณณ์ ํ๋ก์๋ฅผ ์ ์ฉํ๊ณ
Advisor : ๋ถ๊ฐ๊ธฐ๋ฅ๊ณผ ๋ถ๊ฐ๊ธฐ๋ฅ์ ์ ์ฉํ ๋์์ ์๊ณ ์๋ ์ค๋ธ์ ํธ
ํ๋ก์ ๊ฐ์ฒด์์ ์ํ๋ ๋ถ๊ฐ๊ธฐ๋ฅ์ ์คํ์ํค๊ณ ์๋ณธ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ฑฐ๋, ์๋ณธ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ ๋ถ๊ฐ๊ธฐ๋ฅ์ ์คํ์ํค๋๋ก ํ ์ ์๋ค.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BindingCheck {
}
์์ ๊ฐ์ ์ปค์คํ ์ด๋ ธํ ์ด์ ์ ์ ์ํ๋ค.
@Aspect
@Component
public class BindingCheckAop {
@Before(value = "@annotation(com.wanted.global.annotation.BindingCheck)")
public void before(JoinPoint joinPoint) {
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof BindingResult) {
BindingResult br = (BindingResult) arg;
if (br.hasErrors()) {
throw new BindingException(br.getFieldError().getDefaultMessage());
}
}
}
}
}
์์ ๊ฐ์ด @Aspect
์ @Component
์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ๋ฉด ๋น ํ์ฒ๋ฆฌ๊ธฐ๊ฐ ๋ด๊ฐ ์ ์ํ Advisor
๋ฅผ ์ฐพ์ ์ ์๋ค.
@Before
์ value
์ต์
๊ฐ์ ์ปค์คํ
์ด๋
ธํ
์ด์
์ ๊ฒฝ๋ก ์ ๋ณด๋ฅผ ์
๋ ฅํ๋ค.
์ด ์ด๋
ธํ
์ด์
์ด ์ ์ฉ๋ ๋ฉ์๋๊ฐ ์คํํ๊ธฐ ์ , before()
์ ์ ์ํ ๋ก์ง์ด ์คํ๋๋๋ก ์ค์ ํ์๋ค.
JoinPoint
๊ฐ์ฒด๋ฅผ ํตํด์ ์ปค์คํ
์ด๋
ธํ
์ด์
์ด ์ ์ฉ๋ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ์ ์ ๊ทผํ ์ ์๊ณ
BindingResult
๊ฐ์ฒด์ ์ ๊ทผํด ์๋ฌ๊ฐ ์๋์ง ํ์ธํ๋ ๋ฉ์๋์ด๋ค.
๊ฐ์ ํ
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserApiController {
private final UserService userService;
@PostMapping
@BindingCheck
public ResponseEntity<Response<UserCreateResponse>> create(@RequestBody @Validated UserCreateRequest requestDto, BindingResult br) {
UserCreateResponse response = userService.createUser(requestDto);
return ResponseEntity.ok(Response.success(response));
}
}
Controller ์ฝ๋๊ฐ ๊น๋ํด์ก๋ค.
ํ ์คํธ ์ฝ๋
๋จผ์ , Controller ํ ์คํธ๋ฅผ WebMvcTest์ ์ง์ ์ค์ ํ Spring Security๋ฅผ import ํด์ ์ฌ์ฉํ๊ณ ์์๋ค.
ํ ์คํธ ์ฝ๋์์๋ AOP ์ ์ฉ์ ์ํด์๋, ์ถ๊ฐ ์ด๋ ธํ ์ด์ ์ด ํ์ํ๋ค.
@WebMvcTest(value = UserApiController.class)
@EnableAspectJAutoProxy
@Import({SecurityConfig.class, BindingCheckAop.class})
class UserApiControllerTest {
.....
}
@EnableAspectJAutoProxy
์ ์ ์ํ Advisor
ํด๋์ค์ธ BindingCheckAop.class
๋ฅผ ์ถ๊ฐํด์ฃผ์๋ค.
private static Stream<Arguments> createUserFailScenarios() {
return Stream.of(
Arguments.of(EMAIL_BINDING_ERROR_MESSAGE, UserCreateRequest.builder().email("email").password("12345678").build()),
Arguments.of(PASSWORD_BINDING_ERROR_MESSAGE, UserCreateRequest.builder().email("email@email.com").password("1234567").build())
);
}
@DisplayName("Request Dto ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ")
@ParameterizedTest
@MethodSource("createUserFailScenarios")
void createUser_error_bindingError(String errorMessage, UserCreateRequest request) throws Exception {
mockMvc.perform(post("/api/v1/users")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").exists())
.andExpect(jsonPath("$.message").value("ERROR"))
.andExpect(jsonPath("$.result").exists())
.andExpect(jsonPath("$.result").value(errorMessage));
}
์์ ๊ฐ์ด Controller ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์๋ค.
์ฒซ๋ฒ์งธ ์๋๋ฆฌ์ค๋ @๊ฐ ํฌํจ๋์ด์์ง ์์ email์ ๋ด์ ์์ฒญํ๊ณ
๋๋ฒ์งธ ์๋๋ฆฌ์ค๋ 8์๋ฆฌ๋ณด๋ค ์งง์ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ด์ ์์ฒญํ๋ค.
์ํ๋ ํ ์คํธ ๊ฒฐ๊ณผ๊ฐ ๋์๋ค.