[Spring AOP] 중복되는 Validation 코드 자동화 처리하기

peeerr·2023년 12월 30일
0

Spring

목록 보기
10/10
post-thumbnail

📌 1. 배경

바로 예제부터 확인해 보자.

1) 기존 댓글작성 코드

@PostMapping("/comment")
public ResponseEntity<?> saveComment(@Valid @RequestBody CommentRequest commentRequest,
                                     BindingResult bindingResult,
                                     @AuthenticationPrincipal PrincipalDetails principalDetails) {
    // 여기서부터
    if (bindingResult.hasErrors()) {
        Map<String, String> errorMap = new HashMap<>();

        for (FieldError error : bindingResult.getFieldErrors()) {
            errorMap.put(error.getField(), error.getDefaultMessage());
        }

        throw new CustomValidationApiException("유효성 검사 오류", errorMap);
    }
    // 여기까지의 코드만 주목

    String content = commentRequest.getContent();
    Long imageId = commentRequest.getImageId();
    Long principalId = principalDetails.getUser().getId();
    
    Comment comment = commentService.writeComment(content, imageId, principalId);
    return ResponseEntity.status(HttpStatus.CREATED)
            .body(new CMResponse<>(1, "댓글 추가 성공", comment));
}

2) 기존 유저정보 수정 코드

@PutMapping("/user/{id}")
public ResponseEntity<CMResponse<?>> update(@PathVariable Long id,
                                            @Valid UserUpdateRequest userUpdateRequest,
                                            BindingResult bindingResult,
                                            @AuthenticationPrincipal PrincipalDetails principalDetails) {
    // 여기서부터
    if (bindingResult.hasErrors()) {
        Map<String, String> errors = new HashMap<>();

        for (FieldError error : bindingResult.getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }

        throw new CustomValidationApiException("유효성 검사 오류", errors);
    }
    // 여기까지의 코드만 주목
    
    User user = userService.update(id, userUpdateRequest.toEntity());
    principalDetails.setUser(user);  // 세션 정보 업데이트 (회원정보 업데이트 창 반영)

    return ResponseEntity.ok()
            .body(new CMResponse<>(1, "회원정보 변경 성공", user));
}

  • 코드를 보면 똑같은 유효성 검사 코드가 중복되어 계속 쓰이고 있다.

  • 유효성 검사가 필요할 때마다 저 코드를 계속해서 사용해야 되는 문제가 존재한다.


📌 2. AOP 개념 및 적용하기

1) AOP란?

AOPAspect Oriented Programming의 약자로 관점 지향 프로그래밍이라는 의미이다.
쉽게 말해 핵심적인 기능부가적인(공통) 기능을 나누고, 공통 기능을 따로 모듈화해서 재사용하는 것이다.

위 코드에서 핵심 기능은 댓글작성 기능과 유저정보 수정이 될 것이다.
그럼 공통 기능은 무엇일까?

핵심로직을 수행하기 전에 수행되는 유효성 검사가 공통 기능이다.

예를 들어 로그인 기능은 아래와 같이 공통 기능과 핵심 기능이 나뉜다.

이제 이 유효성 검사를 AOP 처리하면 유효성 검사가 필요할 때마다 똑같은 코드를 작성하는 것이 아니라 재사용해 코드를 깔끔하게 정리할 수 있다.


2) AOP 처리하기

바로 코드부터 살펴보자.

ValidationAdvice.class

@Component
@Aspect
public class ValidationAdvice {

    @Around("execution(* com.peeerr.instagram.controller.api.*Controller.*(..))")
    public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();

        for (Object arg: args) {
            if (arg instanceof BindingResult) {
                BindingResult bindingResult = (BindingResult) arg;
            
                // 여기서부터
                if (bindingResult.hasErrors()) {
                    Map<String, String> errorMap = new HashMap<>();

                    for (FieldError error : bindingResult.getFieldErrors()) {
                        errorMap.put(error.getField(), error.getDefaultMessage());
                    }

                    throw new CustomValidationApiException("유효성 검사 실패", errorMap);
                }
                // 여기까지 기존 공통기능
                
            }
        }

