상세 컨텐츠

본문 제목

[스프링 2팀] 10~11장. 유효성 검사와 예외처리 & 액추에이터 활용하기

24-25/Spring 2

by dalpaeng4 2025. 1. 17. 10:00

본문

728x90

 

10장. 유효성 검사와 예외처리

 

애플리케이션의 비즈니스 로직이 올바르게 동작하려면 데이터를 사전 검증하는 작업이 필요함

=> 유효성 검사 or 데이터 검증이라고 부름

 

10.1 일반적인 애플리케이션 유효성 검사의 문제점

일반적으로 사용되는 데이터 검증 로직에는 몇 가지 문제점 존재

=> 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산되어 있어 관리하기 어려움

& 검증 로직에 의외로 중복이 많아 여러 곳에 유사한 기능의 코드가 존재 가능

& 검증해야 할 값이 많다면 검증하는 코드가 길어짐

=> 코드가 복잡해지고 가독성 떨어짐

 

문제 해결을 위해 자바 진영에서는 2009년부터 Bean Validation이라는 데이터 유효성 검사 프레임워크 제공

Bean Validation=> 어노테이션을 통해 다양한 데이터를 검증하는 기능 제공

Bean Validation을 사용한다는 것은 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 의미임

& Bean Validation은 어노테이션을 사용한 검증 방식이기 때문에 코드의 간결함도 유지 가능함

 

10.2 Hibernate Validator

Hibernate Validator=> Bean Validation 명세의 구현체임

스프링 부트에서는 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용 중임

Hibernate Validator는 JSR-303 명세의 구현체로서 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능하게 도와줌

 

10.3 스프링 부트에서의 유효성 검사

애플리케이션에 유효성 검사 기능 추가

 

프로젝트 생성

  • groupId: com.springboot
  • artifactId: valid_exception
  • name: valid_exception
  • Developer Tools: Lombok, Spring Configuration Processor
  • Web: Spring Web
  • SQL: Spring Data JPA, MariaDB Driver

+ 7장에서 자바 파일을 가져와 기본적인 프로젝트 생성

+ 5장에서 다룬 것처럼 SwaggerConfiguration 클래스 파일 생성 & 관련 의존성을 pom.xml에 추가

 

원래 스트링 부트의 유효성 검사 기능은 spring-boot-starter-web 에 포함돼 있었음

but 스프링 부트 2.3 버전 이후로 별도의 라이브러리로 제공 중임

pom.xml 파일에 유효성 검사 라이브러리를 의존성으로 추가하면 사용 가능함

<dependencies>
//생략
	<dependency>
    	<groupId>org.springframework.boot</groupId>
    </dependency>
//생략
</dependencies>

 

 

유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시함

스프링 부트 프로젝트에서는 계층 간 데이터 전송에 대체로 DTO 객체를 활용하고 있기 때문에 아래와 같이 유효성 검사를 DTO 객체를 대상으로 수행하는 것이 일반적임

 

DTO와 컨트롤러 생성

ValidRequestDto 라는 이름의 DTO 객체 생성

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
	
    @NotBlank
    String name;
    
    @Email
    String email;
    
    @Pattern(regexp = "01(?:0|1|[6-9]).-]?(\\d{3}|\\d{4}[.-]?(\\d{4})$")
    String phoneNumber;
    
    @Min(value = 20) @Max(value = 40)
    String description;
    
    @Positive
    int count;
    
    @AsserTrue
    boolean booleanCheck;
    
}

코드를 보면 각 필드에 어노테이션이 선언된 것을 볼 수 있음

각 어노테이션은 유효성 검사를 위한 조건을 설정하는 데 사용됨

대표적인 어노테이션(추려서 작성함)

문자열 검증

  • @Null: null 값만 허용함
  • @NotEmpty: null, ""을 허용하지 않음, " "는 허용

최댓값/최솟값 검증

  • BigDecimal, BigInteger, int, long 등의 타입 지원
  • @DemicalMax(value = "$numberString"): $numberString보다 작은 값 허용

값의 범위 검증

  • BigDecimal, BigInteger, int, long 등의 타입 지원
  • @Positive: 양수를 허용함
  • @PositiveOrZero: 0을 포함한 양수를 허용함

시간에 대한 검증

  • Date, LocalDate, LocalDateTime 등의 타입 지원
  • @Future: 현재보다 미래의 날짜 허용

이메일 검증

  • @Email: 이메일 형식 검사, ""는 허용

자릿수 범위 검증

  • BigDecimal, BigInteger, int, long 등의 타입 지원
  • @Digits(integer = $number1, fraction = $number2): $number1의 정수 자릿수와 $number2의 소수 자릿수 허용

