Wanna be Brilliant Full-Stack Developer

2/21 SpringBoot 유효성 검사 및 자동화 AOP처리 본문

Back-End/Spring Boot

2/21 SpringBoot 유효성 검사 및 자동화 AOP처리

Flashpacker 2022. 2. 21. 18:39


package com.cos.photogramstart.web.dto.comment;

import lombok.Data;

@Data
public class CommentDto {
	private String content;
	private int imageId;
	
	// toEntity는 필요 없다.
}

 

imageId 나 content가 안들어오면 안되기 떄문에 둘다 NotBlank를 걸어야한다. 

package com.cos.photogramstart.web.dto.comment;

import javax.validation.constraints.NotBlank;

import lombok.Data;

@Data
public class CommentDto {
	@NotBlank
	private String content;
	@NotBlank
	private int imageId;
	
	// toEntity는 필요 없다.
}

이렇게 걸면 commentApiController에서 @Valid가 걸려야하고 애가 걸리면 바로 뒤에 bindingResult를 걸어야한다 이걸 걸면 if문이 필요하다 

package com.cos.photogramstart.web.api;

import java.util.HashMap;
import java.util.Map;

import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.config.auth.PrincipalDetails;
import com.cos.photogramstart.domain.comment.Comment;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.service.CommentService;
import com.cos.photogramstart.web.dto.CMRespDto;
import com.cos.photogramstart.web.dto.comment.CommentDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
public class CommentApiController {

	private final CommentService commentService;
	
	
	@PostMapping("/api/comment")
	public ResponseEntity<?> commentSave(@Valid @RequestBody CommentDto commentDto, 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);
		} 
		
		Comment comment =	commentService.댓글쓰기(commentDto.getContent(), commentDto.getImageId(), principalDetails.getUser().getId()); // content,imageId, userId 
		return new ResponseEntity<>(new CMRespDto<>(1, "댓글쓰기성공",comment),HttpStatus.CREATED);
	}
	
	@DeleteMapping("/api/comment/{id}")
	public ResponseEntity<?> commentDelete(@PathVariable int id) {
		commentService.댓글삭제(id);
		 return  new ResponseEntity<>(new CMRespDto<>(1, "댓글삭제성공", null),HttpStatus.OK);
	}
}

이렇게 에러가 있으면 유효성 검사 실패함 커스텀ValidationApiException 을 던지면된다!

이걸 테스트하려면 story.js에서 

/*   if (data.content === "") {
      alert("댓글을 작성해주세요!");
      return;
   }*/ 이렇게 주석처리하고 테스트한다! 

 

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.lang.Integer'. Check configuration for 'imageId' 이 오류에서는 Integer는 NotBlank를 못한다고한다 .blank는 null값도 확인하기떄문에

package com.cos.photogramstart.web.dto.comment;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

import lombok.Data;

@Data
public class CommentDto {
	@NotBlank // 빈값이거나 null 체크 그리고 빈 공백까지
	private String content;
	@NotEmpty // 빈값이거나 null 체크
	private int imageId;
	
	// toEntity는 필요 없다.
}

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Integer'. Check configuration for 'imageId'

또 이런 오류가 나왔는데 한번 구글링을 통해 확인해보려고한다 

package com.cos.photogramstart.web.dto.comment;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

import com.sun.istack.NotNull;

import lombok.Data;

// @NotNull = null 값 체크
// NotEmpty  빈값이거나 null 체크
// NotBlank  빈값이거나 null 체크 그리고 빈(스페이스) 공백까지


@Data
public class CommentDto {
	@NotBlank // 빈값이거나 null 체크 그리고 빈 공백까지
	private String content;
	@NotNull //  null 체크
	private Integer imageId;
	
	// toEntity는 필요 없다.
}


   story.js에서 댓글쓰기 오류에 responseJSON을 추가한다

