Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- 프로그래머스
- DevOps
- static-factory-method
- 3계층 아키텍처
- Level2
- 코딩테스트
- 가용영역
- constructor
- 글또
- QueryDSL
- SpringBoot
- HashMap
- React
- 회고
- builder-pattern
- design-pattern
- 포트앤어댑터 아키텍처
- 클라우드아키텍처
- axios
- ReverseNested
- 다짐글
- 코엑스그랜드볼룸
- 레벨1
- 글또10기
- 글쓰기세미나
- 클린 아키텍처
- UserLand
- OpenSearch
- 헥사고날 아키텍처
- object-creation
Archives
- Today
- Total
oguri's garage
Spring 검증 어노테이션 (@Valid, @NotNull, @NotEmpty, @NotBlank) 본문
1. 기본 검증 어노테이션 비교
Spring에서 문자열 검증에 사용되는 세 가지 핵심 어노테이션을 우체통 편지 검사에 비유할 수 있다.
@NotNull - "편지가 있는지만 확인"
public class User {
@NotNull(message = "이름은 null일 수 없습니다")
private String name;
}
검증 규칙:
null❌ (불가능)""✅ (빈 문자열 허용)" "✅ (공백만 있는 문자열 허용)"John"✅ (정상)
사용 케이스:
- 객체가 존재하기만 하면 되는 경우
- Collection, Map, Array 등에서 null만 막고 싶을 때
@NotEmpty - "편지가 있고 내용이 있는지 확인"
public class User {
@NotEmpty(message = "이름은 비어있을 수 없습니다")
private String name;
@NotEmpty(message = "취미 목록은 비어있을 수 없습니다")
private List<String> hobbies;
}
검증 규칙:
null❌ (불가능)""❌ (빈 문자열 불가능)" "✅ (공백만 있어도 허용)"John"✅ (정상)Collections.emptyList()❌ (빈 컬렉션 불가능)
사용 케이스:
- 문자열, Collection, Map, Array에서 빈 값을 막고 싶을 때
- 최소한 하나의 요소나 문자는 있어야 하는 경우
@NotBlank - "편지가 있고 의미있는 내용이 있는지 확인"
public class User {
@NotBlank(message = "이름은 공백일 수 없습니다")
private String name;
}
검증 규칙:
null❌ (불가능)""❌ (빈 문자열 불가능)" "❌ (공백만 있는 문자열 불가능)"John"✅ (정상)" John "✅ (앞뒤 공백은 trim되어 정상)
사용 케이스:
- String에만 적용 가능
- 의미있는 텍스트가 반드시 필요한 경우 (이름, 제목 등)
2. 실제 예시로 이해하기
public class UserRegistrationForm {
@NotNull(message = "사용자 ID는 null일 수 없습니다")
private Long userId; // Long 타입 - @NotNull만 사용 가능
@NotBlank(message = "사용자명은 필수입니다")
private String username; // 의미있는 텍스트 필요
@NotEmpty(message = "이메일은 비어있을 수 없습니다")
@Email(message = "올바른 이메일 형식이 아닙니다")
private String email; // 빈 값 방지 + 이메일 검증
@NotEmpty(message = "최소 하나의 취미는 선택해야 합니다")
private List<String> hobbies; // 컬렉션 - @NotEmpty 사용
}
테스트 케이스:
// ❌ 검증 실패 케이스들
UserRegistrationForm invalidUser1 = UserRegistrationForm.builder()
.userId(null) // @NotNull 위반
.username("") // @NotBlank 위반
.email(" ") // @Email 위반 (@NotEmpty는 통과)
.hobbies(List.of()) // @NotEmpty 위반
.build();
// ✅ 검증 성공 케이스
UserRegistrationForm validUser = UserRegistrationForm.builder()
.userId(123L)
.username("john_doe")
.email("john@example.com")
.hobbies(List.of("reading", "gaming"))
.build();
3. @Valid - 검증 실행의 트리거
@Valid는 실제 검증을 실행하라고 명령하는 스위치 역할
3.1 컨트롤러에서의 사용
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<String> createUser(
@Valid @RequestBody UserRegistrationForm form, // 검증 실행!
BindingResult result) {
if (result.hasErrors()) {
// 검증 에러 처리
return ResponseEntity.badRequest()
.body("검증 실패: " + result.getAllErrors());
}
// 검증 성공 시 비즈니스 로직 실행
userService.createUser(form);
return ResponseEntity.ok("사용자 생성 성공");
}
}
3.2 중첩 객체 검증
@Valid는 중첩된 객체까지 재귀적으로 검증합니다.
public class Order {
@NotBlank(message = "주문 번호는 필수입니다")
private String orderNumber;
@Valid // 중첩 객체 검증을 위한 @Valid
@NotNull(message = "배송 주소는 필수입니다")
private Address shippingAddress;
@Valid // 컬렉션의 각 요소도 검증
@NotEmpty(message = "최소 하나의 상품은 주문해야 합니다")
private List<OrderItem> items;
}
public class Address {
@NotBlank(message = "도시명은 필수입니다")
private String city;
@NotBlank(message = "상세 주소는 필수입니다")
private String street;
@NotEmpty(message = "우편번호는 필수입니다")
@Size(min = 5, max = 6, message = "우편번호는 5-6자리여야 합니다")
private String zipCode;
}
public class OrderItem {
@NotBlank(message = "상품명은 필수입니다")
private String productName;
@Min(value = 1, message = "수량은 1개 이상이어야 합니다")
private Integer quantity;
}
검증 동작:
@PostMapping("/orders")
public ResponseEntity<String> createOrder(@Valid @RequestBody Order order) {
// @Valid에 의해 다음이 모두 검증됨:
// 1. Order 객체 자체 (@NotBlank orderNumber)
// 2. Address 객체 (@NotBlank city, street, @NotEmpty zipCode)
// 3. List<OrderItem>의 각 OrderItem (@NotBlank productName, @Min quantity)
}
4. 의존성 설정
Spring Boot에서 검증 기능을 사용하려면 다음 의존성이 필요합니다:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
이 의존성이 포함하는 내용:
- Jakarta Bean Validation API (JSR 380)
- Hibernate Validator (구현체)
- 관련 Spring 통합 라이브러리
5. 실무 사용 패턴
5.1 Request DTO 패턴
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CreateUserRequest {
@NotBlank(message = "사용자명은 필수입니다")
private String username;
@NotBlank(message = "비밀번호는 필수입니다")
@Size(min = 8, message = "비밀번호는 최소 8자리여야 합니다")
private String password;
@NotBlank(message = "이메일은 필수입니다")
@Email(message = "올바른 이메일 형식이 아닙니다")
private String email;
@Valid
@NotNull(message = "프로필 정보는 필수입니다")
private UserProfile profile;
}
public class UserProfile {
@NotBlank(message = "실명은 필수입니다")
private String realName;
@Min(value = 14, message = "최소 연령은 14세입니다")
@Max(value = 120, message = "최대 연령은 120세입니다")
private Integer age;
}
5.2 컨트롤러 예외 처리
@RestController
@Slf4j
public class UserController {
@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.ok(response);
}
// 검증 실패 시 자동으로 호출되는 예외 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse errorResponse = ErrorResponse.builder()
.message("입력값 검증 실패")
.errors(errors)
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
}
6. 고급 검증 패턴
6.1 조건부 검증 (@Validated + 그룹)
// 검증 그룹 정의
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserRequest {
@NotNull(groups = UpdateGroup.class, message = "업데이트 시 ID는 필수입니다")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class},
message = "사용자명은 필수입니다")
private String username;
}
@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(
@Validated(CreateGroup.class) @RequestBody UserRequest request) {
// id는 검증하지 않고, username만 검증
}
@PutMapping("/users/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Validated(UpdateGroup.class) @RequestBody UserRequest request) {
// id와 username 모두 검증
}
6.2 커스텀 검증 어노테이션
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
String message() default "올바른 전화번호 형식이 아닙니다";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
return phoneNumber != null && phoneNumber.matches("^010-\\d{4}-\\d{4}$");
}
}
// 사용법
public class UserRequest {
@NotBlank
@ValidPhoneNumber // 커스텀 검증
private String phoneNumber;
}
7. 검증 어노테이션 선택 가이드
| 상황 | 권장 어노테이션 | 이유 |
|---|---|---|
| Long, Integer 등 숫자 타입 | @NotNull |
null만 방지하면 됨 |
| 사용자명, 제목 등 의미있는 텍스트 | @NotBlank |
공백 문자열까지 방지 |
| 이메일 (기본 검증만) | @NotEmpty + @Email |
빈 값 방지 + 형식 검증 |
| List, Set 등 컬렉션 | @NotEmpty |
빈 컬렉션 방지 |
| Optional한 필드 | 검증 어노테이션 없음 | null 허용 |
8. 실무 권장 패턴
8.1 Request DTO 표준 패턴
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CreatePostRequest {
@NotBlank(message = "제목은 필수입니다")
@Size(max = 100, message = "제목은 100자를 초과할 수 없습니다")
private String title;
@NotBlank(message = "내용은 필수입니다")
@Size(max = 5000, message = "내용은 5000자를 초과할 수 없습니다")
private String content;
@NotEmpty(message = "최소 하나의 카테고리는 선택해야 합니다")
private List<String> categories;
@Valid // 중첩 검증
private PostMetadata metadata;
}
public class PostMetadata {
@NotEmpty(message = "태그는 최소 하나 필요합니다")
private List<String> tags;
private Boolean isPublic = true; // 기본값 설정, 검증 불필요
}
8.2 컨트롤러 표준 패턴
@RestController
@RequestMapping("/api/posts")
@Validated
public class PostController {
@PostMapping
public ResponseEntity<PostResponse> createPost(
@Valid @RequestBody CreatePostRequest request) {
PostResponse response = postService.createPost(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
// 글로벌 예외 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fieldErrors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse errorResponse = ErrorResponse.builder()
.message("입력값이 올바르지 않습니다")
.fieldErrors(fieldErrors)
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
}
8.3 서비스 레이어 검증
@Service
@Validated // 서비스 레벨에서도 검증 가능
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public UserResponse createUser(
@Valid CreateUserRequest request) { // 서비스에서도 검증
// 중복 체크 등 비즈니스 검증
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("이미 존재하는 사용자명입니다");
}
User user = User.builder()
.username(request.getUsername())
.email(request.getEmail())
.build();
User savedUser = userRepository.save(user);
return UserResponse.builder()
.id(savedUser.getId())
.username(savedUser.getUsername())
.email(savedUser.getEmail())
.build();
}
}
9. 검증 실행 흐름
1. HTTP 요청 수신
↓
2. @Valid 어노테이션 감지
↓
3. Hibernate Validator 자동 실행
↓
4. 각 필드별 검증 어노테이션 확인
(@NotBlank, @NotEmpty, @NotNull 등)
↓
5. 중첩 객체가 있으면 @Valid로 재귀 검증
↓
6. 검증 결과
├── 성공 → 컨트롤러 메소드 실행
└── 실패 → MethodArgumentNotValidException 발생
10. 주요 주의사항
- @NotBlank는 String에만 사용 가능 - Collection에는 @NotEmpty 사용
- @NotEmpty가 @NotNull을 포함함 - 중복 어노테이션 불필요
- @Valid 없으면 검증 실행 안됨 - 어노테이션만 있어도 검증되지 않음
- 중첩 객체는 반드시 @Valid 필요 - 재귀 검증을 위해 필수
- spring-boot-starter-validation 의존성 필요 - 자동 설정을 위해 필수
'개발하다 > Spring' 카테고리의 다른 글
| Spring vs Spring Boot 핵심 차이점 (0) | 2025.10.03 |
|---|---|
| Lombok 어노테이션(@Builder, @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor, @Data) (0) | 2025.10.02 |
| QueryDSL Q-Type 클래스 - 개념, 생성, 활용법 (0) | 2025.10.01 |
| 쉽게쉽게 알아보는 헥사고날 아키텍처! (8) | 2024.10.30 |
| @RestController 사용 시 불필요한 속성 포함하는 문제 (8) | 2024.05.28 |