프로젝트 관리하다가 외주업체에 맞겼는데 if..else로 도배를 해놓았던것이다.
음 개발자입문할때 누군가가 그러더라 "개발자는 if문,for문만 알아도 개발을 할수 있다" 고. 지금와서 생각해보니 틀린말은 아니다.
그런데 우리는 사람이다. 사람은 항상 생각하고 불편함을 해결하고 복잡한것보다 쉬운것을 추구한다.
그래서 나도 오늘 좀 생각없이 개발하는 아재 개발자들한테 if..else를 줄일수 있는 방법을 말해보려고 한다.
이미 알고 있는 고수분들은 그냥 잘난척하는 내앞을 지나가 주라!
일단 ...이런 코드를 많이 봤지?
@RestController
@PostMappting("/create")
public CommonResult testController(@RequestBody TestDto testDto ){
if(StringUtils.isEmpty(testDto.getName()){
return CommonResult.validateFailed("이름이 비었음!");
}
if(testDto.getSort() == null || testDto.getSort() < 0){
return CommonResult.validateFailed("0 보다 작을수 없음!");
}
int count = testService.create(testDto);
if(count ==1){
return CommonResult.success(testDto);
}
}
Springboot 에서 Hibernate Validator library를 이용하여 검증을 하면 편하다.
1. library 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. 객체를 만든다.
@Data
public class Student {
@NotBlank(message = "이름은 필수잆력")
private String name;
@NotNull(message = "나이는 필수잆력")
@Range(min = 1,max =50, message = "나이 범위1-50")
private Integer age;
@NotEmpty(message = "성적은 필수잆력")
private List<Double> scores;
}
3. post 요청 controller 를 만든다.
@RestController
@RequestMapping("/student")
public class StudentController {
@PostMapping("/add")
public Rest<?> addStudent(@Valid @RequestBody Student student) {
return RestBody.okData(student);
}
요청결과:
POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json
{
"name": "felord.cn",
"age": 77,
"scores": [
55
]
}
4. get 요청 controller 를 만든다.
@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {
return RestBody.okData(student);
}
요청결과:
GET /student/get?name=blake.com&age=12 HTTP/1.1
Host: localhost:8888
5. 커스텀 어노테이션
주위 깊게 보았으면 발견했을듯! 바로 Student 객체에 @NotNull + @Range 어노테이션을 겹쳐서 사용했다.
@NotNull(message = "나이필수 입력")
@Range(min = 1,max =50, message = "나이범위 1-50")
private Integer age;
원인은 @Range 가 공백값에 대한 제약을 할수 없다는것, 값이 있을때만 제약대상이다. 이처럼 어노테이션을 여러개 사용해야 하는경우가 가끔있는데 이럴때 이런 어노테이션들을 하나로 묶는것도 방법이긴 하다.
import org.hibernate.validator.constraints.Range;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;
@Constraint(
validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD,
ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {
// message 필수
String message() default "나이는 필수입력,범위는 1-50 ";
// 선택
Class<?>[] groups() default {};
// 선택
Class<? extends Payload>[] payload() default {};
}
혹은 상태와 같은 불변의 뭔가를 따로 관리해야 된다고 하면 enum클래스를 만들어서 사용하면 좋다.
public enum Colors {
RED, YELLOW, BLUE
}
public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();
@Override
public void initialize(Color constraintAnnotation) {
Colors[] value = constraintAnnotation.value();
List<String> list = Arrays.stream(value)
.map(Enum::name)
.collect(Collectors.toList());
COLOR_CONSTRAINTS.addAll(list);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return COLOR_CONSTRAINTS.contains(value);
}
}
@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,
ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
// 에러 메시지
String message() default "규격에 맞는 않음.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 유형
Colors[] value();
}
제약이 잘 걸리는지 함 테스트 해보자!
@Data
public class Param {
@Color({Colors.BLUE,Colors.YELLOW})
private String color;
}
요청하기
GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888
결과:
BindException 발생한다!
CAY 는 존재하지 않기때문. 그래서 BLUE, YELLOW, RED를 넣으면 리턴이 정상이다.
트러블슈팅:
@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {
return RestBody.okData(color);
}
// 혹은
@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {
return RestBody.okData(color);
}
// 혹은
@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
return RestBody.okData(student);
}
이처럼 클래스아닌 method 에 적용하연 제약기능이 작동하지 않는다. 구글링해보면 클래스에 적용해야 되지 메서드에 적용하면 안된다는것을 할수 있다. (직접 검색해 보시길!)
마지막으로
어노테이션을 겹쳐서 사용시 제약이 걸리지 않는 문제 도 있음!
@Data
public class Student {
@NotBlank(message = "이름 필수입력")
private String name;
@Age
private Integer age;
@NotEmpty(message = "성적 필수입력")
private List<Double> scores;
@NotNull(message = "학교 공백일수 없음")
private School school;
}
@Data
public class School {
@NotBlank(message = "이름 공백일수 없음")
private String name;
@Min(value = 0,message ="나이는 0보다 커야 됨." )
private Integer age;
}
이런 구조일때 get 요청시 문제 없으니, post 요청시 School 에 대한 제약기능이 작동안된다.
이때 우리는 @Valid 어노테이션을 붙여주면 된다.
@Data
public class Student {
@NotBlank(message = "이름 필수입력")
private String name;
@Age
private Integer age;
@NotEmpty(message = "성적 필수입력")
private List<Double> scores;
@Valid
@NotNull(message = "학교 공백일수 없음")
private School school;
}
참고:
// message는 오류 메시지를 정의하는 곳
// null만 허용
@Null(message="")
// null 허용 X, "", " "는 허용
@NotNull(message="")
// null, "" 허용 X, " "는 허용
@NotEmpty(message="")
// null, "", " " 허용 X
@NotBlank (message="")
// 데이터의 사이즈(최소 길이, 최대 길이) 설정
@Size(min=, max= ,message="")
// 정규식을 이용해서 검사
@Pattern(regexp = ,message="")
// value 이하의 값만 허용
@Max(value = ,message="")
// value 이상의 값만 허용
@Min(value = ,message="")
// 값을 양수만 허용
@Positive(message="")
// 값을 양수와 0만 허용
@PositiveOrZero(message="")
// 값을 음수만 허용
@Negative(message="")
// 값을 음수와 0만 허용
@NegativeOrZero(message="")
// 현재보다 미래의 날짜만 허용
@Future(message="")
// 현재보다 과거의 날짜만 허용
@Past(message="")
// True일 때만 허용(null 체크 X)
@AssertTrue(message="")
// False일 때만 허용(null 체크 X)
@AssertFalse(message="")
끝!