// (4) 댓글쓰기
function addComment(imageId) {

   let commentInput = $(`#storyCommentInput-${imageId}`);
   let commentList = $(`#storyCommentList-${imageId}`);

   let data = {
      imageId:imageId,
      content: commentInput.val()
   }

/*   if (data.content === "") {
      alert("댓글을 작성해주세요!");
      return;
   }*/
   
   $.ajax({
      type:"post",
      url: "/api/comment",
      data:JSON.stringify(data),
      contentType:"application/json; charset=utf-8",
      dataType:"json"
      
   }).done(res=>{
      //console.log("성공",res);
      
      let comment = res.data;
      
      let content = `
        <div class="sl__item__contents__comment" id="storyCommentItem-${comment.id}""> 
          <p>
            <b>${comment.user.username} :</b>
            ${comment.content}
          </p>
          <button onclick="deleteComment(${comment.id})"><i class="fas fa-times"></i></button>
        </div>
	`;
   commentList.prepend(content);
   
   }).fail(error=>{
      
      console.log("오류",error.responseJSON);
   })


   commentInput.val("");  //인풋 필드를 깨끗하게 비워준다.
}

   }).fail(error=>{
      
      console.log("오류",error.responseJSON.data.content);
      alert(error.responseJSON.data.content);
   })

이렇게 추가하면

깔끔하게 프론트탄에서 서버단에서 유효성검사를 할수 있게되었다!


유효성 검사 AOP처리를 하려고한다

AOP는 무엇인가?? Aspect Orientied Programing 관점 지향 프로그래밍!?

개념은 간단하다

우리가 관점지향 프로그래밍으로 바꾼다는것이 아니라 객체지향프로그래밍을 하면서 관점지향 프로그래밍을 같이 쓴다는것이다. 

 

관점지형프로그래밍(AOP)란? 

내가 만약 로그인이라는 기능과 회원가입이라는 기능을 만들고 싶으면 로그인기능을 봤을때 핵심 1번 username과 passwrod를 잘받는것과 두번째는 DB에서 Select해서 있는지 찾아봐야한다. 확인한뒤 세번쨰 로그인이 되는것이다

 

회원가입은? 정보를 다 받는것이다, username, password 를 받고 DB에 INSERT한다는것이 핵심 기능인데

이핵심기능을 완성하기 위해서는 웹에서 해야할것이 많은데 

첫번쨰로 혹시나 username이나 password가 잘못나오지 않을까 해서 유효성검사를 해야한다

또한 앞단에서 보안처리도 해야한다! DB에 INSERT했으면 회원가입이 되고 - 다른기능에 따라 후처리가 필요할지도 모른다여기서도 몇시 몇분에 회원가입 했는지 로그를 남길수 있다. 

 

로그인 같은 경우도 전처리로 유효성검사와 보안처리도 해야하고 후처리에서 로그인이 끝나면 log같은걸 남길수도 있다이러한것들을 파일로도 남길수 있다. 

 

회원가입에서 기본 로직은  User user = signupDto.toEntitiy();
authService.회원가입(user); 이두줄이면 되지만 전처리나 후처리가 더 길수있다. 
핵심 기능은 로그인과 회원가입이 동일하지 않지만 전처리와 후처리가 동일하다. 이러한것들을 공통기능이라고 부른다. 공통기능과 핵심기능을 두개로 분리해서 핵심기능은 :우리가 실제로 컨트롤러나 서비스로 코드를 짜고공통기능은 따로 필터처리를 할 수 있다. 이러한부분을 필터에서 처리하면 코드를 안적어도된다. 그러면 훨씬 깔끔해진다. 그걸 우리가 해보려고한다

 

1. 핵심기능은 코드로 적고 공통기능은 AOP처리한다는것이다전처리 후처리 를 처리하려면 라이브러리가 하나 필요하다.https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop/2.4.5

pom.xml에 추가하고 handler에 패키지를 하나 추가한다! aop라는 패키지를 만들고 ValidationAdvice를 추가하는데

여기서 Advice는 공통 기능을 말한다! 

여기에 공통기능을 넣으려고하는데! 처음으로 @Aspect 추가해야 AOP를 처리할수있는 핸들러가 될수 있다.

애가 메모리에는 띄어야하니까 애는 설정파일이 아니니까 Component로 처리하면된다. 

왜냐하면 RestController, Service 이 모든 것들이 Component의 구현체들 이기떄문에 이 컴포넌트로 상속해서 만들어져있다. 

 

만약에 AuthController에서 signup 함수가 실행될떄 실행직전에 먼가 실행하고싶으면 @Before, signup 함수가 끝나고나서 실행하고 싶으면 @after , 전에도 실행하고 끝날때까지 관여하고싶으면 Around라는걸 사용하는데 

우리는 Around를 사용할것이다. 

 

애가 언제 등장할것인지에대한 주소를 작성해야하는데 

