[Spring] @Constraint로 커스텀 validation 만들기

bluewhale·2021년 5월 30일
1

Spring

목록 보기
9/9

@Constraint

@Constraint 어노테이션을 활용하면 사용자가 원하는 ConstraintValidation을 만들어 이를 적용할 수 있다. 개인 프로젝트 과정에서 String 타입의 필드(ex, 성별)를 Enum 타입으로 변환하여 저장할 필요가 생겼다. 그러나, 변환 과정에 대한 에러처리가 Controller에서 이뤄지다보니 불필요하게 코드가 복잡해졌다. 이 글에서는 @Constraint 어노테이션을 활용하여 이를 어떻게 개선할 수 있는지 다루었다.

우선적으로 회원가입 과정에 필요한 User, UserApiControllerCreateUserDto 객체들을 구현하도록 하자.

User


// User.java

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    @Column(name = "user_id")
    private Long id;

    @Email
    private String email;

    private String password; 

    @Enumerated(EnumType.STRING)
    private Gender gender;         <------------------ 목표

    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

UserController

UserApiController는 REST API 요청을 받아서, User 엔티티를 생성하여 UserService에게 이를 전달하여 저장하는 역할을 수행한다.


// UserApiController.java

@RestController
@AllArgsConstructor
public class UserApiController {

    private final UserService userService;

    @PostMapping("/users/sign-up")
    public CreateUserResDto createUser(@RequestBody @Valid CreateUserReqDto request) throws Throwable {
        User user = User.builder()
                .email(request.getEmail())
                .password(request.getPassword())
                .gender(Gender.valueOf(request.getGender().toUpperCase())) <---------- 변환 오류시 해당 라인에서 에러가 발생
                .build();
		
        ....
        
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class CreateUserReqDto {
        @Email(message = "올바른 이메일을 입력해주세요")
        private String email;

        private String password;

        @ValidGender(message = "올바른 성별을 입력해주세요") <-- CustomValidator
        private Gender gender;
    }
}

 

@Constraint

User 엔티티를 생성하는 과정에서 String 타입의 gender 필드를 Gender Enum 타입으로 변환하게 되는데, 해당 gender가 존재하지 않는 경우(ex, femmale) 에러가 발생한다. 이러한 에러 처리 과정들을 Controller 코드에 추가할 경우, 코드가 지저분해지고 에러 처리 과정의 일관성이 떨어지게 된다.

이를 해결하기 위해, 커스텀 Constraint를 만들고 데이터 벨리데이션 과정을 Controller에서 분리시켜 처리하도록 하였다.

// ValidGender.java

@Constraint(validatedBy = GenderValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidGender {
    String message() default "올바른 성별을 입력해주세요";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@ConstraintValidator

// GenderValidator.java

public class GenderValidtor implements ConstraintValidator<ValidGender, String>{

    @Override
    public boolean isValid(String gender, ConstraintValidatorContext context) {
        try {
            Gender.valueOf(gender.toUpperCase()); 
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

 

ControllerAdvice

마지막으로 ControllerAdvice를 등록하여 Validation 과정에서 발생한 에러를 일관된 형태로 처리하도록 하였다.


@RestControllerAdvice
public class UserApiControllerAdvice  {

    /**
     * @Valid 과정에서 발생한 에러 핸들링
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Error> processValidationError(MethodArgumentNotValidException exception) {
        return new ResponseEntity<>(
                Error.from(exception.getBindingResult().getAllErrors().get(0).getDefaultMessage()),
                HttpStatus.BAD_REQUEST
        );
    }


    @Data
    static class Error{
        private String message;

        public static Error from(String message) {
            Error error = new Error();
            error.setMessage(message);
            return error;
        }
    }
}

UserAPiControllerTest

잘못된 요청을 보낸 경우 원하는 형태의 에러 메시지를 반환하는지 확인할 수 있다.

// UserApiControllerTest.java

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void 회원가입_실패_유효하지않은_성별_400() throws Exception {

        String content = objectMapper.writeValueAsString( new CreateUserReqDto(
                        "paul@gmail.com",
                        "123123",
                        "maaaaaaaaaaaaaale" <------
                )
        );

        mockMvc.perform(
                MockMvcRequestBuilders.post("/users/sign-up")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content)
        )
                .andExpect(status().is4xxClientError())
                .andExpect(content().json("{'message':'올바른 성별을 입력해주세요'}"))
                .andDo(print());
    }

Summary

이번 글에서는 @ConstraintConstraintValidator를 활용하여 직접 벨리데이션을 만들고 이를 사용하는 과정을 다루었습니다. 커스텀 벨리데이션을 활용하면, 데이터 검사 로직과 비지니스 로직을 분리하여 코드를 작성할 수 있어 유용할 것 같습니다.

profile
안녕하세요

0개의 댓글