Boolean 검증

  • @AssertTrue: true인지 체크, null 값은 체크 x
  • @AssertFalse: false인지 체크, null 값은 체크 x

문자열 길이 검증

  • @Size(min = $number1, max = $number2): $number1 이상 $number2 이하의 범위 허용

정규식 검증

  • @Pattern(regexp = "$expression"): 정규식 검사, 정규식은 자바의 java.util.regex.Pattern 패키지의 컨벤션을 따름

 

유효성 검사에 사용하는 어노테이션은 인텔리제이 IDEA에서도 확인 가능

화면 우측의 [Bean Validation] 탭을 선택하면 목록 확인 가능함

 

인텔리제이 IDEA에는 자동으로 우측에 [Bean Validation] 탭을 추가하는 기능이 있는데, 만약 화면에서 이를 확인할 수 없다면 메뉴에서 [View]-> [Tool Windows]-> [Bean Validation]을 차례로 선택해 탭 추가 가능

 

DTO를 사용하는 컨트롤러 객체 생성- ValidationController 생성

@RestController
@RequestMapping("/validation")
public class ValidationController {
	private final Logger LOGGER = LoggerFactor.getLogger(ValidationController.class);
    
    @PostMapping("/valid")
    public ReponseEntity<String> checkValidationByValid(
    	@Valid @RequestBody ValidRequestDto validRequestDto) {
        LOGGER.info(validRequestDto.toString());
        return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
        }
}

checkValidationByValid() 메서드는 ValidRequestDto 객체를 RequestBody 값으로 받고 있음

이 경우 @Valid 어노테이션을 지정해야 DTO 객체에 대해 유효성 검사를 수행함

동작을 확인하기 위해 애플리케이션 실행->  Swagger 페이지에 접속

checkValidationByValid() 메서드를 호출하기 위해 아래와 같이 입력

{
"age": 30,
"booleanCheck": true,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}

위의 값은 유효성 검사를 통과할 수 있는 값들임

age: @Min(value=20), @Max(value=40) 데이터만 받겠다는 것을 의미

booleanCheck: @AssertTrue를 통해 값이 true인지 체크

count: @Positive가 설정됨-> 0이 아닌 양수가 값으로 들어오는지 체크

description: @Size를 통해 문자열의 길이 제한

email: 값에 '@' 문자가 있는지 확인

name: @NotBlank로 null 값이나 "", " " 모두 허용하지 않게 설정해서 값을 의무적으로 받도록 설정함

phoneNumber: @Pattern을 통해 정규식 설정, regexp 속성의 값을 "01(?:0|1|[6-9]).-]?(\\d{3}|\\d{4}[.-]?(\\d{4})$"로 설정하면 휴대전화 번호 형식인지 검증 가능

 

 @Valid 어노테이션=> 자바에서 지원하는 어노테이션, 스프링도 @Validated라는 별도의 어노테이션으로 유효성 검사 지원함

@Validated=> @Valid 어노테이션의 기능을 포함하고 있기 때문에 @Validated로 변경 가능

@Validated=> 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있음

검증 그룹은 별다른 내용이 없는 마커 인터페이스를 생성해서 사용함

data 패키지 내에 group 패키지 생성-> ValidationGroup1 & ValidationGroup2 인터페이스 생성

두 인터페이스 모두 내부 코드 x, 인터페이스만 생성해서 그룹화하는 용도로 사용함

ValidationGroup1 인터페이스

public interface ValidationGroup1{

}

ValidationGroup2 인터페이스

public interface ValidationGroup1{

}

검증 그룹 설정은 DTO 객체에서 함, 새로운 DTO 객체 생성

@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, group=ValidationGroup1.class)
    @Max(value=40, group=ValidationGroup1.class)
    private int age;
    
    @Size(min=0, max=40)
    private String description;
    
    @Positive(groups=ValidationGroup2.class)
    private int count;
    
    @AsserTrue
    private boolean booleanCheck;
    
}

17~18번 줄에서 @Min, @Max 어노테이션의 groups 속성을 사용해 ValidationGroup1 그룹 설정 & 24번째 줄에서 ValidationGroup2 설정 => 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 지정함

 

실제로 그룹을 어떻게 설정해서 유효성 검사를 실시할지 결정하는 것은 @Validated 어노테이션에서 함

유효성 검사 그룹을 설정하기 위해 컨트롤러 클래스에 메서드 추가

@RestController
@RequestMapping("/validation")
public class ValidationController {
	private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
    