package com.cos.photogramstart.handler.aop;

import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {
	
	@Around("execution(*)")
	public Object apiAdvice() {
		
	}
	
	
	@Around
	public Object advice() {
		
	}
}

이 괄호안에 *은 어떤함수를 선택할것인지 , 이 *자리가 public함수로 할래? protected함수할래? 선택할수 있는건데 

이번에는 다할거라서 *로 할것이다. 그다음에 패키지 이름

package com.cos.photogramstart.handler.aop;

import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {
	
	@Around("execution(* com.cos.phtogramstart.web.api.*.Controller.*(..))")
	public Object apiAdvice() {
		
	}
	
	
	@Around("execution(* com.cos.phtogramstart.web.*.Controller.*(..))")
	public Object advice() {
		
	}
}

Return이 오브젝트로 되어있기 때문에 

package com.cos.photogramstart.handler.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {
	
	@Around("execution(* com.cos.photogramstart.web.api.*Controller.*(..))")
	public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		
		System.out.println("web api 컨트롤러====================");
		// proceedingJoinPoint = > profile 함수의 모든 곳에 접근할 수 있는 변수 
		//profile 함수보다 먼저 실행
		
		return proceedingJoinPoint.proceed(); // profile 함수가 실행됨.
	}
	
	
	@Around("execution(* com.cos.photogramstart.web.*Controller.*(..))")
	public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		System.out.println("web 컨트롤러 =======================");
		return proceedingJoinPoint.proceed();
	}
}

ProceedingJoinPoint 이게 무슨뜻이나면 내가 만약에 API컨트롤러중에 CommentAPiControlle에서 commentSave함수가 실행된다고 하면 ProceedingJoinPoint이거를 매개변수를 해놓으면  CommentSave에 모든 내부정보에 접근할수 있는 파라미터이다. 

 

밑에있는것도 만약 이미지 컨트롤러에 imageUpload가 실행이 되면 그 이미지업로드 내부에 있는 모든 정보에 접근할수 있는 proceedingJoinPoint라는 매개변수 전달을 해준다! 

 

밑에 return proceedingJoinPoin.proceed();는 머냐면 그 함수로 다시 돌아가라는것! 

 

우리가 하게 될것은 어떤 특정함수가 실행될때 실행되는 순간 이 함수의 모든 정보를 proceedingJoinPoint에 담고 

함수보다 먼저 실행된다! 

그리고  return proceedingJoinPoint.proceed(); 이 부분에서 profile 함수가 실행됨. 

이걸 안적으면 profile 함수가 실행이 안된다. 

Web Controller와 WebApi컨트롤러가 같이 작동한다. Web컨트롤러가 페이지로 가는거고 이 페이지 내부에서 api로 이 모든 데이터를 호출하기 떄문에!

 

또한 댓글을 남기면 API가 호출된다. 

만약 인기페이지로 가면 WEB 컨트롤러가 호출된다. 

일단은 접근을 하였고 이제는 유효성 체크를 하면된다. 

 

package com.cos.photogramstart.handler.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {
	
	@Around("execution(* com.cos.photogramstart.web.api.*Controller.*(..))")
	public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		
		System.out.println("web api 컨트롤러====================");
		Object[] args = proceedingJoinPoint.getArgs();
		for (Object arg : args) {
			System.out.println(arg);
		}
		// proceedingJoinPoint = > profile 함수의 모든 곳에 접근할 수 있는 변수 
		//profile 함수보다 먼저 실행
		
		return proceedingJoinPoint.proceed(); // profile 함수가 실행됨.
	}
	
	
	@Around("execution(* com.cos.photogramstart.web.*Controller.*(..))")
	public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		System.out.println("web 컨트롤러 =======================");
		Object[] args = proceedingJoinPoint.getArgs();
		for (Object arg : args) {
			System.out.println(arg);
		}
		return proceedingJoinPoint.proceed();
	}
}

Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
} 를 추가하였는데 매개변수가 어떤것이 있는지 뽑아보려고한다! 

 

근데 무한참조의 오류가나는데 User 에 Images에서 

이 유저 객체를 실행하면 ToString호출이 된다. ToString이 호출될때 

