저번에도 한번 AOP에 관해서 블로그를 쓴적이 있는데 다른 예시로 공부한 코드를 한번 더 작성해보려 합니당
AOP 는 공통 관심 사항을 분리시켜 메서드 중간에 코드를 주입하는것으로 이해를 했습니다.
은행의 계좌 입금 기능을 개발 할때
@PostMapping("/account/deposit") //인증이 필요없기에 /s제외 public ResponseEntity<?> depositAccount(@RequestBody @Valid AccountDepositReqDto accountDepositReqDto, BindingResult bindingResult){ AccountDepositRespDto accountDepositRespDto = accountService.계좌입금(accountDepositReqDto); return new ResponseEntity<>(new ResponseDto<>(1, "계좌 입금 완료", accountDepositRespDto), HttpStatus.CREATED); }
@Getter @Setter public static class AccountDepositReqDto{ @NotNull @Digits(integer = 4, fraction = 4) private Long number; @NotNull private Long amount; @NotEmpty @Pattern(regexp = "^(DEPOSIT)$") // private String gubun; @NotEmpty @Pattern(regexp = "^[0-9]{11}") private String tel; }
validation 어노테이션을 사용하여 유효성 검사를 하고자 합니다
@Getter @Setter public static class AccountDepositRespDto{ private Long id; //계좌ID private Long number; //계좌번호 private TransactionDto trasaction; //dto 안에 엔티티 들어올수 없다, 순환참조 될수 있따 public AccountDepositRespDto(Account account, Transaction transaction) { this.id = account.getId(); this.number = account.getNumber(); this.trasaction = new TransactionDto(transaction); //엔티티를 Dto로 변환 } @Getter @Setter public class TransactionDto{ private Long id; private String gubun; private String sender; private String receiver; private Long amount; private String tel; private String createdAt; @JsonIgnore //내 계좌가 아니기에 가려준다 json 응답시 테스트시에는 제거 하고 확인 private Long depositAccountBalance; public TransactionDto(Transaction transaction) { this.id = transaction.getId(); this.gubun = transaction.getGubun().getValue(); this.sender = transaction.getSender(); this.receiver = transaction.getReceiver(); this.amount = transaction.getAmount(); this.depositAccountBalance = transaction.getDepositAccountBalance(); this.tel = transaction.getTel(); this.createdAt = CustomDateUtil.toStringFormat(transaction.getCreatedAt()); } } }
@Component @Aspect public class CustomValidationAdvice { @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void postMapping(){} @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void putMapping(){} //postMapping, putMapping 두군데다 하겠다 @Around("postMapping() || putMapping()") //joinpoint 의 전후 제어 public Object validationAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object[] args = proceedingJoinPoint.getArgs(); //joinpoint의 매개변수들, joinpoint는 메서드들 for(Object arg : args){ if(arg instanceof BindingResult){ //BindingResult를 상속받거나 인스턴스면 BindingResult bindingResult = (BindingResult) arg; //형변환 if(bindingResult.hasErrors()){ //bindingresult에 에러가 있으면 Map<String, String> errorMap = new HashMap<>(); for(FieldError error : bindingResult.getFieldErrors()){ errorMap.put(error.getField(), error.getDefaultMessage()); //에러들이 담긴다. } throw new CustomValidationException("유효성 검사 실패", errorMap); } } } return proceedingJoinPoint.proceed(); } }
@Getter public class CustomValidationException extends RuntimeException { private Map<String, String> erroMap; public CustomValidationException(String message, Map<String, String> erroMap) { super(message); this.erroMap = erroMap; } }
코드를 설명 하자면 Http의 바디만 있는 PostMapping, PutMapping 에 유효성 검사를 진행 해주면 되는데
PostMapping, PutMapping 의 컨트롤러가 동작 할 때 Dto에 사용 된 Validation 어노테이션에 의해 유효성 검사가 어긋나면 에러가 bindingResult 에 담기는데 HashMap 컬렉션을 사용하여 String의 key, value 값을 담아서 throw new CustomValidationException("유효성 검사 실패", errorMap); 를 통해 익셉션 처리를 해주는 코드이다.
bindingresult 에 에러가 없으면 해당 컨트롤러 메서드를 정상적으로 진행한다.
ProceedingJoinPoints는 joinpoint의 자손 클래스로 Aop가 적용되는 메서드들이다. (PostMapping, PutMapping)
필터나 인터셉로도 적용을 할수 있지 않을까 싶지만
인터셉터나 필터는 메서드 파라미터에 접근이 안되고 반면, AOP는 리플렉션이 적용되어 있어서 메서드 파라메터에 접근할 수 있기 떄문에 AOP로 처리가 가능하다고 한다.
그리고 필터, 인터셉터, AOP에 대한 인상적인 비유를 봤는데
필터는 아파트 A동 입구
인터셉터는 A동에 1102호 입구 (본인 집 문앞)
AOP는 아파트 전체를 관리
AOP는 서버에 무리가 많이 가는 작업이기 때문에 연산이 많아 되도록이면 적게 쓰는 것이 좋다고 한다.매개변수나, 필드등에 접근해야 할 이유가 없다면, 혹은 어노테이션을 분석해야할 이유가 없다면 굳이 다른 방법이 있다면 젤 마지막에 사용!!
그럼 필터와 인터셉터가 남는데?
아파트 입구에서 잡상인을 막는것과, 문앞에서 막는것은 차원이 다른 문제라는 글을 보니 인상적이었습니다.