Backend/Java

[Spring Boot] 유효성 검사

비비빅B 2020. 12. 16. 01:51

※ SpringBoot를 활용한 REST 개발 공부 중, HTTP RequestBody 유효성 검사에 관한 내용을 정리한 자료입니다.

 

@Test @DisplayName("username 없이, POST 회원가입 , 400 badRequest")
public void post_whenUsernameIsNull_receiveBadRequest() {
	User user = TestUtil.createValidUser();
	user.setUsername(null);
	ResponseEntity<Object> response = postUser(user, Object.class);
	assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}

 

회원 Entity가 있고 username 널 값 여부를 판단해, 널이면 BAD_REQUEST(400) 에러를 응답하려 한다.

 

여러가지 방법이 있겠지만 먼저 제일 단순하게 널여부를 직접 확인해보자.


1. 커스텀 에러 만든 후, 비즈니스 로직에서 처리

1-1. 커스텀 에러 클래스 생성

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class UserNotValidException extends RuntimeException{
	
}

프로그램 수행 시 에러를 던지는 RuntimeException 상속한다. 하지만 지금은 널일 경우 BAD_REQUEST(400)을 받고 싶으므로, ResponseStatus 어노테이션을 추가해서 400 에러로 정의한다.

 

1-2. Service에서 null일 경우, 에러 던짐

public User createUser(User user) {
	if (user.getUsername() == null) {
		throw new UserNotValidException();
	}
	String hashedPassword = passwordEncoder.encode(user.getPassword());
	user.setPassword(hashedPassword);
	return userRepository.save(user);
}

이렇게 하면 테스트를 통과할 수 있다. 그런데 문제가 있다. 만약 User 객체에 다른 속성도 null 여부를 확인해야 되면 어떻게 될까?

public User createUser(User user) {
	if (user.getUsername() == null
    	|| user.getPassword() == null
        || user.getNickName() == null
        || user.getEmail() == null) {
		throw new UserNotValidException();
	}
	String hashedPassword = passwordEncoder.encode(user.getPassword());
	user.setPassword(hashedPassword);
	return userRepository.save(user);
}

이런식으로 User Entity에 변화가 생길 때마다 구현 코드를 찾아가서 수정해야 될 것이다.

 

이를 해결하고 개발의 번거로움을 줄이기 위해 자바에서는 JavaBean Validation이라는 유효성 검사을 위한 메타데이터 모델과 API를 정의하고 있다. 그럼 JavaBean Validation은 어떤 식으로 사용하는걸까?


2. JavaBean Validation 사용

JavaBean Validation

JavaBean은 보통 어노테이션으로 구현되어있는데, 이는 단순히 유효성 검사를 위한 명세다.

 

유효성 검사를 하기위해서는 직접 따로 구현이 필요하다.

 

2-1. JPA Entity에 JavaBean Annotation

@Data
@Entity
public class User {
	
	@Id @GeneratedValue
	private long id;
	
	@NotNull
	private String username;
	
	@NotNull
	private String displayName;
	
	@NotNull
	private String password;
}

미리 구현된 JavaBean 어노테이션을 사용해 제약조건을 쉽게 명세할 수 있다.

2-2. Service에서 유효성 검사

public User createUser(User user) {
	ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
	Validator validator = factory.getValidator();
	Set<ConstraintViolation<User>> violations = validator.validate(user);
	
	for (ConstraintViolation<User> violation : violations) {
		throw new UserNotValidException();
	}
	
	String hashedPassword = passwordEncoder.encode(user.getPassword());
	user.setPassword(hashedPassword);
	return userRepository.save(user);
}

명세한 제약조건을 검사하기 위해서는 위와같이 구현코드가 필요하다. Entity가 변화해도 서비스 구현 코드에는 변경할 필요가 없기 때문에 이전의 문제는 해결했다.

 

그런데 유효성 검사를 위한 코드가 상당히 많다. 또한, 검사가 필요한 위치마다 저 코드를 반복해야 되는 것이 마음에 들지 않는다.

 

이를 해결하기 위해 SpringHibernate Validator를 사용하고 있다.


3. Hibernate Validation 사용

사실 위와 같이 JavaBean Validation을 사용하는 사람은 거의 없을 것이다.

 

