ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링부트+JPA - 인스타그램 클론 코딩 03 - 2 AOP/회원가입 마무리
    스프링/스프링부트+JPA - 인스타 2021. 9. 23. 12:08

    이지업 최주호 강사님 강의 참조

     

     

     

    01 중복회원가입 / 글자수 제한 걸기 (전처리/후처리)

     

    ① 모델의 username에 어노테이션으로 제한 걸기

    	@Column(unique = true, length = 20)
    	private String username;
    • 글자수 제한은 전처리에서, 중복검사는 후처리에서 가능하다!! (validation + exception handler)
    • 여기서 AOP 개념이 나온다. 회원가입이 핵심이기능, 그외 기능을 공통기능이라고 부른다. 
    • AOP를 구현해서 컨트롤러는 컨트롤러에 일만 하도록 만들고 (엄청 코드가 깔끔해짐) 오류시에 클라이언트를 위한 페이지 또한 따로 만들어보자

     

    • AOP를 위한 pom.xml 추가
      public String signup(@Valid SignupDto signupDto) {
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

     

    • 이제 @Valid 라는 AOP 라이브러리가 사용이 가능하다. 필요한 메서드에 걸어주자 
    public String signup(@Valid SignupDto signupDto) {
    • DTO에서 넘어오는 데이터에 대한 유효성검사이니 당연히 데이터를 가져오는 파라미터에 걸어야한다. DTO로 가서 나머지 세팅을 하자
      (https://bamdule.tistory.com/35) @Valid 기능 및 설명 블로그 참조!
    @Data
    public class SignupDto {
    	
    	@Size(min = 2, max = 20)
    	@NotBlank
    	private String username;
    	@NotBlank
    	private String password;
    	@NotBlank
    	private String email;
    	@NotBlank
    	private String name;
    	
    	public User toEntity() {
    		return User.builder()
    				.username(username)
    				.password(password)
    				.email(email)
    				.name(name)
    				.build();
    	}
    }

     

    • 모델에도 필요한 부분에 유효성 검사를 걸어 DB에 체크를 할 수 있도록 하자
    public class User {
    	..
    	@Column(nullable = false)
    	private String password;
    	@Column(nullable = false)
    	private String name;
    	..
        	@Column(nullable = false)
    	private String email;

    → DB 세팅이 바뀌면 반드시 YML 'ddl-auto' 부분을 건드려서 DB업데이트를 하자

     

     

    • 다시 컨트롤러로 돌아가 오류들을 담을 수 있는 'BindingResult'를 걸고 테스트
    	@PostMapping("/auth/signup")
    	public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) {
    		
    		if(bindingResult.hasErrors()) {
    			Map<String, String> errorMap = new HashMap<>();
    			for (FieldError error : bindingResult.getFieldErrors()) {
    				errorMap.put(error.getField(), error.getDefaultMessage());
    				System.out.println(error.getDefaultMessage());
    			}
    		}
            ....
    • 현재 걸린 @Valid기능들을 통해 모든 오류들(개수 제한 x)는 bindingResult에 담김
    • bindingResult.hasErrors() → 오류가 있으면 if문을 탐
    • errorMap 변수에 HashMap을 주고 사용가능하도록 선언
    • 포문을 돌면서 'FeiledError'라는 객체의 기능(오류를 변수에 담을 수 있게 함)을 가진 'error'에 bindingResult에 담긴 에러들을 하나씩 넣음
    • 에러가 있으면 errorMap의 해쉬를 통해 담음 

    → 이제 분기를 해서 회원가입을 마무리 하자!

     

     

     

    02 분기 후 데이터 리턴

    • 테스트를 해보면 프론트단에서 빈값은 이동하지 못하도록 막아놨다. 그렇다면 왜 백엔드에서 다시 한번 유효성 검사를 할까?
      → 프론트에서 못막는 POSTMAN으로 가입을 요청한다던지 다른 경로로 오는 것을 막아야 하기때문!
    • 분기를 시킬 때 return 을 데이터/파일 구분 해서 둘다 한 메소드에 구현하는것 또한 좋지 않다. 더해서 현재 오류가 터지면 STS 오류 페이지를 그대로 사용자에게 돌려주기때문에 이것도 포함하여 해결해보자. 
    • 컨트롤러에 오류를 캐치하는 문구 추가 (AuthController의 회원가입 메소드)
    @RequiredArgsConstructor
    @Controller
    public class AuthController {
    	...
        	//회원가입요청
    	@PostMapping("/auth/signup")
    	public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) {
    		
    		if(bindingResult.hasErrors()) {
    			Map<String, String> errorMap = new HashMap<>();
    			for (FieldError error : bindingResult.getFieldErrors()) {
    				errorMap.put(error.getField(), error.getDefaultMessage());
    			}
    			throw new RuntimeException("Failed");

    @RestController
    @ControllerAdvice
    public class ControllerExceptionHandler {
    	
    	@ExceptionHandler(RuntimeException.class)
    	public String validationException(RuntimeException e) {
    		return e.getMessage();
    	}
    }
    • AOP강의에서 배웠던 것이 그대로 나왔다
    1. @RestController → return을 데이터 형태로 돌려주기 위함 (즉 e.getMessage()의 메세지를 그대로 데이터형태로)
    2. @ControllerAdvice → 하나가 아닌 모든 @Controller 즉 전역에서 발생하는 예외를 잡아 처리하는 역할
    3. @ExceptionHandler → @Controller/@RestController 가 적용된 Bean내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리하도록 만듬

     

    • 문제는 런타임 오류 메세지가 아닌 errorMap에 담긴 오류의 종류가 리턴되어야한다. 처리해보자

    public class CustomValidationException extends RuntimeException {
    
    	//객체 구분용 - 중요하지 않음
    	private static final long serialVersionUID = 1L;
    	
    	private Map<String, String> errorMap;
    	
    	public CustomValidationException(String message, Map<String, String> errorMap) {
    		super(message);
    		this.errorMap = errorMap;
    	}
    	
    	public Map<String, String> getErrorMap(){
    		return errorMap;
    	}
    }
    • Message는 부모 클래스가 처리를 해주기때문에 자체 세팅이 필요없고 errorMap은 직접 받아줘야 한다
    • RuntimeException대신 이 핸들러를 받아주면 된다. 
    @RestController
    @ControllerAdvice
    public class ControllerExceptionHandler {
    	
    	@ExceptionHandler(CustomValidationException.class)
    	public Map<String, String> validationException(CustomValidationException e) {
    		return e.getErrorMap();
    	}
    }
    @RequiredArgsConstructor
    @Controller
    public class AuthController {
    	...
        	//회원가입요청
    	@PostMapping("/auth/signup")
    	public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) {
    		
    		if(bindingResult.hasErrors()) {
    			Map<String, String> errorMap = new HashMap<>();
    			for (FieldError error : bindingResult.getFieldErrors()) {
    				errorMap.put(error.getField(), error.getDefaultMessage());
    			}
    			throw new CustomValidationException("Failed", errorMap);

    굿

     

     

     

    03 분기 후 스트링 리턴 및 공통 응답 DTO 만들기 

    • 마지막으로 "Failed"이라는 문자열도 띄우도록 해보자

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class CMRespDto {
    	private String message;
    	private Map<String, String> errorMap;
    }
    • 핸들러의 타입을 이 공통DTO로 바꿔주면 된다.
    @RestController
    @ControllerAdvice
    public class ControllerExceptionHandler {
    	
    	@ExceptionHandler(CustomValidationException.class)
    	public CMRespDto validationException(CustomValidationException e) {
    		return new CMRespDto(e.getMessage(),e.getErrorMap());
    	}
    }

    → 테스트

    굿!

    • 조금 더 디테일하게 만들기 위해서 제네릭을 써서 모든 데이터 형태를 처리 가능하도록 해보자
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class CMRespDto<T> {
    	private int code; //1(성공), -1(실패)
    	private String message;
    	private T data;
    }
    @RestController
    @ControllerAdvice
    public class ControllerExceptionHandler {
    	
    	@ExceptionHandler(CustomValidationException.class)
    	public CMRespDto<?> validationException(CustomValidationException e) {
    		return new CMRespDto<Map<String,String>>(-1, e.getMessage(), e.getErrorMap());
    	}
    }

    • 제네릭을 써서 모든 형태 처리가 가능하다 (getErrorMap() 자리에 원하는 타입의 데이터를 넣으면 된다)

     

     

     

     

    • 위에 형태도 좋긴 하나 UX측면을 더 부각시켜 보자 (Script 세팅)

    public class Script {
    	
    	public static String back(String msg) {
    		StringBuffer sb = new StringBuffer();
    		sb.append("<script>");
    		sb.append("alert('"+msg+"');");
    		sb.append("history.back();");
    		sb.append("</script>");
    		return sb.toString();
    	}
    }
    • 경고창을 띄우고 뒤로가게하는 기능
    @RestController
    @ControllerAdvice
    public class ControllerExceptionHandler {
    	
    	@ExceptionHandler(CustomValidationException.class)
    	public String validationException(CustomValidationException e) {
    		return Script.back(e.getErrorMap().toString());
    	}
    }

    → 둘중 어느것이 더 좋은지 판단하고 쓰고 싶은걸 쓰자

    • 클라이언트에게 응답시에는 Script가 당연 좋음
    • 하지만 나중에 AJAX통신/안드로이드 통신등 CMRespDto가 당연 좋음
    • 차이는 AJAX/안드로이드는 개발자가 JS코드로 서버쪽으로 던져서 응답을 받는 형태
    • Script는 브라우저가 단순 응답을 받는 형태일뿐

     

Designed by Tistory.