개발 일기

Spring AOP 를 활용해보자! - BindingResult 본문

Spring

Spring AOP 를 활용해보자! - BindingResult

dev-jo 2022. 1. 17. 15:58

https://github.com/pursue503/study-tdd

 

GitHub - pursue503/study-tdd: TDD

TDD . Contribute to pursue503/study-tdd development by creating an account on GitHub.

github.com

 

 

최근 백엔드 지인과 TDD 로 회원가입 및 게시판을 만드는 간단한 프로젝트를 진행하였다.

 

해당 프로젝트를 진행하는 도중에

 

컨트롤러에서 중복된 코드가 많이 발생하였는데

 

코드는 아래처럼 되어 있었다.

 

 

 

@Slf4j
@RequiredArgsConstructor
@RestController
public class PostController {

    private final PostService postService;

    /**
     * 게시물 저장 요청을 받아 저장 처리후 반환값으로 저장된 게시물의 postId를 반환합니다.
     *
     * @param dto    게시물 제목, 게시물 내용, 이미지 주소 (선택사항)
     * @return 성공적으로 저장된 게시물의 고유 아이디
     */
    @PostMapping("/posts")
    public ResponseDTO<Long> savePost(@Valid @RequestBody PostSaveRequestDTO dto, BindingResult result) {
        if (result.hasErrors()) {
            throw new InvalidPostParameterException(result, GeneralParameterErrorCode.INVALID_PARAMETER)
        }
        return new ResponseDTO<>(postService.savePost(dto), PostMessage.SAVE_POST_SUCCESS, HttpStatus.OK);
    }

    @GetMapping("/posts/{postId}")
    public ResponseDTO<PostOneDTO> findOnePost(@PathVariable Long postId) {
        return new ResponseDTO<>(postService.findOnePost(postId), PostMessage.FIND_POST_ONE_SUCCESS, HttpStatus.OK);
    }

    @GetMapping("/posts")
    public ResponseDTO<PageResponseDTO> findPostPage(@RequestParam int page) {
        return new ResponseDTO<>(postService.findPostsPage(page), PostMessage.FIND_POST_PAGE_SUCCESS, HttpStatus.OK);
    }

    @PatchMapping("/posts")
    public ResponseDTO<UpdatedPostDTO> updateOnePost(@Valid @RequestBody PostPatchRequestDTO dto, BindingResult result) {
        if (result.hasErrors()) {
            throw new InvalidPostParameterException(result, GeneralParameterErrorCode.INVALID_PARAMETER)
        }
        return new ResponseDTO<>(postService.updateOnePost(dto), PostMessage.UPDATE_POST_SUCCESS, HttpStatus.OK);
    }

    @DeleteMapping("/posts/{postId}")
    public ResponseDTO<LocalDateTime> deleteOnePost(@PathVariable Long postId) {
        return new ResponseDTO<>(postService.deleteOnePost(postId), PostMessage.DELETE_POST_SUCCESS, HttpStatus.OK);
    }
}

 

여기서 지금 중복되고 있는 코드는 BindResult 부분이다

 

BindResult Valid 에서는 따로 설명을 하지 않겠다.

 

이 코드를 공통으로 묶어서 처리할 방법이 없을까 라는 생각을 가지게 되었는데

 

지난달에 토비의 스프링을 학습하면서

AOP 부분을 본 기억이 났다. 이 코드를 Spring AOP 로 묶어서 처리해볼수 없을까?

 

한번 바로 적용을 해봤다.

 

https://dev-jo.tistory.com/58?category=947368 

 

토비의 스프링 6장 AOP

노션을 참고하면 더욱 좋습니다. 6장 AOP AOP 는 IoC , DI, 서비스 추상화와 더불어 스프링 3대 기반기술의 하나이다. AOP는 정말 중요한 개념이다 해당 장을 이용하여 자세히 알아보도록 하자 6.1 트랜

