728x90

프로젝트 관리하다가 외주업체에 맞겼는데 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="")

끝!

+ Recent posts