    @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> checkValidation(
    	@Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
        	LOGGER.info(validatedRequestDto.toString());
            return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
        }
        
    @PostMapping("/validated/group2")
    public ResponseEntity<String> checkValidation(
    	@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());
        }
}

9번째 줄에서는 @Validated 어노테이션이 속성을 지정하지 않고 16번 주에서는 ValidationGroup1, 23번 줄에서는 ValidationGroup2를 그룹을 지정함, 30~31 줄에서는 두 그룹을 모두 지정함

 

  • @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우에는 groups가 설정되지 않은 필드에 대해 유효성 검사 수행
  • @Validated 어노테이션에 특정 그룹을 설정하는 경우에는 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사 수행

=> 그룹을 지정해서 유효성 검사를 실시하는 경우에는 어떤 상황에 사용할지를 적절하게 설계해야 의도대로 유효성 검사 실시 가능, 만약 이를 제대로 설계 x-> 비효율적이거나 생산적이지 못한 패턴을 의미하는 안티 패턴이 발생하게 됨

 

전화번호 형식이 일치하는지 확인하는 간단한 유효성 검사 어노테이션 생성

ConstraintValidator 인터페이스를 구현하는 클래스 생성해야 함-> TelephoneValidator 클래스 생성

public class TelephonValidator implements ConstraintValidator<Telephone, String> {
	@Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
    	if (value==null){
        	return false;
        }
        return value.matches("01(?:0|1|[6-9]).-]?(\\d{3}|\\d{4}[.-]?(\\d{4})$");
    }
}

 

ConstraintValidator 인터페이스에서 정의한 Telephone 인터페이스

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
    String message() default "전화번호 형식이 일치하지 않습니다.";
    Class[] groups() default {};
    Class[] payload() default {};
}

@Retention 어노테이션은 이 어노테이션이 실제로 적용되고 유지되는 범위를 의미함

@Retention의 적용 범위는 RetentionPolicy를 통해 지정

지정 가능한 항목

  • RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조, 리플렉션이나 로깅에 많이 사용되는 정책임
  • RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유지
  • RetentionPolicy.SOURCE : 컴파일 전까지만 유지, 컴파일 이후에는 사라짐

10.4 예외 처리

애플리케이션 개발 시 발생하는 오류-> 자바에서는 try/catch, throw 구문을 활용해 처리함

스프링 부트에서는 더욱 편리하게 예외 처리를 할 수 있는 기능 제공

 

예외=> 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황

에러=> 주로 자바의 가상머신에서 발생시키는 것, 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없음

 

모든 예외 클래스는 Throwable 클래스를 상속받음-> 크게 Checked Exception & Unchecked Exception 으로 구분 가능

Checked Exception=> 컴파일 단계에서 확인 가능한 예외 상황

Unchecked Exception=> 런타임 단계에서 확인되는 예외 상황

 

예외가 발생했을 때 처리하는 방법

  • 예외 복구: 예외 상황을 파악해서 문제를 해결하는 방식
  • 예외 처리 회피: 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식
  • 예외 전환: 예외 복구 + 예외 처리 회피 방식을 적절하게 섞은 방식

스프링 부트의 예외 처리 방식

  • @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
  • @ExceptionHandler를 통해 특정 컨트롤러의 예외 처리

커스텀 예외

커스텀 예외를 만들어서 사용하면 네이밍에 개발자 의도를 담을 수 있음-> 이름만으로도 어느 정도 예외 상황 짐작 가능

표준 예외를 사용할 때는 예외 메시지를 상세히 작성해야 하는 번거로움이 있음

커스텀 예외를 사용하면 애플리케이션에서 발생하는 예외를 개발자가 직접 관리하기 수월해짐

+ 예외 상황에 대한 처리 용이

 

11. 액추에이터 활용하기

 

11.1 프로젝트 생성 및 액추에이터 종속성 추가

프로젝트 생성

  • groupId: com.springboot
  • artifactId: actuator
  • name: actuator
  • Developer Tools: Spring Configuration Processor
  • Web: Spring Web

+ 이전 장의 SwaggerConfiguration 클래스를 가져오고 의존성 추가

+ spring-boot-starter-actuator 모듈의 종속성 추가

<dependencies>
//생략
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
//생략
</dependencies>

 

11.2 엔드포인트

액추에이터의 엔드포인트=> 애플리케이션의 모니터링을 사용하는 경로

스프링 부트에는 여러 내장 엔드포인트 포함 & 커스텀 엔드포인트도 추가 가능