dev-jo.tistory.com

 

적용한 코드는 아래처럼 변경되었다.

 

 

BindingReuslt 의 if hasErrors가 사라진 컨트롤러

 

@Slf4j
@RequiredArgsConstructor
@RestController
public class PostController {

    private final PostService postService;

    /**
     * 게시물 저장 요청을 받아 저장 처리후 반환값으로 저장된 게시물의 postId를 반환합니다.
     *
     * @param dto    게시물 제목, 게시물 내용, 이미지 주소 (선택사항)
     * @return 성공적으로 저장된 게시물의 고유 아이디
     */
    @PostMapping("/posts")
    public ResponseDTO<Long> savePost(@Valid @RequestBody PostSaveRequestDTO dto, BindingResult result) {
        return new ResponseDTO<>(postService.savePost(dto), PostMessage.SAVE_POST_SUCCESS, HttpStatus.OK);
    }

    @GetMapping("/posts/{postId}")
    public ResponseDTO<PostOneDTO> findOnePost(@PathVariable Long postId) {
        return new ResponseDTO<>(postService.findOnePost(postId), PostMessage.FIND_POST_ONE_SUCCESS, HttpStatus.OK);
    }

    @GetMapping("/posts")
    public ResponseDTO<PageResponseDTO> findPostPage(@RequestParam int page) {
        return new ResponseDTO<>(postService.findPostsPage(page), PostMessage.FIND_POST_PAGE_SUCCESS, HttpStatus.OK);
    }

    @PatchMapping("/posts")
    public ResponseDTO<UpdatedPostDTO> updateOnePost(@Valid @RequestBody PostPatchRequestDTO dto, BindingResult result) {
        return new ResponseDTO<>(postService.updateOnePost(dto), PostMessage.UPDATE_POST_SUCCESS, HttpStatus.OK);
    }

    @DeleteMapping("/posts/{postId}")
    public ResponseDTO<LocalDateTime> deleteOnePost(@PathVariable Long postId) {
        return new ResponseDTO<>(postService.deleteOnePost(postId), PostMessage.DELETE_POST_SUCCESS, HttpStatus.OK);
    }
}

 

그리고 모든 컨트롤러에 Before 에서 BindResult 로직을 수행할 AOP가 추가되었다.

 

@Slf4j
@Component
@Aspect
public class BindingResultAop {


    /**
     *
     * api 패키지 내부 컨트롤러 실행전에 실행
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Before("execution(* study.tdd.simpleboard.api..*Controller.*(..))")
    public void bindingBefore(JoinPoint joinPoint) throws Throwable {
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof BindingResult) {
                BindingResult result = (BindingResult) arg;

                if (result.hasErrors()) {
                    throw new InvalidPostParameterException(result, GeneralParameterErrorCode.INVALID_PARAMETER);
                }

            }
        } // end for
    } // end

}

 

이 코드를 설명하자면

 

모든 컨트롤러가 실행되기전에 실행되고

 

JoinPoint.getArgs() 컨트롤러의 모든 파라미터를 가져와서

반복문을 통해 현재 Object 가 BindResult 로 instanceof 변환이 가능하다면

 

캐스팅을 한 후 공통 로직을 수행하도록 설정했다.

 

추후 리팩토링 검증을 위해 모든 테스트 코드를 실행해 보았고.

모든 테스트 코드가 통과되었다.

 


 

1. Spring AOP 는 컨트롤러 말고 서비스단에서도 가능하다 ( 유저 처리 공통 로직 등 )

2. Spring AOP 는 정말 대단하다..

 


 

2022. 01. 24 추가

 

이전 코드에는 BindResult도 파라미터에서 제거하였는데

제거 하면 AOP에서 BIndReuslt 를 못잡습니다.

그래서 if has errors의 코드만 제거하고 파라미터는 그대로 둬야 합니다.

https://github.com/pursue503/study-tdd/pull/22