@Valid(+ 정규 표현식), @Validated

박영준·2022년 12월 7일
1

Java

목록 보기
27/112

1. @Valid

1) 정의

  • 빈 검증기(Bean Validator)를 이용해 객체의 제약 조건을 검증하도록 지시하는 어노테이션
  • @Valid는 기본적으로 컨트롤러에서만 동작하며, 기본적으로 다른 계층에서는 검증이 되지 않는다.
  • 유효성 검증에 실패할 경우, MethodArgumentNotValidException이 발생

2) 사용 방법

  1. build.gradle 에 의존성을 추가를 해야 사용 가능

    implementation 'org.springframework.boot:spring-boot-starter-validation'
  2. 유효성 검증
    방법1) Controller 의 메소드에 @Valid 붙이기 (@Valid 는 주로 @RequestBody 앞에 붙음)

    @PostMapping("/user/add") 
    public ResponseEntity<Void> addUser(@Valid @RequestBody AddUserRequest addUserRequest) {
          ...
    }

    방법2) 유효성 검사를 할 필드에 붙이기

    public class Person {
        @NotNull
        @Size(max = 64)
        private String name;
            ...
    }
  3. 해당 제약 조건들이 정상적으로 검증되는지 확인하기 위해서 테스트 코드를 작성하는데,
    @Valid에 의한 반환값은 400 BadRequest이므로 이를 결과로 예측하면 된다.

'유효성 검사'와 '정규 표현식'의 관계
해당 데이터가 유효한지, 정규 표현식을 활용하여 검사한다.
즉, 정규 표현식은 '유효성 검증 도구'

유효성 검사는 Controller, Service, Dto, Entity 中 어디서 해야할까?
방법 1
Controller 에서 @Valid 를 통해, Entity 수준에서 유효성 검사를한다.
위의 '2) 사용 방법' 예시가 이 방식으로 진행된다.

방법 2
Controller 에서 @Valid 를 통해, Dto 수준에서 유효성 검사를한다.
→ Dto : 파라미터가 넘어오는 Dto 에서 유효성 검사를 한다.

Controller

@PostMapping("/signup")
public void registerUser (@RequestBody @Valid SignupDTO signupDTO) {
 User user = signupDTO.toEntity (signupDTO);
 Long userId = userService.singup (user);
}

Dto

public class SignupDTO {
    @NotBlank (message = "이메일은 필수값입니다.")
    @Email (message = "이메일 형식이 아닙니다.")
    private String email;

    @NotBlank
    @Pattern (regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8.20}", message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8 ~20자의 비밀번호여야 합니다.")
    private String password;
    private String name;

    // 현재는 가정일뿐, 수정 가능
    @NotBlank (message = "사용자 이름은 필수값입니다.")
    @Pattern (regexp = "^[a-zA-Z0-0]*$", message = "사용자이름은 영어랑 숫자만 가능합니다.")
    private String nickname;
}

방법 1, 2 의 문제는
- 특정 레이어에서만 유효성 검사를 실시하므로, 이 경우 DB 에 원하는 값이 들어가지 않는 경우가 생긴다.
- Controller 에서 Service 로 정상값이 전달 될 것이라고 가정하고 코드를 작성하는 것은 좋지 않다.

그 해결법?
방법 3
모든 레이어에서 유효성 검사의 책임을 갖도록 한다.

Entity

@Entity
@Getter
@NoArgsConstructor (access = AccessLevel.PROTECTED)
@ToString (exclude = "roleSet")
@Table (name = "users")
public class User extends AuditingCreateUpdateEntity {

	@Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private Long id;
   
    @Column (nullable = false, length = 60)
    @Length (min = 1, max = 60, message = "사이즈를 확인하세요.")
    @NoBlank (message = "이메일은 필수 값 입니다.")
    @Email
    private String email;
   
    @JsonIgnore
    @Column (nullable = false, length = 80)
    @NotBlank (message = "비밀번호는 필수 값 입니다.")
    private String email;
   
    @Column (nullable = false, length = 80)
    @Length (min = 1, max = 60, message = "사이즈를 확인하세요.")
    @NotBlank (message = "이름은 필수 값 입니다.")
    private String name;
}

validation 어노테이션 방식으로 Entity를 수정 (하나의 방식으로 통일하기 위해)

public class ValidateUtil {

	@Autowired
    private Validator validator;
   
    // 예외 종류는 무관
    // RuntimeException 계열로 바꾸면 상관 없으나, checked 예외로 바꾼다면 rollbackFor = "예외" 코드를 추가해야한다.
    public void validtae (object entity) {
    	Set<ContraintViolation<Object>> violation = validator.validate(entity);
   
    	if (!violation.isEmpty()) {
        	StringBuilder sb = new StringBuilder();
            for (ConstraintViolation<Object> constraintViolation : violation) {
            	sb.append(constraintViolation.getMessage());
            }
           
            throw new ConstraintViolationException("Error occurred: " + sb, violation)       
        }
    }
}

util 클래스 : 각 클래스 마다 구현하는것은 클래스의 책임을 늘리기 때문에, 한 클래스에서 유효성 검사를 해결

