-
스프링부트+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강의에서 배웠던 것이 그대로 나왔다
- @RestController → return을 데이터 형태로 돌려주기 위함 (즉 e.getMessage()의 메세지를 그대로 데이터형태로)
- @ControllerAdvice → 하나가 아닌 모든 @Controller 즉 전역에서 발생하는 예외를 잡아 처리하는 역할
- @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는 브라우저가 단순 응답을 받는 형태일뿐
'스프링 > 스프링부트+JPA - 인스타' 카테고리의 다른 글
스프링부트+JPA - 인스타그램 클론 코딩 05 - 회원정보수정 (0) 2021.09.23 스프링부트+JPA - 인스타그램 클론 코딩 04 - 로그인 (0) 2021.09.23 스프링부트+JPA - 인스타그램 클론 코딩 03 - 1 시큐리티 및 회원가입 (0) 2021.09.17 스프링부트+JPA - 인스타그램 클론 코딩 02-1 - 프로젝트 기본 세팅 및 기본 개념 이해 (0) 2021.09.17 스프링부트+JPA - 인스타그램 클론 코딩 02 - 프로젝트 기본 세팅 및 기본 개념 이해 (0) 2021.09.16