validation AOP 적용하기

최준호·2022년 11월 20일
0

Spring

목록 보기
43/47
post-thumbnail

📗 Validation

spring에서 api 요청시 들어오는 값들을 체크해주는 라이브러리이다. validation을 반복적으로 작성하지 않아도 되도록 도와준다!

📄 Validation 적용

⌨️ gradle

dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.5'
}

⌨️ 코드 적용하기

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostBoardRequest {
    @NotEmpty(message = "제목을 입력해주세요.")
    @Size(max = 30, message = "제목은 30글자를 넘을 수 없습니다.")
    private String title;
    @NotEmpty(message = "내용을 입력해주세요.")
    @Size(max = 3000, message = "내용이 너무 깁니다!")
    private String content;
}

dto의 내용을 다음과 같이 @NotEmpty@Size를 통해 에러 메세지도 미리 지정해줄 수 있다.

@RestController
@RequestMapping("/v1/board")
@RequiredArgsConstructor
public class BoardController {
    private final BoardService boardService;

    @PostMapping("")
    public ResponseEntity<CommonApi<PostBoardResponse>> postBoard(@Valid @RequestBody PostBoardRequest postBoardRequest, BindingResult bindingResult){
        System.out.println("-------");
        
        // 원래대로라면 service 매개변수로 넘겨서 매번 체크해주어야 함.
        System.out.println(bindingResult.hasErrors());
        bindingResult.getFieldErrors().forEach(fieldError -> System.out.println(fieldError.getDefaultMessage()));

        System.out.println("-------");

        return ResponseEntity.status(HttpStatus.CREATED)
                .body(
                        CommonApi.<PostBoardResponse>builder()
                                .resultCode(ResultCode.SUCCESS)
                                .resultType(ResultType.NONE)
                                .data(boardService.postBoard(postBoardRequest))
                                .build()
                );
    }
}

테스트를 위해 다음과 같이 작성해주자.

여기서 Controller에서 유효성 체크를 하고자 하는 Dto에 @Valid 어노테이션을 꼭 추가해주어야 한다.

@SpringBootTest
@Execution(ExecutionMode.SAME_THREAD)
public class BoardControllerTest extends ControllerTest {
    @Autowired
    private BoardRepository boardRepository;

    private final String PREFIX = "/v1/board";

    @Test
    @DisplayName("title 존재하지 않은 게시물 등록은 실패한다.")
    void postBoardFail1() throws Exception {
        //given
        PostBoardRequest postBoardRequest = new PostBoardRequest(null, "내용만 입력");
        //when
        ResultActions perform = mockMvc.perform(post(PREFIX)
                .contentType(MediaType.APPLICATION_JSON)
                .content(convertToString(postBoardRequest)))
                .andDo(print());
        

        //then
    }
}   

그리고 다음과 같이 테스트 코드를 통해 실행해보자!

우리가 위에 적어둔 print 내용이 찍혀 나오는 것을 확인할 수 있었다.

물론 이를 통해서 Controller 다음에 실행될 각 Service에서 bindingResult를 체크해줄 순 있겠지만... 그거 너무 귀찮다. 그냥 AOP로 풀어보자!

📗 AOP가 적용된 Validation

📄 AOP 적용하기

⌨️ gradle

dependencies {
	...
    implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-aop:2.7.0'
}

⌨️ AOP 작성

@Slf4j
@Aspect
@Component
public class ValidAdvice {

    @Around(value = "execution(* com.molu.molu.controller..*.*(..))")
    public Object validAdviceHandler(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        for (Object arg : args) {
            BindingResult bindingResult = getBindingResult(arg);
            if(bindingResult == null) continue;
            if(bindingResult.hasErrors()){
                FieldError error = bindingResult.getFieldErrors().get(0);
                String errorMessage = error.getDefaultMessage();
                throw new IllegalArgumentException(errorMessage);
            }
        }
        return pjp.proceed();
    }

    private BindingResult getBindingResult(Object arg) {
        BindingResult bindingResult = null;
        if(arg instanceof BindingResult){
            bindingResult = (BindingResult) arg;
        }
        return bindingResult;
    }
}

aop가 적용되도록 작성해주었고 RestControllerAdvcie를 통해 IllegalArgumentException을 미리 정의해놔서 그냥 바로 Exception 터트렸다.

그리고 만약 테스트 코드에도 aop가 적용되게 해주려면

dependencies {
	...
    
	testImplementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-aop:2.7.0'
}

의존성을 추가로 작성해주어야 테스트 코드에서도 aop가 정상적으로 작동한다.

@SpringBootTest
@Execution(ExecutionMode.SAME_THREAD)
public class BoardControllerTest extends ControllerTest {
    @Autowired
    private BoardRepository boardRepository;

    private final String PREFIX = "/v1/board";

    @Test
    @DisplayName("title 존재하지 않은 게시물 등록은 실패한다.")
    void postBoardFail1() throws Exception {
        //given
        PostBoardRequest postBoardRequest = new PostBoardRequest(null, "내용만 입력");
        //when
        ResultActions perform = mockMvc.perform(post(PREFIX)
                .contentType(MediaType.APPLICATION_JSON)
                .content(convertToString(postBoardRequest)))
                .andDo(print());
        

        //then
    }
}

그리고 위에서 작성했던 내용으로 다시 시도하면

다음과 같이 잘 나오고

실제로 포스트맨을 사용해서 실행해도

잘 찍혀 나오는 것을 확인할 수 있다!

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글