@Constraint
어노테이션을 활용하면 사용자가 원하는 Constraint
와 Validation
을 만들어 이를 적용할 수 있다. 개인 프로젝트 과정에서 String
타입의 필드(ex, 성별)를 Enum
타입으로 변환하여 저장할 필요가 생겼다. 그러나, 변환 과정에 대한 에러처리가 Controller
에서 이뤄지다보니 불필요하게 코드가 복잡해졌다. 이 글에서는 @Constraint
어노테이션을 활용하여 이를 어떻게 개선할 수 있는지 다루었다.
우선적으로 회원가입 과정에 필요한 User
, UserApiController
와 CreateUserDto
객체들을 구현하도록 하자.
// 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;
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;
}
}
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 {};
}
// 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
를 등록하여 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.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());
}
이번 글에서는 @Constraint
와 ConstraintValidator
를 활용하여 직접 벨리데이션을 만들고 이를 사용하는 과정을 다루었습니다. 커스텀 벨리데이션을 활용하면, 데이터 검사 로직과 비지니스 로직을 분리하여 코드를 작성할 수 있어 유용할 것 같습니다.