@Override
@Transactional
public Long updateUserInfo (Long principalId, Stirng name, String nickname, Stirng university, String introduction) {
	User user = userRepository.findById(pringcipalId)
    		.orElseThrow(() -> new EntityNotFoundException("throw notFoundException"));
    user.updateUser(name, nickname, university, introduction);
    validateUtil,validate(user);	// Service 레이어에서도 유효성 검사를 실시
    return user.getId();
}

3) 정규 표현식 (정규식)

참고: 정규 표현식(정규식, Regular Expression)

(1) 정의

(3) 사용 방법

예시 Member

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Member {

    @NotNull(message = "id는 필수 값입니다.")
    @Size(min = 5, max = 10)
    private String id;

    @Max(value = 25, message = "25세 이하만 가능합니다.")
    @Min(value = 18, message = "18살 이상만 가능합니다.")
    private int age;

    @Pattern(regexp = "[a-zA-z0-9]+@[a-zA-z]+[.]+[a-zA-z.]+")
    private String email;
    
    ..setter/getter 생략..
}

예시 TestController

import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/member")
public class TestController {
    
    @PostMapping
    @ResponseBody
    public ResponseEntity saveMember(@Valid Member member, BindingResult bindingResult) {
        
        // Member 객체가 유효하지 않으면 bindingResult.hasErrors() 메소드에서 true 값이 반환된다.
        if (bindingResult.hasErrors()) {
        
        	// Member가 유효하지 않다면, ResponseEntity에 BAD_REQUEST와 bindingResult.getAllErrors() 값을 넣고 생성하여 반환한다.
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(bindingResult.getAllErrors());
        }
        
        /*
        	save Memeber
        */
        
        return ResponseEntity.ok(member);
    }
}

@Valid 어노테이션을 통해, Member가 유효한 객체인지 검사

  • regular expression = regex = regexp

  • 프로그래밍에서 문자열을 다룰 때, 문자열의 일정한 패턴을 표현하는 일종의 형식 언어

(2) 사용법

① 클래스

Java 에서 정규 표현식을 사용하기 위해, java.util.regex 패키지에 있는 클래스들을 사용
→ 주로 사용하는 것이 Pattern 클래스, Matcher 클래스

Pattern 클래스

String pattern = "^[0-9]*$"; // 숫자만 등장하는지
String str = "123321"; 

// 첫번째 인자 pattern : 정규 표현식
// 두번째 인자 str : 정규 표현식에 매칭되는지 확인하는 문자열 → 매칭되면 true, 아니면 false
boolean result = Pattern.matches(pattern, str);
System.out.println(result); // true
  • 생성자 X

  • 지원하는 메소드

Matcher 클래스

Pattern pattern = Pattern.compile("^[0-9]*$");
String str = "04234234";

Matcher matcher = pattern.matcher(str);
System.out.println(matcher.find());
  • 생성자 X

  • 지원하는 메소드

② 어노테이션

@Null : Null만 입력 가능
@NotNull : Null 불가

@NotEmpty : Null, 빈 문자열 불가
@NotBlank : Null, 빈 문자열, 스페이스만 있는 문자열 불가

@Size(min=,max=) : 값이 min과 max사이에 해당하는가? (CharSequence, Collection, Map, Array에 해당)
@Pattern(regex=) : 정규식을 만족하는가?

@Positive : 양수만 가능
@PositiveOrZero : 양수와 0만 가능
@Negative : 음수만 가능
@NegativeOrZero : 음수와 0만 가능
@Digits(integer=, fraction = ) : 대상 수가 지정된 정수와 소수 자리 수 보다 작은가?

@Max(숫자) : 값이 Max보다 큰지 확인
@Min(숫자) : 값이 Min보다 작은지 확인
@DecimalMax(value=) : 지정된 값(실수) 이하인가?
@DecimalMin(value=) : 지정된 값(실수) 이상인가?

@Future : 현재 보다 미래인가?
@Past : 현재 보다 과거인가?

@Email : 이메일 형식만 가능

@AssertTrue : true 인가?
@AssertFalse : false 인가?

2. @Validated

1) 정의

  • 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야 한다.(@Valid가 아닌)
  • 입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋으므로, @Validated 사용은 차선책
  • JSR 표준 기술이 아니며 Spring 프레임워크에서 제공하는 어노테이션 및 기능
  • 유효성 검증에 실패할 경우, ConstraintViolationException이 발생

2) 사용 방법

클래스에 @Validated 를 붙이고,
유효성을 검증할 메소드의 파라미터에 @Valid 를 붙인다.

@Service
@Validated
public class UserService {

	public void addUser(@Valid AddUserRequest addUserRequest) {
		...
	}
}

참고: [Spring] @Valid와 @Validated를 이용한 유효성 검증의 동작 원리 및 사용법 예시 - (1/2)
참고: Spring Boot | spring-boot-starter-validation
참고: 정규 표현식
참고: [Spring Boot] @Valid 어노테이션으로 Parameter 검증하기
참고: 알고 있어야 할 8가지 정규식 표현 from nettuts+
참고: 정규 표현식
참고: 유효성 검사 dto, entity, controller ,service 어디서...?
참고: [Java] 정규표현식 사용법 및 예제 - Pattern, Matcher
참고: 정규식을 이용한 데이터 유효성 검사

profile
개발자로 거듭나기!

0개의 댓글