이전 [[8. 컨트롤러 구현(API)|컨트롤러 구현 코드]]에는 API 기능 구현에 집중했기 때문에 사용자의 데이터를 검증하는 코드를 작성하지 않았다. 그러니 이번에는 사용자의 데이터를 검증하는 코드를 추가해보자. 검증 코드는 컨트롤러에 추가할 것이고 JSR-303과 Spring Validator를 사용해볼 것이다.
## 참고 : [[G. JSR-303 @Annotation 목록|주요 JSR-303 @Annotation]]
## 참고 : [@Valid 동작 원리](https://incheol-jung.gitbook.io/docs/q-and-a/spring/valid)
## 의존성 설정
```groovy
dependencies {
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
}
```
## JSR-303
JSR-303은 자바 빈의 유효성 검사를 위한 표준 스펙이다. 어노테이션을 통해 쉽게 자바 빈의 유효성 검사를 할 수 있어 자주 사용된다.
### `PromptTemplateRequest` 수정
```java
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class PromptTemplateRequest {
@NotNull(message = "ID is required")
@Length(min=1, max = 20, message = "ID length must be between 1 and 20 characters")
private String id;
@NotNull(message = "Content is required")
@Length(min=1, max = 1000, message = "Content length must be between 1 and 1000 characters")
private String content;
public PromptTemplate toEntity(){
return PromptTemplate.builder()
.id(id)
.content(content)
.build();
}
}
```
- `id`와 `content`는 모두 null이면 안 되고 길이 제한이 존재한다. 그래서 `@NotNull`과 `@Length`를 추가하였다.
- 위 어노테이션만으로는 검증이 작동하지 않고 컨트롤러에 따로 어노테이션을 더 추가해야만 한다.
### `ChatGPTController` 수정
```java
@PutMapping("/prompt-template/{id}")
public ResponseEntity<PromptTemplateResponse> updatePromptTemplateById(
@PathVariable("id") String id,
@Valid @RequestBody PromptTemplateRequest promptTemplateRequest,
BindingResult bindingResult) {
if (bindingResult.hasErrors()){
System.out.println(
Objects.requireNonNull(
bindingResult.getFieldError()
).getDefaultMessage()
);
throw new BadRequestException(
bindingResult.getFieldError().getDefaultMessage()
);
}
... (생략)
}
```
- 검증하고자 하는 `PromptTemplateRequest` 인자에 `@Valid` 어노테이션을 부착한다. 이 어노테이션을 부착하지 않으면 검증 코드가 작동하지 않는다.
- `BindingResult`는 검증 결과를 데이터로 바인딩해주는 인터페이스로 `@Valid`이 붙은 인자 바로 다음 순서에 위치해야 한다.
- 컨트롤러에서 예외 처리를 하지 않고 `BadRequestException`을 던지는 이유는 `@ControllerAdvice`에서 일괄적으로 예외 처리를 할 것이기 때문이다.
## Spring Validator
JSR-303의 어노테이션을 이용한 검증 방법은 간단해서 좋다. 하지만 검증 과정이 복잡해질 수록 이런 방법을 적용하기 힘들어진다. 스프링에서는 이런 경우 Spring Validator를 통해 개발자가 직접 검증 과정을 구현할 수 있다. 컨트롤러에서 `@InitBinder`를 통해 Validator를 추가해주기만 하면 기존 JSR-303과 동일한 코드로 작동하기 때문에 편리하다.
### `o.s.validation.Validator`
```java
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
```
- Spring Validator를 사용하려면 위 인터페이스를 구현해야 한다.
- `Validator::supports`는 `@Valid`를 단 자바 빈이 검증 대상인지 확인하는 메서드다.
- `Validator::validate`는 검증 대상을 검증하고 문제가 있는 경우 에러를 반환하는 메서드다.
### `PromptTemplateValidator`
#### 클래스 생성
```java
public class PromptTemplateValidator implements Validator {
}
```
- `o.s.validation.Validator`를 구현한다.
#### `supports` 메서드 구현
```java
@Override
public boolean supports(Class<?> clazz) {
return PromptTemplateRequest.class.equals(clazz);
}
```
- 자바 빈이 검증 대상인 `PromptTemplateRequest`에 해당하는지 확인한다.
#### `validate` 메서드 구현
```java
@Override
public void validate(Object target, Errors errors) {
PromptTemplateRequest request = (PromptTemplateRequest) target;
if (Objects.isNull(request.getId())) {
errors.rejectValue("id", "required", "ID is required");
return;
}
if (Objects.isNull(request.getContent())) {
errors.rejectValue("content", "required", "Content is required");
return;
}
if (request.getId().isEmpty() || request.getId().length() > 20) {
errors.rejectValue("id", "length", "ID length must be between 1 and 20 characters");
return;
}
if (request.getContent().isEmpty() || request.getContent().length() > 1000) {
errors.rejectValue("content", "length", "Content length must be between 1 and 1000 characters");
}
}
```
- 검증 내용은 JSR-303을 사용할 때와 같다.
### `ChatGPTController` 수정
```java
@RestController
@Slf4j
public class ChatGPTController {
@InitBinder
void initBinder(WebDataBinder webDataBinder){
webDataBinder.addValidators(new PromptTemplateValidator());
}
... (생략)
@PutMapping("/prompt-template/{id}")
public ResponseEntity<PromptTemplateResponse> updatePromptTemplateById(
@PathVariable("id") String id,
@Valid @RequestBody PromptTemplateRequest promptTemplateRequest,
BindingResult bindingResult) {
if (bindingResult.hasErrors()){
System.out.println(
Objects.requireNonNull(
bindingResult.getFieldError()
).getDefaultMessage()
);
throw new BadRequestException(
bindingResult.getFieldError().getDefaultMessage()
);
}
... (생략)
```
- `@InitBinder`로 Validator를 추가한다. 추가한 Validator는 디스패처 서블릿이 컨트롤러를 호출할 때 자동으로 호출되어 유효성 검사를 진행한다.
- `@Valid`나 `BindingResult` 나 `BadRequestException`는 JSR-303과 동일하게 적용된다.
## [[4-4. 컨트롤러 테스트 코드(검증 & 예외처리)|테스트 코드]]
![[4-4. 컨트롤러 테스트 코드(검증 & 예외처리)#`ValidationAndExceptionTest` 작성]]