반응형
반응형
해당 글에서는 Spring Validation 라이브러리를 이용하여 클라이언트에서 전송된 '데이터'를 유효성 검증을 처리하는 방법에 대해서 공유합니다.
1) 개발 환경
💡 Spring Validation 구성을 위한 사용된 개발환경 입니다
개발 환경 | 버전 |
java | 1.8 |
Spring Boot | 2.7.4 |
빌드관리도구 | Gradle 7.5 |
개발 툴 | IntelliJ IDEA 2022.3 |
spring-boot-starter-validation | 2.7.4 |
[참고] Maven Repository: spring-boot-starter-validation
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation
2) 데이터 검증 유효성 전체적 흐름
⭕ Spring Validation을 이용하여서 데이터에 대한 유효성 검증 처리를 수행 과정입니다.
- 클라이언트는 데이터를 담아서 @RequestBody, @RequstParam, @PathVariable Annotation을 이용하여 API로 호출을 합니다.
- API에서는 @Vaild or @Vadlidated Annotation으로 데이터 유효성을 검증합니다.
- 유효성 검증을 통과한 경우
- API는 클라이언트로 ‘성공 응답(Response)’ 데이터를 전송합니다.
- 유효성 검증을 실패한 경우
- MethodArgumentNotValidException 에러가 발생합니다.
- @ControllerAdvice / @ExceptionHandler 로 구성한 ‘GlobalException’에서 해당 에러를 캐치합니다
- @ExceptionHandler(MethodArgumentNotValidException.class)로 에러로 발생 시키는 것이 아닌 클라이언트로 ‘에러 응답(Response)’ 데이터를 전송합니다.
- 유효성 검증을 통과한 경우
- 클라이언트와 API와의 통신이 완료되었습니다.
[ 더 알아보기 ]
💡 GlobalException이란?
- @ExceptionHandler 어노테이션을 기반으로 API 통신 중에 발생하는 에러에 대해서 해당 핸들러에서 캐치하여 오류에 대한 코드와 응답 메시지를 전달해주는 기능을 의미합니다.
- 추후에 이와 관련되어 글을 작성 예정입니다 😄
3) Spring Validation
💡 Spring Validation 이란?
- 클라이언트에서 서버로 값을 전달하고자 할 때 (@RequestBody, @RequestParam, @PathVariable) 전달되는 데이터에 대해 유효성 검증을 수행하며 유효하지 않을 경우 에러(MethodArgumentNotValidException)를 발생하도록 처리하는 기능을 수행하는 라이브러리입니다.
1. 데이터 전송 관련 Annotation
💡 클라이언트에서 API 서버로 전송을 위해 사용되는 주요 Annotation에 대해서 확인합니다.
1.1. @RequestBody
💡 클라이언트에서 서버로 JSON 형태로 전송하는 데이터를 VO의 ‘자바 객체’ 형태로 자동으로 값을 매칭시켜서 받기 위해 사용되는 어노테이션입니다.
💡 [참고] @Valid + @RequestBody를 함께 사용하여서 클라이언트에서 json 형태로 전송하는 데이터에 대해서 유효성 검증을 거쳐서 통과 시 로직을 수행하고 실패 시 에러를 도출합니다.
import org.springframework.web.bind.annotation.RequestBody;
/**
* [API] 코드 추가
*
* @return ApiResponseWrapper<List < TemplateVO>> : 응답 결과 및 응답 코드 반환
*/
@PostMapping("/insertCode")
@Operation(summary = "코드 추가", description = "코드를 추가하는데 사용하는 API입니다.")
public ResponseEntity<ApiResponse<Integer>> insertCode(@RequestBody @Valid CodeVO codeVO) {
log.debug("코드를 등록합니다.");
Integer result = codeService.insertCode(codeVO);
ApiResponse<Integer> ar = new ApiResponse<>(result, SuccessCode.INSERT.getStatus(), SuccessCode.INSERT.getMessage());
return new ResponseEntity<>(ar, HttpStatus.OK);
}
1.2. @RequestParam
💡 클라이언트에서 서버로 Query-String 형태로 데이터를 전송할때 이를 받기 위해 사용되는 어노테이션입니다.
ex) Client Request -> http://localhost:8080/userId=adjh54&userNm=lee
@GetMapping("/")
public ResponseEntity<ApiResponse<CodeVO> selectTemplate(@Valid @RequestParam @Size(min = 1, max = 10) String grpCd) {
}
1.3. @PathVariable
💡 클라이언트에서 서버로 Rest 형태로 전송되는 데이터를 받기 위해 사용되는 어노테이션입니다.
ex) Client Request -> http://localhost:8080/{userId}/userName
@GetMapping("/{userId}/{userNm}")
public ResponseEntity<ApiResponse<CodeVO>> selectTemplate(@Valid @PathVariable @Size(min = 1, max = 10) String userId, @Valid @Size(min = 1, max = 10) @PathVariable("userNm") String userNm) {
}
2. 데이터 유효성 관련 Annotation
💡 클라이언트에서 API 서버로 전송되는 데이터에 대한 유효성 검증을 위해 사용되는 Annotation에 대해 확인합니다.
2.1. @Valid
💡 ’Java’에서 유효성 검증으로 제공해주는 어노테이션입니다. [참고] import javax.validation.Valid;
💡 @RequestBody와 함게 사용되며 JSON 형태로 전송받은 데이터에 대해서 유효성을 검증하기 위해 사용됩니다.
💡 해당 어노테이션을 선언한 곳에 유효성 검증을 실패하였을 시 ‘MethodArgumentNotValidException’ 로 에러가 발생합니다.
import javax.validation.Valid;
/**
* [API] 코드 추가
*
* @return ApiResponseWrapper<List < TemplateVO>> : 응답 결과 및 응답 코드 반환
*/
@PostMapping("/insertCode")
public ResponseEntity<ApiResponse<Integer>> insertCode(@RequestBody @Valid CodeVO codeVO) {
log.debug("코드를 등록합니다.");
Integer result = codeService.insertCode(codeVO);
ApiResponse<Integer> ar = new ApiResponse<>(result, SuccessCode.INSERT.getStatus(), SuccessCode.INSERT.getMessage());
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.2. @Validated
💡 ’Spring Framework’에서 유효성 검증으로 제공해주는 어노테이션입니다.
[참고] import org.springframework.validation.annotation.Validated;
💡 @RequestBody와 함게 사용되며 JSON 형태로 전송받은 데이터에 대해서 유효성을 검증하기 위해 사용됩니다.
💡 해당 어노테이션을 선언한 곳에 유효성 검증을 실패하였을 시 ‘MethodArgumentNotValidException’ 로 에러가 발생합니다.
import org.springframework.validation.annotation.Validated;
/**
* [API] 코드 추가
*
* @return ApiResponseWrapper<List < TemplateVO>> : 응답 결과 및 응답 코드 반환
*/
@PostMapping("/insertCode")
public ResponseEntity<ApiResponse<Integer>> insertCode(@RequestBody @Valid CodeVO codeVO) {
log.debug("코드를 등록합니다.");
Integer result = codeService.insertCode(codeVO);
ApiResponse<Integer> ar = new ApiResponse<>(result, SuccessCode.INSERT.getStatus(), SuccessCode.INSERT.getMessage());
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.3. @Valid vs @Validated 차이점
💡 두 어노테이션의 차이는 @Validated는 @Valid의 어노테이션의 기능을 모두 포함하고 있으며 추가로 유효성 검토를 할 수 있는 그룹을 지정 할 수 있습니다.
💡 그룹의 의미는 VO내에서 원하는 유효성 그룹으로 묶어서 사용 하는것을 의미합니다.
분류 | @Valid | @Validated |
용도 | 클라이언트로 부터 RequestBody 형태(객체)로 넘어오는 데이터에 대해 유효성 검증을 위해 사용됩니다. | 클라이언트로 부터 RequestBody 형태(객체)로 넘어오는 데이터에 대해 유효성 검증을 위해 사용됩니다. |
제공 라이브러리 | Java에서 제공합니다. | Spring Framework에서 제공합니다 |
Import 구조 | import javax.validation.Valid; | import org.springframework.validation.annotation.Validated; |
유효성 검증 실패 시 | MethodArgumentNotValidException 에러가 발생합니다 | MethodArgumentNotValidException 에러가 발생합니다. |
유효성 그룹 지정기능 | 없음 | 있음 |
3. 데이터에 대한 유효성 검증 관련 Annotation
💡 해당 Annotation은 Spring Validation에서 제공하는 Annotation이며 VO내에서 사용됩니다.
Annotation | 사용 위치 | 설명 | 참고 |
@Null | **VO.java | - Null 값만 입력 가능함을 유효성 검증 | @Null String unusedString; |
@NotNull | **VO.java | - Not NULL 값만 입력 가능함을 유효성 검증 | @NotNull String userName; |
@NotEmpty | **VO.java | - Null, 빈 문자열 불 가능함을 유효성 검증 | @NotEmpty String userName; |
@NotBlank | **VO.java | - Null, 빈 문자열, 스페이스만 있는 문자열 불가능 함을 유효성 검증 | @NotBlank String userName; |
@Size(min=, max=) | **VO.java | - 해당 값에 대한 최소값과 최대값을 지정함을 유효성 검증 | @Size(min=2, max=240) String message; |
@Max | **VO.java | - 해당 값의 최대값을 지정함을 유효성 검증 | @Max(10) int quantity; |
@Min | **VO.java | - 해당 값의 최소값을 지정함을 유효성 검증 | @Min(5) int quantity; |
@Pattern | **VO.java | - 해당 값의 유효성 패턴을 지정함을 유효성 검증 | @Pattern(regexp="\\(\\d{3}\\)\\d{3}-\\d{4}") String phoneNumber; |
@Future | **VO.java | - 입력된 날짜는 미래의 날짜여야 함을 유효성 검증 | @Future Date eventDate; |
@Past | **VO.java | - 입력된 날짜는 과거의 날짜여야 함을 유효성 검증 | @Past Date birthday; |
[참고] 기타 VO Annotation 종류
4) 실행 환경
1. build.gradle 의존성을 추가합니다.
💡 Spring Validation을 사용하기 위해 Gradle에 의존성을 추가합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.4' // Spring Boot validation
}
2. Controller 구성
💡 INSERT를 수행하는 API입니다.
💡 클라이언트로 부터 RequestBody 로 VO형태로 데이터를 받고 있습니다. 그리고 @Valid를 통해서 데이터 유효성 검증을 수행합니다.
import javax.validation.Valid;
/**
* [API] 코드 리스트 출력 함수
*
*/
@PostMapping("/insertCode")
@Operation(summary = "코드 등록", description = "코드 등록")
public ResponseEntity<Integer> insertCode(@RequestBody @Valid CodeVO codeVO) {
log.debug("코드를 등록합니다.");
Integer resultList = codeService.insertCode(codeVO);
return new ResponseEntity<>(resultList, HttpStatus.OK);
}
3. 일반적인 VO 구성
package com.adjh.multiflexapi.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.sql.Timestamp;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class CodeVO {
private String grpCd;
private String cd;
private String grpCdNm;
private String cdNm;
private int sortOrder;
private Timestamp regDt;
private boolean useYn;
}
4. 호출
💡 테이블에는 NOT NULL이든, 아무런 제약사항을 넣어두지 않은 상태입니다.
💡 INSERT 구문에 빈 JSON 형태의 데이터를 던졌을때, 무작정 들어가고 있습니다.
5. Validation 적용
💡 다양한 Option의 Validation을 적용해봅니다.
package com.adjh.multiflexapi.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.*;
import java.sql.Timestamp;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class CodeVO {
@NotBlank(message = "grpCd is mandatory")
@Pattern(regexp = "^[a-zA-z]$", message = "grpCd is not English")
@Size(min = 1, max = 16, message = "grpCd must be between 1 and 16")
private String grpCd;
@Pattern(regexp = "^[a-zA-z]$")
@NotBlank
@Size(min = 1, max = 16)
private String cd;
@NotBlank
@Pattern(regexp = "^[a-zA-z가-힣]$")
@Size(min = 1, max = 50)
private String grpCdNm;
@NotBlank
@Pattern(regexp = "^[a-zA-z가-힣]$")
@Size(min = 1, max = 50)
private String cdNm;
@Min(1)
private int sortOrder;
private Timestamp regDt;
private boolean useYn;
}
6. 호출
💡 Validation을 적용하여 동일하게 빈 JSON 데이터를 전송하였을 경우 MethodArgumentNotValidException 에러가 발생하였습니다.
"timestamp": "2022-11-09T07:49:49.623+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for object='codeVO'. Error count: 5",
"path": "/api/v1/code/insertCode"
"errors": [
{
"codes": [
"NotBlank.codeVO.cdNm",
"NotBlank.cdNm",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"codeVO.cdNm",
"cdNm"
],
"arguments": null,
"defaultMessage": "cdNm",
"code": "cdNm"
}
],
"defaultMessage": "공백일 수 없습니다",
"objectName": "codeVO",
"field": "cdNm",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"NotBlank.codeVO.grpCdNm",
"NotBlank.grpCdNm",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"codeVO.grpCdNm",
"grpCdNm"
],
"arguments": null,
"defaultMessage": "grpCdNm",
"code": "grpCdNm"
}
],
"defaultMessage": "공백일 수 없습니다",
"objectName": "codeVO",
"field": "grpCdNm",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"NotBlank.codeVO.grpCd",
"NotBlank.grpCd",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"codeVO.grpCd",
"grpCd"
],
"arguments": null,
"defaultMessage": "grpCd",
"code": "grpCd"
}
],
"defaultMessage": "grpCd is mandatory",
"objectName": "codeVO",
"field": "grpCd",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"Min.codeVO.sortOrder",
"Min.sortOrder",
"Min.int",
"Min"
],
"arguments": [
{
"codes": [
"codeVO.sortOrder",
"sortOrder"
],
"arguments": null,
"defaultMessage": "sortOrder",
"code": "sortOrder"
},
1
],
"defaultMessage": "1 이상이어야 합니다",
"objectName": "codeVO",
"field": "sortOrder",
"rejectedValue": 0,
"bindingFailure": false,
"code": "Min"
},
{
"codes": [
"NotBlank.codeVO.cd",
"NotBlank.cd",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"codeVO.cd",
"cd"
],
"arguments": null,
"defaultMessage": "cd",
"code": "cd"
}
],
"defaultMessage": "공백일 수 없습니다",
"objectName": "codeVO",
"field": "cd",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
}
],
7. 정상 데이터 전송
💡 성공 결과 값으로 1을 리턴 받았습니다.
5) 결론
💡 해당 글에서 클라이언트로부터 전송한 데이터가 API 서버에서 유효한지 검증을 하는 Validation에 대해서 확인해보았습니다.
이를 확장시켜 데이터뿐만 아니라 로직 상에서 발생하는 에러에 대해서 클라이언트로 정해진 Response 값을 반환받을 수 있는 'GlobalException'에 대해서 이해하고 이를 구성을 해봅니다.
💡 추가로 데이터 유효성 검증과 이어서 Controller내에서 Exception 처리를 할 수 있는 GlobalException에 대해서 추가 참고하시면 더욱 좋으실것 같습니다.
반응형
'Java > Spring Boot' 카테고리의 다른 글
[Java] Business Exception 이해하고 구성하기 : Service Exception (0) | 2022.12.10 |
---|---|
[Java] Global Exception 이해하고 구성하기 : Controller Exception (5) | 2022.11.13 |
[Java/Library] Thymeleaf, Thymeleaf Layout 적용하기 (4) | 2022.10.30 |
[Java/Library] Slf4j - Log4j2 이해하고 설정하기 (2) | 2022.10.21 |
[Java/Library] HikariCP 이해하고 적용하기 (with. MyBatis) (0) | 2022.10.20 |