-
Notifications
You must be signed in to change notification settings - Fork 8
DTO 검증하기
스프링에서는 요청에 포함된 데이터를 DTO 객체로 자동으로 바인딩 해 주는데요. 이 과정에서 제약 조건을 추가해 두면 Spring 이 알아서 검증을 진행해 줍니다.
이번 글에서는 스프링에서 DTO를 검증하는 방법을 설명합니다.
DTO 검증을 하기 위해선 아래 의존성을 추가해 주어야 합니다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
위 의존성을 추가해 주지 않아도 @NotNull
과 같은 어노테이션을 사용할 수는 있습니다.
해당 어노테이션은 jakarta.validation.constraints
의 어노테이션, 즉 어느 프레임워크에 의존하지 않는 자바 자체의 스펙이기 때문이죠.
다만, 어노테이션들이 붙어 있는 필드를 검증하기 위해선 Validator 의 도움을 받아야 하는데요. 위 의존성을 추가해 주지 않으면 아무런 Validator 도 빈으로 등록되지 않기에 필드 검증이 진행이 되지 않습니다.
위 의존성을 추가해 주고 나면 org.springframework.validation.beanvalidation
패키지 아래의 LocalValidatorFactoryBean
이 빈으로 등록되기 때문에 정상적으로 검증을 진행할 수 있습니다.
이제 어노테이션을 사용해 제약 조건을 추가해 보겠습니다.
public record CreateSnippetRequest(
@NotNull(message = "파일 이름이 null 입니다.")
@Size(max = 255, message = "파일 이름은 최대 255자까지 입력 가능합니다.")
String filename,
@NotNull(message = "파일 내용이 null 입니다.")
String content,
@NotNull(message = "스니펫 순서가 null 입니다.")
int ordinal
) {
}
모든 필드에는 null
이 들어가지 못하도록 @NotNull
어노테이션을 붙여 주었습니다.
추가로, filename
필드는 255자를 넘어가지 못하도록 @Size
어노테이션을 사용해 주었습니다.
public record CreateTemplateRequest(
@NotNull(message = "템플릿 이름이 null 입니다.")
@Size(max = 255, message = "템플릿 이름은 최대 255자까지 입력 가능합니다.")
String title,
@NotNull(message = "스니펫 리스트가 null 입니다.")
@Valid
List<CreateSnippetRequest> snippets
) implements ValidatedSnippetsOrdinalRequest {
@Override
public List<Integer> extractSnippetsOrdinal() {
return snippets.stream().map(CreateSnippetRequest::ordinal).toList();
}
}
마찬가지로 CreateTemplateRequest
의 각 필드에도 @NotNull
로 null
검증을 해 주었습니다. 또한, CreateSnippetRequest
의 각 필드 검증이 진행될 수 있도록 snippets
필드에 @Valid
어노테이션을 사용해 주었습니다.
@Valid
어노테이션은 필드로 가지고 있는 객체의 제약조건을 검사하기 위한 목적으로 필드에 사용할 수 있습니다.
각 어노테이션에는 해당 유효성 검증이 실패하였을 시 사용될 에러 메시지를 작성해 줄 수 있습니다.
마지막으로, 해당 DTO 가 binding 되어 파라미터로 들어오는 곳에 @Valid
어노테이션을 사용해 검증을 수행해 줍니다.
@PostMapping
public ResponseEntity<Void> create(@Valid @RequestBody CreateTemplateRequest createTemplateRequest) {
return ResponseEntity.created(URI.create("/templates/" + templateService.create(createTemplateRequest)))
.build();
}
@Controller
의 메서드 파라미터에@Valid
어노테이션을 붙여주어야 해당 파라미터의 유효성 검증이 진행됩니다.
때로는 주어진 특정 어노테이션을 필드에 붙이는 방법으로 유효성 검증이 어려운 경우가 존재합니다.
이럴때는 @AssertTrue
/ @AssertFalse
어노테이션과 getter 를 사용해 유효성 검증을 진행할 수 있습니다.
방법은 간단합니다. @AssertTrue
/ @AssertFalse
이 붙은 getter 에서 우리가 검증해 줄 로직을 작성해 주면 됩니다.
아래는 content
필드의 값이 65,535 Byte 이하인 것을 검증하는 getter 가 추가된 CreateSnippetRequest
입니다.
public record CreateSnippetRequest(
@NotNull(message = "파일 이름이 null 입니다.")
@Size(max = 255, message = "파일 이름은 최대 255자까지 입력 가능합니다.")
String filename,
@NotNull(message = "파일 내용이 null 입니다.")
String content,
@NotNull(message = "스니펫 순서가 null 입니다.")
int ordinal
) {
@AssertTrue(message = "파일 내용은 최대 65,535 Byte까지 입력 가능합니다.")
public boolean isAcceptableByteLength() {
return this.content
.getBytes(StandardCharsets.UTF_8).length < 65_535;
}
}
위의 CreateSnippetRequest
코드가 조금 마음에 들지 않는데요. 우리가 사용할 메서드가 아님에도, 검증을 위해 getter 가 추가되어 있습니다.
해당 검증도 어노테이션을 추가하는 방법으로 수행해 줄 수 없을까요?
이번엔 우리가 직접 제약 조건 어노테이션을 만들어 보겠습니다. 마침 제약 조건에 Byte 길이를 검증해 주는 어노테이션이 존재하지 않네요.
먼저, 제약 조건 어노테이션입니다.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ByteLengthValidator.class)
public @interface ByteLength {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int max();
int min() default 0;
}
필드에서 사용할 것이므로 @Target(ElementType.FIELD)
를, 어플리케이션 구동 중에 해당 어노테이션 정보를 활용할 것이므로 @Retention(RetentionPolicy.RUNTIME)
을 사용해 주었습니다.
@Constraint()
어노테이션에선 검증 로직이 작성될 Validator 클래스를 넣어주게 됩니다.
필드 중 message
, groups
, payload
필드는 제약 조건을 위한 어노테이션이 꼭 가지고 있어야 하는 필드입니다.
message
필드는 에러 메시지에 사용될 메시지를 저장하기 위해 사용됩니다.
groups
필드는 제약조건을 그룹으로 구분하여 특정 상황에서만 해당 제약조건을 확인하도록 구분하는 목적으로 사용됩니다.
(자세한 설명은 추후에 추가 예정입니다.)
groups
, payload
에 대한 설명은 아래 아티클을 함께 참고해 주세요!
max
, min
필드는 각 필드마다 허용할 byte 제한을 다르게 설정하기 위해 추가해 주었습니다.
다음으로는 검증 로직을 포함하는 Validator 를 작성합니다.
public class ByteLengthValidator implements ConstraintValidator<ByteLength, String> {
private int max;
private int min;
@Override
public void initialize(ByteLength constraintAnnotation) {
max = constraintAnnotation.max();
min = constraintAnnotation.min();
}
@Override
public boolean isValid(String target, ConstraintValidatorContext constraintValidatorContext) {
int byteLength = target.getBytes(StandardCharsets.UTF_8).length;
return min <= byteLength && byteLength <= max;
}
}
validator 는 ConstraintValidator<A, T>
를 구현해야 합니다.
A
는 검증할 필드, 클래스 등에 사용할 어노테이션을, T
는 검증할 필드, 클래스의 타입을 입력해 줍니다. 우리는 아까 만들었던 ByteLength
어노테이션을 사용할 것이고, String
값을 검증해 줄 것이므로 ConstraintValidator<ByteLength, String>
을 구현해 주겠습니다.
public void initialize(A a)
메서드는 Validator 를 초기화 하는 메서드, public boolean isValid(T t, ConstraintValidatorContext constraintValidatorContext)
메서드는 실제 검증을 진행하는 메서드입니다.
isValid()
메서드에서 ByteLength
의 정보를 가져올 수 없기 때문에 initialize()
에서 ByteLength
의 min
, max
값을 ByteLengthValidator
의 필드에 저장해 둡니다.
이후 isValid(String target)
의 target
을 검증합니다.
마지막으로 DTO의 필드에 어노테이션을 붙여 검증을 합니다.
public record CreateSnippetRequest(
@NotNull(message = "파일 이름이 null 입니다.", groups = NotNullGroup.class)
@Size(max = 255, message = "파일 이름은 최대 255자까지 입력 가능합니다.", groups = {ByteGroups.class})
String filename,
@NotNull(message = "파일 내용이 null 입니다.")
@ByteLength(max = 65_535, message = "파일 내용은 최대 65,535 Byte까지 입력 가능합니다.")
String content,
@NotNull(message = "스니펫 순서가 null 입니다.")
int ordinal
) {
}
- https://medium.com/@saiteja-erwa/spring-boot-dto-validation-using-groups-and-payload-attributes-e2c139f5b1ef
- https://stackoverflow.com/questions/64493818/what-is-the-use-of-groups-and-payload-in-custom-annotation-in-java
- https://kapentaz.github.io/spring/Spring-Boo-Bean-Validation-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90/#
- https://www.baeldung.com/spring-mvc-custom-validator
- 백엔드 코드 컨벤션
- 백엔드 기술 스택 및 선정 이유
- 각종 인스턴스 설정 파일 및 구성 위치 가이드
- 1.1.2 버전 ERD (24.09.27)
- 백엔드 CI CD 동작 프로세스
- 로컬 DB 환경 설정
- 백엔드 로깅 전략
- 백엔드 로그 모니터링 구성도
- 스프링 메트릭 모니터링 구성도
- Flyway 로 스키마 관리
- 코드잽 서버 구성도
- Git Submodule 사용 메뉴얼
- 프론트엔드 코드 컨벤션
- 프론트엔드 기술 스택 및 선정 이유
- 프론트엔드 서비스 타겟 환경 및 브라우저 지원 범위 선정
- 프론트엔드 모니터링 및 디버깅 환경 구축
- 프론트엔드 테스트 목록
- 프론트엔드 라이브러리 기술 검토
- 프론트엔드 개발서버, 운영서버 빌드 및 배포 환경 구분
- 목표했던 타겟 환경과 디바이스에서 서비스 핵심 기능 동작 확인
- 프론트엔드 접근성 개선 보고서
- EC2 로그 확인 방법
- VSCode를 통한 EC2 인스턴스 SSH 연결 방법
- 터미널을 통한 EC2 인스턴스 SSH 연결 방법
- NGINX 설정 파일 접근 및 적용 방법
- DB 접속 및 백업 방법
- [QA] 배포 전 체크리스트
- CI 파이프라인 구축
- CD 파이프라인 구축
- 백엔드 CI CD 트러블슈팅
- Lombok Annotation Processor 의존성을 추가한 이유
- 2차 스프린트 기준 ERD
- DTO 검증하기
- ProblemDetail
- Fork된 레포지토리 PR에서 CI Secrets 접근 문제 해결
- AWS CloudWatch 모니터링
- 스프링 메트릭 모니터링 구축 방법
- 로깅과 Logback에 대해 알아보아요.
- 백엔드 CD 파이프라인 Ver.2
- 요청, 응답 로그에 correlationId 를 추가하자!
- 3차 스프린트 기준 ERD
- 더미데이터 생성하고 실행하기
- 쿼리 성능 개선 결과
- 테이블별 인덱스 설정 목록
- 사용자 증가 시 발생할 수 있는 문제 상황과 개선 방안
- k6를 사용한 서버 부하 테스트
- 6차 스프린트 기준 ERD
- Query Performance Improvement Results
- 테스트 전략 및 CI 설정
- CI CD 구조
- 배포 전, 로컬에서 로그인 기능 포함 테스트해보는 법
- stylelint 적용기
- 내 작업 브랜치 중간에 Merge된 동료의 작업물을 넣고 싶다면 pull vs rebase
- [TS] Webpack config
- [TS] Webpack 환경에서 MSW v2 이슈
- [TS] webpack에서 react‐router‐dom 적용 안됨
- 2024.07.28 새 기획 회의
- 2024.07.26 2차 데모데이 후 회의
- 2024.07.11 백엔드 논의 좀 할게요
- 2024.07.11 백엔드 ERD 회의
- 2024.07.09 깃 브랜치 전략, PR 템플릿 회의
- 2024.07.03 주제 선정 회의
- 2023.07.03 팀빌딩데이 킥오프 회의
- 2023.08.07 3차 스프린트 중간회고