애플리케이션의 비즈니스 로직이 올바르게 동작하려면 데이터를 사전 검증하는 작업이 필요합니다.
이것을 유효성 검사 또는 데이터 검증이라 부릅니다.
1. 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산돼 있어 관리하기가 어렵습니다.
2. 검증 로직에 중복이 많아 여러 곳에 유사한 기능의 코드가 존재할 수 있습니다.
3. 검증해야 할 값이 많다면 검증하는 코드가 길어집니다.
이러한 문제로 코드가 복잡해지고 가독성이 떨어집니다.
자바 진영에서는 Bean Validation이라는 데이터 유효성 검사 프레임워크를 제공합니다.
Bean Validation은 어노테이션을 통해 다양한 데이터를 검증하는 기능을 제공합니다.
Hibernate Validator는 Bean Validation 명세의 구현체입니다.
스프링 부트에서는 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용하고 있습니다.
프로젝트를 생성할 때 아래의 의존성을 추가합니다.
Developer Tools : Lombok, Spring Configuration Processor
Web : Spring Web
SQL : Spring Data JPA, Maria DB Driver
7장의 자바 파일과 SwagerConfiguration 클래스 파일을 가져오고 관련 의존성을 pom.xml에 추가합니다.
pom.xml 파일에 다음과 같이 유효성 검사 라이브러리를 의존성으로 추가합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시합니다.
스프링부트 프로젝트에서는 유효성 검사를 DTO 객체를 대상으로 수행하는 것이 일반적입니다.
다음과 같이 ValidRequestDto 클래스를 생성합니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
private String phoneNumber;
@Min(value = 20)
@Max(value = 40)
private int age;
@Size(min = 0, max = 40)
private String description;
@Positive
private int count;
@AssertTrue
private boolean booleanCheck;
}
각 어노테이션은 유효성 검사를 위한 조건을 설정하는 데 사용됩니다.
대표적인 어노테이션은 다음과 같습니다.
1. 문자열 검증
@Null : Null 값만 허용
@NotNull : Null을 허용하지 않음. "", " "는 허용
@NotEmpty : null, ""을 허용하지 않음. " "는 허용
@NotBlank : null, "", " " 모두 허용하지 않음
2. 최댓값/최솟값 검증
DecimalMin(value = "$numberString") : $numberString 이상의 값을 허용
DecimalMax(value = "$numberString") : $numberString 이하의 값을 허용
@Min(value = $number) : $number 이상의 값을 허용
@Max(value = $number) : $number 이하의 값을 허용
3. 값의 범위 검증
@Positive : 양수를 허용
@PositiveOrZero : 0을 포함한 양수를 허용
@Negative : 음수를 허용
@NegativeOrZero : 0을 포함한 음수를 허용
4. 시간에 대한 검증
@Future : 현재보다 미래의 날짜를 허용
@FutureOrPresent : 현재를 포함한 미래의 날짜를 허용
@Past : 현재보다 과거의 날짜를 허용
@PastOrPresent : 현재를 포함한 과거의 날짜를 허용
5. 이메일 검증
@Email : 이메일 형식을 검사. ""는 허용
6. 자릿수 범위 검증
@Digits : 수치의 범위를 설정합니다.
7. Boolean 검증
@AssertTrue : true 체크, null 값은 체크하지 않음
@AssertFalse : false 체크, null 값은 체크하지 않음
8. 문자열 길이 검증
@Size(min = $minNumber, max = $maxNumber) : 문자열의 길이를 제한
9. 정규식 검증
@Pattern : 정규식을 검사
@Validated라는 어노테이션은 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있습니다.
public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}
ValidationGroup1과 ValidationGroup2라는 인터페이스를 생성하여 그룹화하는 용도로 사용합니다.
다음과 같이 ValidatedRequestDto 클래스를 생성합니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
private String phoneNumber;
@Min(value = 20, groups = ValidationGroup1.class)
@Max(value = 40, groups = ValidationGroup1.class)
private int age;
@Size(min = 0, max = 40)
private String description;
@Positive(groups = ValidationGroup2.class)
private int count;
@AssertTrue
private boolean booleanCheck;
}
groups 속성을 사용해 ValidationGroup1 그룹과 ValidationGroup2 그룹을 설정했습니다.
이 설정을 통해 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 지정할 수 있습니다.
그룹을 어떻게 설정해서 유효성 검사를 실시할지 결정하는 것은 컨트롤러의 @Validated 어노테이션에서 합니다.
다음과 같이 컨트롤러에 @Validated 어노테이션을 추가합니다.
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
@PostMapping("/valid")
public ResponseEntity<String> checkValidationByValid(
@Valid @RequestBody ValidRequestDto validRequestDto) {
LOGGER.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
}
@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
@Validated @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation1(
@Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
@Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
@Validated({ValidationGroup1.class,
ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
}
자바에서는 오류를 try/catch, throw 구문을 활용해 처리합니다.
스프링 부트에서는 더욱 편리하게 예외 처리를 할 수 있는 기능을 제공합니다.
예외(exception)란 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황을 의미합니다.
예외는 개발자가 직접 처리할 수 있는 것이므로 미리 코드 설계를 통해 처리할 수 있습니다.
에러(error)는 주로 자바의 가상머신에서 발생시키는 것으로서, 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없습니다.
대표적인 예로 메모리 부족(OutOfMemory), 스택 오버플로(StackOverFlow) 등이 있습니다.
모든 예외 클래스는 Throwable 클래스를 상속받습니다.
클래스는 크게 Checked Exception과 Unchecked Exception으로 구분할 수 있습니다.
Checked Exception은 컴파일 단계에서 확인 가능한 예외 상황입니다.
Unchecked Exception은 런타임 단계에서확인되는 예외 상황을 나타냅니다.
스프링 부트에서는 @ControllerAdvice와 @ExceptionHandler를 통해 컨트롤러의 예외를 처리합니다.
다음과 같이 CustomExceptionHandler 클래스를 작성합니다.
@RestControllerAdvice
public class CustomExceptionHandler {
private final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("Advice 내 exceptionHandler 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
@RestControllerAdvice 어노테이션은 @Controller나 @RestController에서 발생하는 예외를 한 곳에서 관리하고 처리할 수 있게 하는 기능을 수행합니다.
@ExceptionHandler 어노테이션은 @Controller나 @RestController가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드를 정의할 때 사용합니다.
value 속성으로 어떤 예외 클래스를 처리할지 등록할 수 있습니다.
특정 컨트롤러에서만 동작하는 @ExceptionHandler 메서드를 생성해서 처리할 수도 있습니다.
@RestController
@RequestMapping("/exception")
public class ExceptionController {
private final Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class);
@GetMapping
public void getRuntimeException() {
throw new RuntimeException("getRuntimeException 메소드 호출");
}
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("클래스 내 handleException 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
위와 같이 컨트롤러 클래스 내에 @ExceptionHandler 어노테이션을 사용한 메서드를 선언하면 해당 클래스에 국한해서 예외 처리를 할 수 있습니다.
스프링 부트 액추에이터는 HTTP 엔드포인트나J뜨 를 활용해 애플리케이션을 모니터링하고 관리할 수 있는 기능을 제공합니다.
프로젝트를 생성할 때 아래의 의존성을 추가합니다.
Developer Tools : Spring Configuration Processor
Web : Spring Web
SwagerConfiguration 클래스 파일을 가져오고 관련 의존성을 pom.xml에 추가합니다.
pom.xml 파일에 다음과 같이 spring-boot-starter-actuator 모듈의 종속성 추가합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
액추에이터의 엔드포인트는 애플리케이션의 모니터링을 사용하는 경로입니다.
액추에이터를 추가하면 기본적으로 엔드포인트 URL로 /actuator가 추가되며, 이 뒤에 경로를 추가해 상세 내역에 접근합니다.
엔드포인트를 활성화하려면 application.properties 파일에 다음과 같이 속성을 추가하면 됩니다.
management.endpoint.shutdown.enabled=true
management.endpoint.caches.enabled=false
1. 애플리케이션 기본 정보(/info)
다음과 같이 application.properties 파일에서 애플리케이션의 정보를 작성할 수 있습니다.
info.organization.name=wikibooks
info.contact.email=thinkground.flature@gmail.com
info.contact.phoneNumber=010-1234-5678
http://localhost:8080/actuator/info URL에 접근하면 JSON 형태로 정보가 출력됩니다.
2. 애플리케이션 상태(/health)
별도의 설정 없이 http://localhost:8080/actuator/health URL에 접근하면 {"status":"UP"}의 결과를 확인할 수 있습니다.
다음과 같이 application.properties 파일에서 애플리케이션 상세 상태 확인을 설정할 수 있습니다.
management.endpoint.health.show-details=always
3. 빈 정보 확인(/beans)
http://localhost:8080/actuator/beans URL에 접근하면 스프링 빈의 전체 목록을 확인할 수 있습니다.
4. 스프링 부트의 자동설정 내역 확인(/conditions)
http://localhost:8080/actuator/conditions URL에 접근하면 스프링 부트의 자동설정 조건 내역을 확인할 수 있습니다.
5. 스프링 환경변수 정보(/env)
http://localhost:8080/actuator/env URL에 접근하면 스프링의 환경변수 정보를 확인할 수 있습니다.
6. 로깅 레벨 확인(/loggers)
http://localhost:8080/actuator/loggers URL에 접근하면 애플리케이션의 로깅 레벨 수준이 어떻게 설정돼 있는지 확인할 수 있습니다.
1. ValidRequestDto 클래스의 각 필드에 알맞은 어노테이션을 추가하세요.
name 필드에는 null, "", " " 모두 허용하지 않고, email 필드에는 email 형식을 허용합니다.
public class ValidRequestDto {
// 알맞은 어노테이션 추가
private String name;
// 알맞은 어노테이션 추가
private String email;
}
2. RuntimeException을 처리하는 메서드를 정의하는 어노테이션을 추가하세요.
@RestControllerAdvice
public class CustomExceptionHandler {
// 알맞은 어노테이션 추가
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request) {
// 생략
}
}
[출처] 장정우, 「스프링부트 핵심 가이드」 10장 ~ 11장
Corner Spring 3
ⓒ Pumpkin
[스프링 3팀] 13장. 서비스 인증과 권한 부여 (0) | 2025.01.31 |
---|---|
[스프링 3팀] 12장. 서버 간 통신 (0) | 2025.01.24 |
[스프링 3팀] 9장. 연관관계 매핑 (0) | 2025.01.10 |
[스프링 3팀] 8장. Spring Data JPA 활용 (0) | 2025.01.08 |
[스프링 3팀] 7장. 테스트 코드 작성하기 (0) | 2024.12.27 |