220228_스프링 입문_스프링의 기능을 활용해보자_Spring Boot Exception 처리_패스트캠퍼스 챌린지 36일차
<2022년 02월 28일 _ 패스트캠퍼스 챌린지 36일차>
[스프링 입문_스프링의 기능을 활용해보자_Spring Boot Exception 처리]
1. Exception
: 프로그래밍에 있어서 가장 필요한 부분
: Java에서는 null값에 대해 접근할 때, null pointer exception이 발생하므로
, 이 부분에 대한 방지를 위해, 미리 검증 하는 과정
ex)
public void run(String account, Spring pw, int age){
if(account == null || pw == null){
return;
}
if(age == 0){
return;
}
//정상 logic
}
(1) 검증해야 할 값이 많은 경우 코드 길이가 길어짐
(2) 구현에 따라 다르지만, 일반적으로 Service Logic과의 분리 필요
(3) 흩어져 있는 경우, 어디에서 검증을 하는지 알기 어렵고, 재사용의 한계
(4) 구현에 따라 다르지만, 검증 Logic이 변경 될 경우, 테스트 코드등의 참조 클래스에서 Logic이 변경되어야 할 수 있음
@Size | 문자 길이 측정 | Int Type 불가 |
@NotNull | null 불가 | |
@NotEmpty | null, "" 불가 | |
@NotBlank | null, "", " " 불가 | |
@Past | 과거 날짜 | |
@PastOrPresent | 오늘이거나 과거 날짜 | |
@Future | 미래 날짜 | |
@FutureOrPresent | 오늘이거나 미래 날짜 | |
@Pattern | 정규식 적용 | |
@Max | 최댓값 | |
@Min | 최솟값 | |
@AssertTrue / False | 별도 Logic 적용 | |
@Valid | 해당 object validation 실행 |
2. 실습 준비
(1) File > New > Project > Spring Initializr
(2) Language: Java, Type: Gradle, Artifact: validation, Java: 11
(3) Web > Spring Web 선택
3. Validation 세팅
(1) gradle dependencies
: bundle.gradle 선택
: implementation("org.springframework.boot:spring-boot-starter-validation")
dependencies에 다음과 같이 한줄 추가한다
implementation 'org.springframework.boot:spring-boot-starter-validation'
(2) bean validation spec : 어노테이션의 릴리즈 내용들을 볼 수 있다
https://beanvalidation.org/2.0-jsr380/
Jakarta Bean Validation - Bean Validation 2.0 (JSR 380)
Bean Validation 2.0 focused on the following topics: support for validating container elements by annotating type arguments of parameterized types e.g. List<@Positive Integer> positiveNumbers. This also includes: more flexible cascaded validation of contai
beanvalidation.org
(3) 핸드폰번호 정규식
"^\\d{2,3}-\\d{3,4}-\\d{4}$"
4. 실습 시작
(1) controller 패키지 추가 > ApiController 클래스 추가
package com.example.validation.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
public Object user(){
return null;
}
}
(2) dto 패키지 추가 > User 클래스 추가
package com.example.validation.dto;
public class User {
private String name;
private int age;
private String email;
private String phoneNumber;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", email='" + email + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
'}';
}
}
(3) ApiController 에서 User를 받아봅시다
package com.example.validation.controller;
import com.example.validation.dto.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
public User user(@RequestBody User user){
System.out.println(user);
return user;
}
}
(4) Talend API 에서 테스트
: (POST) http://localhost:8080/api/user
: body:
{
"name" : "홍길동",
"age" : 10,
"email" : "abcdefg",
"phoneNumber" : "01011112222"
}
결과:
User{name='홍길동', age=10, email='abcdefg', phoneNumber='01011112222'}
정상적으로 200은 응답이 떨어졌지만, 사용자가 원하는 email, phoneNumber 양식이 아닙니다.
이 때, 옛날 개발 방식은 어떻게 할까요?
(5) 옛 validation 방식(실무에선 여전히 많이 쓰이고 있을 수 있음)
package com.example.validation.controller;
import com.example.validation.dto.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
public ResponseEntity user(@RequestBody User user){
System.out.println(user);
if(user.getAge() >= 90){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
}
return ResponseEntity.ok(user);
}
}
테스트:
: (POST) http://localhost:8080/api/user
: body:
{
"name" : "홍길동",
"age" : 100,
"email" : "abcdefg",
"phoneNumber" : "01011112222"
}
결과:
User{name='홍길동', age=100, email='abcdefg', phoneNumber='01011112222'}
당장은 하나이지만, 이런 것들이 굉장히 많이 늘어난다면? 굉장히 복잡해질것입니다. 그럴때 사용하는 바로바로!!
(6) Spring validation 적용
: User 클래스 수정(@Email 어노테이션 추가)
@Email
private String email;
: ApiController 클래스 수정(user 메소드 파라미터 앞에 @Valid 어노테이션 추가)
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user){
System.out.println(user);
if(user.getAge() >= 90){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
}
return ResponseEntity.ok(user);
}
테스트:
: (POST) http://localhost:8080/api/user
: body:
{
"name" : "홍길동",
"age" : 89,
"email" : "abcdefg",
"phoneNumber" : "01011112222"
}
결과:
2022-03-01 22:25:32.853 WARN 13596 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity com.example.validation.controller.ApiController.user(com.example.validation.dto.User): [Field error in object 'user' on field 'email': rejected value [abcdefg]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@725599d1,.*]; default message [올바른 형식의 이메일 주소여야 합니다]] ]
: 이메일에 대한 양식이 맞지 않음을 체크한다
테스트:
: (POST) http://localhost:8080/api/user
: body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "01011112222"
}
결과:
User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='01011112222'}
(7) Spring validation 적용2 : 정규식으로 검증(휴대폰번호)
: User 클래스 수정 (@Pattern 어노테이션 추가)
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
private String phoneNumber;
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "01011112222"
}
결과:
2022-03-01 23:38:39.013 WARN 22348 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity com.example.validation.controller.ApiController.user(com.example.validation.dto.User): [Field error in object 'user' on field 'phoneNumber': rejected value [01011112222]; codes [Pattern.user.phoneNumber,Pattern.phoneNumber,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber],[Ljavax.validation.constraints.Pattern$Flag;@7e28223c,^\d{2,3}-\d{3,4}-\d{4}$]; default message ["^\d{2,3}-\d{3,4}-\d{4}$"와 일치해야 합니다]] ]
@Pattern 에 맞지 않기때문에 위와 같은 결과가 나타나게 됩니다.
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "010-1111-2222"
}
결과:
User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='010-1111-2222'}
(8) Spring validation 적용3(BindingResult 사용하기)
: BindingResult를 사용하면 validation에 대한 값이 BindingResult 파라미터 변수로 들어가게 된다
ApiController 클래스의 user 메소드 변경
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){
if(bindingResult.hasErrors()){
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(objectError -> {
FieldError field = (FieldError) objectError; //어떤 필드에서 에러가 났는지 가져오기
String message = objectError.getDefaultMessage();
System.out.println("field : " + field.getField());
System.out.println(message);
});
}
System.out.println(user);
return ResponseEntity.ok(user);
}
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "01011112222"
}
결과:
field : phoneNumber
"^\d{2,3}-\d{3,4}-\d{4}$"와 일치해야 합니다
User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='01011112222'}
에러 메시지를 변경해봅시다. User 클래스의 @Pattern 변경
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx[x]-xxxx")
private String phoneNumber;
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "01011112222"
}
결과:
field : phoneNumber
핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx[x]-xxxx
User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='01011112222'}
그렇다면, 좀 더 모양을 이쁘게 만들어볼까요
ApiController 클래스의 user 메소드 변경/추가
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){
if(bindingResult.hasErrors()){
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(objectError -> {
FieldError field = (FieldError) objectError; //어떤 필드에서 에러가 났는지 가져오기
String message = objectError.getDefaultMessage();
System.out.println("field : " + field.getField());
System.out.println(message);
sb.append("field : " + field.getField());
sb.append("message : " + message);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
}
System.out.println(user);
return ResponseEntity.ok(user);
}
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "01011112222"
}
결과:
field : phoneNumber
핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx[x]-xxxx
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 89,
"email" : "steve@gmail.com",
"phoneNumber" : "010-1111-2222"
}
결과:
User{name='홍길동', age=89, email='steve@gmail.com', phoneNumber='010-1111-2222'}
(9) Spring validation 적용4(@NotBlank, @Min, @Max)
User 클래스 수정
@NotBlank
private String name;
@Max(value = 90)
private int age;
테스트:
(POST) http://localhost:8080/api/user
body:
{
"name" : "홍길동",
"age" : 100,
"email" : "steve@gmail.com",
"phoneNumber" : "010-1111-2222"
}
결과:
field : age
90 이하여야 합니다
정리:
- 위와 같이 BindingResult 객체를 통해 새로운 변환 값을 받을 수도 있다.
- BindingResult가 없다면, 예외처리를 통해 가능하다(추후 강의)
36일차 강의 완료~
패스트캠퍼스 [직장인 실무교육]
프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.
fastcampus.co.kr
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.