@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", password=" + password + ", name=" + name + ", website="
+ website + ", bio=" + bio + ", email=" + email + ", phone=" + phone + ", gender=" + gender
+ ", profileImageUrl=" + profileImageUrl + ", role=" + role + ", images=" + images + ", createDate="
+ createDate + "]";
} 여기에서 images를 호출하고있다. images안에 내부에 있는애들이 Sysout이 되면 무한참조를 하게 되면 JsonIgnoreProperties로는 안된다. 

 

저기에서 images를 지우면되는데

 

거기서 댓글을 등록하게되면

이 proceedingJoinPoin를 통해서 이 모든 함수들이 실행이 되고 

package com.cos.photogramstart.handler.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {
	
	@Around("execution(* com.cos.photogramstart.web.api.*Controller.*(..))")
	public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		
		System.out.println("web api 컨트롤러====================");
		Object[] args = proceedingJoinPoint.getArgs();
		for (Object arg : args) {
			if(arg instanceof BindingResult) {
				System.out.println("유효성 검사를 하는 함수입니다.");
			}
		}
		// proceedingJoinPoint = > profile 함수의 모든 곳에 접근할 수 있는 변수 
		//profile 함수보다 먼저 실행
		
		return proceedingJoinPoint.proceed(); // profile 함수가 실행됨.
	}
	
	
	@Around("execution(* com.cos.photogramstart.web.*Controller.*(..))")
	public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		System.out.println("web 컨트롤러 =======================");
		Object[] args = proceedingJoinPoint.getArgs();
		for (Object arg : args) {
			if(arg instanceof BindingResult) {
				System.out.println("유효성 검사를 하는 함수입니다.");
			}
		}
		return proceedingJoinPoint.proceed();
	}
}

회원가입을 할떄 자동유효성처리를 한다

package com.cos.photogramstart.handler.aop;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import com.cos.photogramstart.handler.ex.CustomValidationApiException;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {
	
	@Around("execution(* com.cos.photogramstart.web.api.*Controller.*(..))")
	public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		
		System.out.println("web api 컨트롤러====================");
		Object[] args = proceedingJoinPoint.getArgs();
		for (Object arg : args) {
			if(arg instanceof BindingResult) {
				System.out.println("유효성 검사를 하는 함수입니다.");
				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);
				} 
			}
		}
		// proceedingJoinPoint = > profile 함수의 모든 곳에 접근할 수 있는 변수 
		//profile 함수보다 먼저 실행
		
		return proceedingJoinPoint.proceed(); // profile 함수가 실행됨.
	}
	
	
	@Around("execution(* com.cos.photogramstart.web.*Controller.*(..))")
	public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		
		System.out.println("web 컨트롤러 =======================");
		Object[] args = proceedingJoinPoint.getArgs();
		for (Object arg : args) {
			if(arg instanceof BindingResult) {
				System.out.println("유효성 검사를 하는 함수입니다.");
			}
		}
		return proceedingJoinPoint.proceed();
	}
}

API컨트롤러에 공통적으로 가지고 있는 유효성 체크 구문을 이곳에 가져오고 다른 API컨트롤러는 삭제하고 테스트를 진행해본다. 

만약에 이프로젝트가 더 커진다면 이렇게 만들어놓으면 굉장히 편해진다! 

 

밑에도 들고와보려고하는데 밑에는 API쪽이 아니라 일반컨트롤러 쪽에 

package com.cos.photogramstart.handler.aop;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.handler.ex.CustomValidationException;

@Component // RestController, Service이 모든 것들이 Component를 상속해서 만들어져 있음.
@Aspect
public class ValidationAdvice {

	@Around("execution(* com.cos.photogramstart.web.api.*Controller.*(..))")
	public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

		System.out.println("web api 컨트롤러====================");
		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);
				}
			}
		}
		// proceedingJoinPoint = > profile 함수의 모든 곳에 접근할 수 있는 변수
		// profile 함수보다 먼저 실행

		return proceedingJoinPoint.proceed(); // profile 함수가 실행됨.
	}

	@Around("execution(* com.cos.photogramstart.web.*Controller.*(..))")
	public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

		System.out.println("web 컨트롤러 =======================");
		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 CustomValidationException("유효성 검사 실패함", errorMap);
				}
			}
		}
		return proceedingJoinPoint.proceed();
	}
}

우리가 만들어놨으니 유효성검사가 필요한곳에는 첫번쨰로는 DTO를 만들고 Valid, NOtBlank와 같은것들 다 걸어주고

파라미터 받을때 Valid, BindingResult만 넣어주면 끝난다.