        return proceedingJoinPoint.proceed();
    }
    
}

ValidationAdvice에서 Advice는 공통기능을 의미한다.

  • @Component: 빈으로 등록

  • @Aspect: 이 어노테이션을 붙여야 AOP를 처리하는 핸들러가 된다. (Aspect 클래스 선언할 때 사용)

  • @Around(패턴): 지정된 패턴에 해당하는 메서드가 실행되기 전과 후 모두에서 동작한다. (이외에 @Before, @After도 존재)

    -> 패턴: excution(접근제어자 패키지.클래스.메서드(파라미터))

    • 여기서 접근제어자란 protect, public 이런 것을 말하는 것이고 * 로 작성했다.
    • 이후 api 패키지안에 있는 모든 Controller를 대상으로 작동시킬 것이므로 *Controller로 작성했다.
    • 그리고 파라미터는 0개 이상이므로 ..로 표기하였다.

    정리하자면 com.peeerr.instagram.controller.api에 있는 모든 컨트롤러에서 파라미터가 0개 이상인 모든 메서드가 대상인 것이다.
    이렇게 지정된 패턴의 메서드가 실행되면 이 apiAdvice 메서드가 먼저 실행된다.

  • ProceedingJoinPoint: 지정된 메서드의 모든 정보(파라미터 등)에 접근할 수 있는 클래스

  • proceedingJoinPoint.proceed(): 지정된 메서드로 다시 돌아가는 메서드

  • 리턴타입은 Object로 해주어야 한다.
    -> 지정된 메서드에서 반환하는 값을 반환해주어야 하기 때문

  • 내부 코드에서는 getArgs()로 파라미터들을 args 변수에 받아 온다.

  • 그리고 파라미터들 중 BindingResult 가 존재한다면 유효성검사 코드를 실행한다.


-> 이렇게 구현하면 지정된 패턴의 메서드가 실행되기 전에 위 apiAdvice 메서드가 먼저 실행되어 파라미터에 BindingResult가 존재한다면 즉, 유효성검사(공통기능)가 필요하다면 여기서 수행하는 것이다.

-> 이렇게함으로써 재사용성을 확보하였다.


1) 수정된 댓글작성 코드

@PostMapping("/comment")
public ResponseEntity<?> saveComment(@Valid @RequestBody CommentRequest commentRequest,
                                     BindingResult bindingResult,
                                     @AuthenticationPrincipal PrincipalDetails principalDetails) {
    String content = commentRequest.getContent();
    Long imageId = commentRequest.getImageId();
    Long principalId = principalDetails.getUser().getId();

    Comment comment = commentService.writeComment(content, imageId, principalId);
    return ResponseEntity.status(HttpStatus.CREATED)
            .body(new CMResponse<>(1, "댓글 추가 성공", comment));
}

2) 수정된 유저정보 수정 코드

@PutMapping("/user/{id}")
public ResponseEntity<CMResponse<?>> update(@PathVariable Long id,
                                             @Valid UserUpdateRequest userUpdateRequest,
                                             BindingResult bindingResult,
                                             @AuthenticationPrincipal PrincipalDetails principalDetails) {
    User user = userService.update(id, userUpdateRequest.toEntity());
    principalDetails.setUser(user);  // 세션 정보 업데이트 (회원정보 업데이트 창 반영)

    return ResponseEntity.ok()
            .body(new CMResponse<>(1, "회원정보 변경 성공", user));
}

위 수정된 코드에선 공통기능 코드만 제거하고 핵심기능만 남겨둔 것이다.

이제 파라미터로 BindingResult만 받는다면 자동으로 유효성 검사가 진행될 것이다.

profile
개발 공부

0개의 댓글