자주 활용되는 액추에이터의 엔드포인트(추려서 작성함)

  • auditevents: 호출된 Audit 이벤트 정보 표시, AuditEventRepository 빈이 필요
  • configprops: @ConfigurationProperties의 속성 리스트를 표시함
  • httptrace: 가장 최근에 이뤄진 100건의 요청 기록을 표시함, HttpTraceRepository 빈이 필요함
  • metrics:  애플리케이션의 메트릭 정보를 표시함
  • sessions: 스프링 세션 저장소에서 사용자의 세션을 검색하고 삭제 가능, 스프링 세션을 사용하는 서블릿 기반 웹 애플리케이션이 필요함
  • threaddump: 스레드 덤프를 수행함

11.3 액추에이터 기능 살펴보기

  • 액추에이터의 /info 엔드포인트를 활용하면: 가동 중인 애플리케이션의 정보를 볼 수 있음
  • 액추에이터의 /health 엔드포인트를 활용하면: 애플리케이션의 상태를 확인할 수 있음
  • 액추에이터의 /beans 엔드포인트를 활용하면: 스프링 컨테이너에 등록된 스프링 빈의 전체 목록을 표시 가능
  • 액추에이터의 /conditions 엔드포인트를 활용하면: 스프링 부트의 자동설정(AutoConfiguration) 조건 내역 확인 가능
  • 액추에이터의 /env 엔드포인트를 활용하면: 스프링의 환경변수 정보 확인 가능
  • 액추에이터의 /loggers 엔드포인트를 활용하면: 애플리케이션의 로깅 레벨 수준이 어떻게 설정돼 있는지 확인 가능

QUIZ

1. 예외 처리 방법 중 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식은 ()이다.

답: 예외 처리 회피
2. 자주 활용되는 액추에이터의 엔드포인트 중 @ConfigurationProperties의 속성 리스트를 표시하는 것은 ()이다.

답: configprops

3. 애플리케이션의 비즈니스 로직이 올바르게 동작하기 위해 데이터를 사전 검증하는 작업은 ()이다.

답: 유효성 검사 또는 데이터 검증

4. 스프링 부트 유효성 검사에 사용되는 대표적인 어노테이션 중 최댓값/최솟값 검증에 쓰이고 $numberString 보다 큰 값을 허용하는 어노테이션은 ()이다.

답: @DemicalMin(value = "$numberString")

5. 스프링 부트 유효성 검사에 사용되는 대표적인 어노테이션 중 값의 범위 검증에 쓰이고 양수를 허용하는 어노테이션은 ()이다.

답: @Positive

6. @Retention 어노테이션이 지정 가능한 RetentionPolicy 항목 중 컴파일러가 클래스를 참조할 때까지 유지하는 것은 ()이다.

답: RetentionPolicy.CLASS

7. Bean Validation 명세의 구현체이며 스프링 부트에서 유효성 검사 표준으로 채택해서 사용 중인 것은 ()이다.

답: Hibernate Validator

 

코드 QUIZ

1. ValidRequestDto 객체를 생성하는 코드이다. 코드의 '1-1', '1-2' 부분을 채우시오.

//생략
public class ValidRequestDto {
	
    @NotBlank
    String name;
    
    @Email
    String email;
    
    @Pattern(regexp = "01(?:0|1|[6-9]).-]?(\\d{3}|\\d{4}[.-]?(\\d{4})$")
    String phoneNumber;
    
    **1-1**(value = 20) **1-2**(value = 40)
    String description;
    
    @Positive
    int count;
    
    @AsserTrue
    boolean booleanCheck;
    
}

답: 1-1 @Min, 1-2 @Max

 

2. 유효성 검사 그룹을 설정하기 위해 컨트롤러 클래스에 메서드를 추가한 코드이다. 코드의 '*****' 부분에 들어갈 어노테이션을 작성하시오.

@RestController
@RequestMapping("/validation")
public class ValidationController {
	private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
    
    @PostMapping("/validated")
    public ResponseEntity<String> checkValidation(
    	@***** @RequestBody ValidatedRequestDto validatedRequestDto) {
        	LOGGER.info(validatedRequestDto.toString());
            return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
        }
     
    @PostMapping("/validated/group1")
    public ResponseEntity<String> checkValidation(
    	@*****(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
        	LOGGER.info(validatedRequestDto.toString());
            return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
        }
        
    @PostMapping("/validated/group2")
    public ResponseEntity<String> checkValidation(
    	@*****(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(
    	@*****({ValidationGroup1.class, ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
        	LOGGER.info(validatedRequestDto.toString());
            return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
        }
}

답: Validated

 


출처: 스프링부트 핵심 가이드 10장~ 11장

SPRING #2

Editor : 농담곰

728x90

관련글 더보기