왜냐하면 Hibernate Validator를 사용하면 구현코드를 적을 필요없이 명세만 하면된다. 즉, Entity에 제약 에노테이션만 명세하면 스프링이 유효성검사를 자동으로 구현해준다.

 

While you can run validation manually, it is more natural to let other specifications and frameworks validate data at the right time (user input in presentation frameworks, business service execution by CDI, entity insert or update by JPA).
In other words, run once, constrain anywhere.
- Java Validation Homepage

 

3-1. Controller 함수 파라미터에 @Valid 어노테이션

@PostMapping("/user")
public GeneralResponse Signup(@Valid @RequestBody User user) {
	userService.createUser(user);
	return new GeneralResponse("회원 생성 성공");
}

데이터를 받아들이는 Controller 파라미터에서 유효성 검사할 객체 앞에 @Valid 어노테이션을 추가한다. 그러면 스프링 Hibernate가 자동으로 Entity에 정의된 제약조건을 토대로 유효성 검사를 수행한다.

 

이제 Service에서 지저분했던 유효성 검사 구현코드와 임의로 정의했던 커스텀 에러는 더이상 필요없다.

public User createUser(User user) {
	String hashedPassword = passwordEncoder.encode(user.getPassword());
	user.setPassword(hashedPassword);
	return userRepository.save(user);
}

그런데 만약 내가 필요한 유효성 검사가 없으면 어떡할까?

 

예를 들어, 회원가입할 때 username 중복 여부를 확인하는 유효성 검사가 필요하다고 하자. 가장 단순하게는 1번 방법처럼 커스텀 에러를 만들고 구현할 수 있다.

하지만 앞에서 설명했듯이 좋은 방법은 아닐 것이다.


4. Custom JavaBean Validation 사용

필요한 경우 직접 JavaBean Validation Annotation을 만들어 명세 후 사용할 수 있다.

4-1. Annotation 생성

@Target(ElementType.FIELD)
@Constraint(validatedBy = UniqueUsernameValidator.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueUsername {
	String message() default "중복된 username";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
}

Constraints Annotation을 만들기 위해서는 3가지 속성이 필요하다.

  • message는 제약 조건에 만족 못할 경우 반환하는 기본 에러 메시지다.
  • groups는 제약사항 그룹을 지정하는 것으로 추후에 검사 순서를 지정하는 것과 관련있다. 기본으로 Class<?> 배열으로 정의해야 한다.
  • payload는 client에서 payload 객체를 위해 쓰임

또한, 메타데이터 어노테이션으로 Annotation을 설명할 수 있다.

Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) 어노테이션이 사용될 위치를 정의
Constraint(validatedBy = Class<T>) 유효성 검사에 사용될 Validator
Retention(RetentionPolicy.RUNTIME) 런타임시 유효성 검사를 수행

 

4-2. Validator 생성

public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {

	@Autowired
	UserRepository userRepository;
	
	@Override
	public boolean isValid(String username, ConstraintValidatorContext context) {
		User inDB = userRepository.findByUsername(username);
		if (inDB == null) {
			return true;
		}
		return false;
	}

}

앞서 Constaint에서 연결한 UniqueUsernameValidator를 생성했다. ConstraintValidator 인터페이스를 수행하며 검사할 클래스와 값 타입을 적으면 된다.

 

String 타입으로 들어온 username 변수를 이용해 중복된 username이 있으면 false, 없으면 true를 반환한다.

 

4-3. JavaBean Validation Annotation 사용

public class User {
	@NotNull(message = "{hoaxify.constraints.username.NotNull.message}")
	@Size(min = 4, max = 255)
	@UniqueUsername
	private String username;
    
    ...
}

 

Medium

404 Out of nothing, something.

medium.com

 

Java Bean Validation Basics | Baeldung

Learn the basics of Java Bean validation, the common annotations and how to trigger the validation process.

www.baeldung.com

 

'Backend > Java' 카테고리의 다른 글

parse, valueOf 차이와 문제점  (0) 2021.01.21
Repository와 DAO 차이?  (0) 2021.01.14
[JSP] 쿼리문 실행 오류  (0) 2020.07.09
[JSP] 한글 인코딩  (0) 2020.05.28
[안드로이드] 키보드 리스너 정의  (0) 2020.05.20