바로 예제부터 확인해 보자.
@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));
}
@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));
}
코드를 보면 똑같은 유효성 검사 코드가 중복되어 계속 쓰이고 있다.
유효성 검사가 필요할 때마다 저 코드를 계속해서 사용해야 되는 문제가 존재한다.
AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라는 의미이다.
쉽게 말해 핵심적인 기능과 부가적인(공통) 기능을 나누고, 공통 기능을 따로 모듈화해서 재사용하는 것이다.
위 코드에서 핵심 기능은 댓글작성 기능과 유저정보 수정이 될 것이다.
그럼 공통 기능은 무엇일까?
핵심로직을 수행하기 전에 수행되는 유효성 검사가 공통 기능이다.
예를 들어 로그인 기능은 아래와 같이 공통 기능과 핵심 기능이 나뉜다.
이제 이 유효성 검사를 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(접근제어자 패키지.클래스.메서드(파라미터))
정리하자면 com.peeerr.instagram.controller.api
에 있는 모든 컨트롤러에서 파라미터가 0개 이상인 모든 메서드가 대상인 것이다.
이렇게 지정된 패턴의 메서드가 실행되면 이 apiAdvice 메서드가 먼저 실행된다.
ProceedingJoinPoint
: 지정된 메서드의 모든 정보(파라미터 등)에 접근할 수 있는 클래스
proceedingJoinPoint.proceed()
: 지정된 메서드로 다시 돌아가는 메서드
리턴타입은 Object로 해주어야 한다.
-> 지정된 메서드에서 반환하는 값을 반환해주어야 하기 때문
내부 코드에서는 getArgs()
로 파라미터들을 args 변수에 받아 온다.
그리고 파라미터들 중 BindingResult 가 존재한다면 유효성검사 코드를 실행한다.
-> 이렇게 구현하면 지정된 패턴의 메서드가 실행되기 전에 위 apiAdvice 메서드가 먼저 실행되어 파라미터에 BindingResult가 존재한다면 즉, 유효성검사(공통기능)가 필요하다면 여기서 수행하는 것이다.
-> 이렇게함으로써 재사용성을 확보하였다.
@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));
}
@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만 받는다면 자동으로 유효성 검사가 진행될 것이다.