상세 컨텐츠

본문 제목

[스프링 3팀] 10장. 유효성 검사와 예외 처리

23-24/Spring 3

by recoday 2023. 12. 22. 22:16

본문

728x90

 

 

 

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

 

  1. 계층별로 진행하는 유효성 검사는 검사 로직이 각 클래스별로 분산되어 있어 관리가 어려움
  2. 검증 로직에 중복이 많아 여러 곳에 유사한 기능의 코드가 존재할 수 있음
  3. 만약 검증해야 할 값이 많다면 코드가 길어짐

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

위와 같은 문제 해결을 위해 'Bean Validation'이라는 데이터 유효성 검사 프레임워크를 제공하기 시작함

 

 

 

10.2 Hibernate Validator

Hibernate Validator란?

: Bean Validation 명세의 구현체

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

 

 

 

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

10.3.1 프로젝트 생성

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

프로젝트를 새로 생성하여 아래와 같이 프로젝트 구조를 만든다.

SwaggerConfiguration 클래스 파일을 생성한다.

 

 

10.3.2 스프링 부트용 유효성 검사 관련 의존성 추가

아래의 사진처럼 pom.xml파일에 유효성 검사 라이브러리를 의존성으로 추가하여 사용한다.

 

 

 

10.3.3 스프링 부트의 유효성 검사

 

유효성 검사는 데이터가 각 계층을 넘어오는 시점에 검사를 실시한다.

위의 사진처럼 유효성 검사를 DTO 객체를 대상으로 수행한다

->이는 스프링 부트에서 계층 간 데이터 전송에 대체로 DTO를 활용하고 있기 때문

 

-DTO 객체 생성(data/dto/ValidRequestDto.java)

@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)
    int age;
    @Size(min=0,max=40)
    String description;
    
    @Positive
    int count;
    
    @AssertTrue
    boolean booleanCheck;
    
}

 

 

-문자열 검증

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

 

-최댓값/최솟값 검증

  • BigDecimal, BigINteger, int, long 등의 타입을 지원함
  • @DemicalMax(value="$numberString"): $numberString보다 작은 값 허용
  • @DemicalMin(value="$numberString"): $numberString보다 큰 값 허용
  • @Min(value=$number): $number 이상의 값을 허용
  • @Max(value=$number): $number 이하의 값을 허용

 

-값의 범위 검증

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

 

-시간에 대한 검증

  • Date, LocalDate, LocalDateTime 등이 타입을 지원함
  • @Future: 현재보다 미래의 날짜를 허용
  • @FutureOrPresent: 현재를 포함한 미래의 날짜를 허용
  • @Past: 현재보다 과거의 날짜 허용
  • @PastOrPresent: 현재를 포함한 과거의 날짜 허용

-이메일 검증

  • @Email: 이메일 형식을 검사함. ""는 허용

 

-자릿수 범위 검증

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

 

-Boolean 검증

  • @AssertTrue: true인지 체크함. null값은 체크하지 않음
  • @AssertFalse: false인지 체크함. null값은 체크하지 않음

-문자열 길이 검증

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

 

-정규식 검증

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

 

 

-앞에서 생성한 DTO를 사용하는 컨트롤러 객체 생성(controller/ValidationController.java)

@Restcontroller
@RequestMapping("/validation")
public class ValidatonController {
    private final Logger LOGGER=LoggerFactory.getLogger(ValidatonController.class);
    
    @PostMapping("/valid")
    public ResponseEntity<String> checkValidationByValid(
            @Valid @RequestBody ValidRequestDto validRequestDto){
        LOGGER.info(validRequestDto.toString());
        return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
    }
}
  • checkValidationByValid() : ValidRequestDto 객체를 RequestBody 값으로 받음
  • @Valid 어노테이션을 지정해야 DTO 객체에 대해서 유효성 검사를 수행할 수 있음

 

Swagger 페이지에 접속하여 동작을 확인

아래와 같이 입력한다.

 

{

"age": 30,

"booleanCheck": true,

"description": "Validation 실습 데이터입니다.",

"email": "flature@wikibooks.co.kr",

"name": "Flature",

"phoneNumber": "010-1234-5678"

}

 

제대로 입력하여 호출하였다면 '200 OK'로 응답할 것이다.

만약 설정한 규칙에서 벗어나는 값을 호출했다면 아래의 사진처럼 400 에러가 발생한다.

 

 

아래의 사진처럼 애플리케이션의 로그에 로그가 출력되어 문제가 발생한 지점을 확인할 수 있다.

아직 예외 처리를 하지 않아 어디에서 에러가 발생했는지는 알 수 없다.

 

 

 

 

검사를 실패한 개수를 로그에 포함시키면 아래의 사진처럼 'with 2 errors' 라는 문구가 뜨도록 한다.

 

더보기

-정규식이란?

:특정한 규칙을 가진 문자열 집합을 표현하기 위해 쓰이는 형식

 

  • ^ : 문자열의 시작
  • $ : 문자열의 종료
  • . : 임의의 한 문자
  • * : 앞 문자가 없거나 무한정 많음
  • + : 앞 문자가 하나 이상
  • ? : 앞 문자가 없거나 하나 존재
  • [ , ] : 문자의 집합이나 범위를 나타냄ㅕ, 두 문자 사이는 - 기호로 범위를 표현
  • { ,  } : 횟수 또는 범위를 의미
  • ( ,  ) : 괄호 안의 문자를 하나의 문자로 인식
  • | : 패턴 안에서 OR 연산을 수행
  • \ : 정규식에서 역슬래시를 확장문자로 취급, 역슬래시 다음에 특수문자가 오면 문자로 인식
  • \b : 단어의 경계
  • \B : 단어가 아닌 것에 대한 경계
  • \A : 입력의 시작 부분
  • \G : 이전 매치의 끝
  • \Z : 종결자가 있는 경우 입력의 끝
  • \z : 입력의 끝
  • \s : 공백 문자
  • \S : 공백 문자가 아닌 나머지 문자(^\s와 동일)
  • \w : 알파벳이나 숫자
  • \W : 알파벳이나 숫자가 아닌 문자(^\w와 동일)
  • \d : 숫자 [0-9]와 동일하게 취급
  • \D : 숫자를 제외한 모든 문자(^0-9와 동일)

 

 

 

10.3.4 @Validated의 활용

  • @Validated는 스프링에서 지원하는 별도의 어노테이션으로 유효성 검사를 수행하는 어노테이션
  • @Validated는 @Valid 어노테이션 기능을 포함 -> @Valid를 @Validated로 변경할 수 있음
  • @Validated는 유효성 검사를 그룹으로 묶어서 대상을 특정할 수 있음

 

data 패키지 안에 group 패키지 생성 -> ValidationGroup1, ValidationGroup2 인터페이스를 생성

 

-ValidationGroup1

public interface ValidationGroup1 {
}

 

-ValidationGroup2

public interface ValidationGroup2 {
}

 

 

 

그 다음 data/dto/ValidatedRequestDto 경로가 되도록  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,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;
}
  • @Min, @Max의 groups 속성을 사용하여 ValidationGroup1과 ValidationGroup2 그룹을 설정 -> 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 지정

 

-controller/ValidationController.java 에 메서드 추가

@Restcontroller
@RequestMapping("/validation")
public class ValidatonController {
    private final Logger LOGGER=LoggerFactory.getLogger(ValidatonController.class);

    @PostMapping("/valided")
    public ResponseEntity<String> checkValidationByValid(
            @Validated @RequestBody ValidatedRequestDto validatedRequestDto){
        LOGGER.info(validatedRequestDto.toString());
        return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
    }

    @PostMapping("/validation/group1")
    public ResponseEntity<String> checkValidation1(
            @Validated(ValidationGroup1.class) @RequestBody ValidationRequestDto 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.toStirng());
    }

    @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());
    }
}

 

 

-Swagger 페이지에서 checkValidation() 메서드 호출

{

"age": -1,

"booleanCheck": true,

"count": -1,

"description": "Validation 실습 데이터입니다.",

"email": "flature@wikibooks.co.kr",

"name": "Flature",

"phoneNumber": "010-1234-5678"

}

 

-checkValidation1()를 호출하면 확인할 수 있는 로그

  • ValidationGroup1을 그룹으로 설정한 age에 대한 에러 발생
  • checkValidation2() 메서드를 호출하면 count에 대한 오류만 발생

 

 

-  마지막 메서드를 호출하면 다음과 같은 로그 출력

 

 

 

-호출 데이터 변경하여 메서드 호출

{

"age": 30,

"booleanCheck": false,

"count": 30,

"description": "Validation 실습 데이터입니다.",

"email": "flature@wikibooks.co.kr",

"name": "Flature",

"phoneNumber": "010-1234-5678"

}

 

  • age, count 검사는 통과하고 booleanCheck 변수에서 검사를 실패하는 데이터
  • @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우에는 groups가 설정되지 않은 필드에 대해 유효성 검사 수행
  • @Validated 어노테이션에 특정 그룹을 설정하는 경우, 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사 수행

 

 

 

10.3.5 커스텀 Validation 추가

※ 자바, 스프링의 유효성 검사 어노테이션에서 제공하지 않는 기능 써야할 때 -> ConstraintValidator와 커스텀 어노테이션을 조합하여 유효성 검사 어노테이션 생성 가능

 

-config/annotation/TelephoneValidator.java

public class TelephoneValidator 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})[.-]$");
    }
}
  • 전화번호 형식이 일치하는지 확인하는 간단한 유효성 검사 어노테이션 
  • TelephoneValidator 클래스를 ConstraintValidator 인터페이스의 구현체로 정의
  • ContraintValidator 인터페이스는 invalid() 메서드를 정의
  • false 리턴-> MethodArgumentNotValidException 예외가 발생

 

-config/annotation/Telephone.java

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=TelephoneValidator.class)
public class Telephone {
    String message() default "전화번호 형식이 일치하지 않습니다.";
    Class[] groups() default {};
    Class[] payload() default {};
}
  • @Target : 해당 어노테이션이 선언될 수 있는 위치 정의

-사용가능한 ElementType

  • ElementType.PACKAGE
  • ElementType.TYPE
  • ElementType.CONSTRUCTOR
  • ElementType.FIELD
  • ElementType.METHOD
  • ElementType.ANNOTATION_TYPE
  • ElementType.LOCAL_VARIABLE
  • ElementType.PARAMETER
  • ElementType.TYPE_PARAMETER
  • ElementType.TYPE_USE
  • @Retention : 해당 어노테이션이 실제로 적용, 유지되는 범위

-RetentionPolicy로 지정가능한 항목

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

 

  • @Constraint : TelephoneValidator 와 매핑하는 작업 수행

-각 요소들이 의미하는 것

  • message() : 유효성 검사를 실패할 경우 반환되는 메시지
  • groups() : 유효성 검사를 사용하는 그룹으로 설정
  • payload() : 사용자가 추가 정보를 위해 전달하는 값

 

인텔리제이 IDEA -> [Bean Validation] 탭에서 @Telephone 추가된 것 확인

 

 

 

-data/dto/ValidatedRequestDto.java 수정

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {

    @NotBlank
    private String name;

    @Email
    private String email;

    @Telephone
    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;
}
  • @Pattern 어노테이션을 @Telephone 어노테이션으로 변경

 

-Swagger 페이지

{

"age": 30,

"booleanCheck": false,

"count": 30,

"description": "Validation 실습 데이터입니다.",

"email": "flature@wikibooks.co.kr",

"name": "Flature",

"phoneNumber": "010-1234-5678"

}

 

  • 별도의 그룹을 지정하지 않아서 checkValidation()을 호출했을 때 오류 발생

 

 

 

 

 

10.4 예외 처리

10.4.1 예외와 에러

  • 예외(exception) : 애플리케이션이 정상적으로 동작하지 못하는 상황( 예. 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우)
  • 에러(error) : 자바의 가상머신에서 발생시키는 것 (예. 메모리 부족, 스택 오버플로 등)

10.4.2 예외 클래스

  • 예외 클래스는 상속 구조를 갖추고 있음

 

 

-Checked Exception과 Unchecked Exception 비교

  Checked Exception Unchecked Exception
처리 여부 반드시 예외 처리 필요 명시적 처리를 강제하지 않음
확인 시점 컴파일 단계 실행 중 단계
대표적인 예외 클래스 IOException
SQLException
RuntimeException
NullPointerException
IllegalArgumentException
IndexOutOfBoundException
SystemException

 

 

 

10.4.3 예외 처리 방법

  1. 예외 복구
  2. 예외 처리 회피
  3. 예외 전환

 

-예외 복구

  • 예외 상황을 파악하여 문제를 해결하는 방식
  • ex) try/catch 구문
int a=1;
String b="a"

try{
    System.out.println(a+Integer.parseInt(b));
 } catch (NumberFormatException e){
     b="2";
     System.out.println(a+Integer.parseInt(b));
 }

 

 

 

-예외 처리 회피

  • 예외가 발생한 시점이 아닌 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식
  • throw 키워드를 통해 어떤 예외가 발생했는지 내용 전달
int a=1;
String b="a"

try{
    System.out.println(a+Integer.parseInt(b));
 } catch (NumberFormatException e){
     throw new NumberFormatException("숫자가 아닙니다.");
 }

 

 

 

-예외 전환 방법

  • 예외  복구 방식과 예외 처리 회피 방식을 섞은 방식
  • 어떤 예외가 발생했는지에 따라 호출부로 예외 내용을 전달하면서 좀 더 적합한 예외 타입으로 전달
  • 예외 처리를 단순하게 하기 위해 래핑(wrapping)
  • try/catch 방식 사용 & catch 블록에서 throw 키워드를 사용해 다른 예외 타입으로 전달

 

 

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

 

-전달받은 예외를 처리하는 방식 

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

※@ControllerAdvice 대신 @RestControllerAdvice를 사용하면 결괏값을 JSON 형태로 반환할 수 있음

 

-common/exception/CustomExcptionHandler.java

@RestControllerAdvice
public class CustomException {
    
    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 내 handleException 호출, {}, {}", requeset.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, @ControllerAdvice : @Controller, @RestController 에서 발생하는 예외를 한 곳에서 관리, 처리할 수 있게 하는 기능을 수행하는 어노테이션
  • ex) @RestControllerAdvice(basePackages="com.springboot.valid_exception") -> 별도의 설정을 통해 예외를 관제하는 범위를 지정할 수 있음
  • @ExceptionHandler : @Controller, @RestController가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드를 정의하는 기능을 하는 어노테이션
  • value 속성은 배열의 형식으로도 전달받을 수 있어서 여러 예외 클래스 등록 가능

 

 

-controller/ExceptionController.java

@RestController
@RequestMapping("/exception")
public class ExceptionController {
    
    @GetMapping
    public void getRuntimeException(){
        throw new RuntimeException("getRuntimeException 메서드 호출");
    }
}
  • 예외를 발생시킬 수 있는 컨트롤러
  • getRuntimeException() : 컨트롤러로 요청이 들어오면 RuntimeException을 발생시킴

 

 

-Swagger 페이지

  • getRuntimeException 호출

  • 에러메시지가 Body 값에 담겨 돌아옴

 

 

-controller/ExceptionController.java

@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 어노테이션 사용한 메서드 선언-> 예외 처리를 해당 클래스에 국한시킴

 

-Swagger 페이지

  • '클래스 내 handlerException 호출' 메시지 확인 가능
  • @ControllerAdvice와 컨트롤러 내에 동일한 예외 타입을 처리한다면 좀 더 우선순위가 높은 클래스 내의 핸들러 메서드가 사용됨

 

-우선순위 비교 방법

1. 예외 타입 레벨에 따른 예외 처리 우선순위

  • 클래스 내에 동일하게 핸들러 메서드가 선언된 상태에서 좀 더 구체적인 클래스가 선언된 쪽이 우선순위를 가짐

 

 

2. 핸들러 위치에 따른 예외 처리 우선순위

 

  • 범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위를 가짐

 

 

 

10.4.5 커스텀 예외

커스텀 예외를 사용하는 이유는?

  • 네이밍에 개발자의 의도를 담을 수 있어 예외 상황을 짐작할 수 있음
  • 애플리케이션에서 발생하는 예외를 개발자가 직접 관리하기가 수월함
  • 예외 상황에 대한 처리가 용이함
  • ->@ControllerAdvice, @ExceptionHandler와 같은 어노테이션을 사용하면 애플리케이션에서 발생하는 예외 상황들을 한 곳에서 처리할 수 있음

 

10.4.6 커스텀 예외 클래스 생성하기

 

-Exception 클래스의 커스텀 예외

public class Exception extends Throwable {
    static final long serialVersionUID=-3387516993124229948L;
    
    public Exception(){
        super();
    }
    
    public Exception(String message, Throwable cause){
        super(message,cause);
    }
    
    public Exception(Throwable cause){
        super(cause);
    }
    
    protected Exception(String message, Throwable cause,
                        boolean enableSuppression,
                        boolean writableStackTrace){
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
  • Exception 클래스는 Throwable 클래스를 상속받음
  • message 변수 활용한 Exception 클래스의 커스텀 예외
  • String 타입의 메시지 문자열 -> Throwable 클래스의 생성자 호출

 

-Throwable 클래스

public class Throwable implements Serializable {
    
    private static final long serialVersionUID=-3042686055658047285L;
    
    private transient Object backtrace;
    
    private String detailMessage;
    
    //생략
    
    public Throwable(){
        fillInStackTrace();
    }
    
    public Throwable(String message){
        fillInStackTrace();
        detailMessage=message;
    }
    
    public String getMessage(){
        return detailMessage;
    }
    
    public String getLocalizedMessage(){
        return getMessage();
    }
    //생략
}

 

  • Exception 클래스는 부모 클래스인 Throwable 클래스의 생성자를 호출
  • message 변수 값을 detailMessage 변수로 전달받음

 

-HttpStatus 열거형 : 주요 코드의 일부

 

public enum HttpStatus{
    
    BAD_REQUEST(400,Series.CLIENT_ERROR, "Bad Request"),
    UNAUTHORIZED(401, Series.CLIENT_ERROE,"Unauthorized"),
    PAYMENT_REQUIRED(402, Series.CLIENT_ERROR,"Payment Requred"),
    FORBIDDEN(403, Series.CLIENT_ERROR, "Forbidden"),
    NOT_FOUND(404, Series.CLIENT_ERROE,"Not Found"),
    METHOD_NOT_ALLOWED(405, Series.CLIENT_ERROR,"Method Not Allowed"),
    
    HttpStatus(int value, Series series, String reasonPhrase){
        this.value=value;
        this.series=series;
        this.reasonPhrase=reasonPhrase;
    }
    
    public int value(){
        return this.value;
    }
    
    public Series series(){
        return this.series;
    }
    
    public String getReasonPhrase(){
        return this.reasonPhrase;
    }
}

 

 

 

-커스텀 예외 클래스를 생성할 때 필요한 내용들

  • 에러 타입(error type): HttpStatus의  reasonPhrase
  • 에러 코드(error code): HttpStatus의 value
  • 메시지(message): 상황별 상세 메시지

 

-common/Constants.java

public class Constants {
    
    public enum ExceptionClass{
        PRODUCT("Product");
        
        private String exceptionClass;
        
        ExceptionClass(String exceptionClass){
            this.exceptionClass=exceptionClass;
        }
        
        public String getExceptionClass(){
            return exceptionClass;
        }
        
        @Override
        public String toString(){
            return getExceptionClass()+"Exception.";
        }
    }
}
  • Constants 클래스  안에  ExceptionClass 생성 
  • ExceptionClass 열거형은 커스텀 예외 클래스에서 메시지 내부에 어떤 도메인에서 문제가 발생했는지 보여줌

 

 

-common/exception/CustomException.java

public class CustomException extends Exception {
    
    private Constants.ExceptionClass exceptionClass;
    private HttpStatus httpStatus;
    
    public CustomException(Constants.ExceptionClass exceptionClass, HttpStatus httpStatus,
                           String message){
        super(exceptionClass.toString()+message);
        this.exceptionClass=exceptionClass;
        this.httpStatus=httpStatus;
    }
    
    public Constants.ExceptionClass getExceptionClass(){
        return exceptionClass;
    }
    
    public int getHttpStatusCode(){
        return httpStatus.value();
    }
    
    public String getHttpStatusType(){
        return httpStatus.getReasonPhrase();
    }
    
    public HttpStatus getHttpStatus(){
        return httpStatus;
    }
}
  • 커스텀 예외 클래스는 ExceptionClass와 HttpStatus를 필드로 가짐

 

-common/exception/CustomExceptionHandler.java의  handleException() 메서드

@ExceptionHandler(value=CustomException.class)
    public ResponseEntity<Map<String, String>> handleException(CustomException e,
                                                               HttpServletRequest request){
        HttpHeaders responseHeaders=new HttpHeaders();
        LOGGER.error("Advice 내 handleException 호출, {}, {}",request.getRequestURI(),
                e.getMessage());
        
        Map<String, String> map=new HashMap<>();
        map.put("error type", e.getHttpStatusType());
        map.put("code", Integer.toString(e.getHttpStatusCode()));
        map.put("message", e.getMessage());
        
        return new ResponseEntity<>(map, responseHeaders, e.getHttpStatus());
    }
  • ExceptionHandler 클래스에 CustomException에 대한 예외 처리 코드를 추가
  • 예외 발생 시점에 HttpStatus를 정의하여 전달 -> 클라이언트 요청에 따라서 유동적인 응답코드를 설정할 수 있음

 

-controller/ExceptionController.java

 @GetMapping("/custom")
    public void getCustomException() throws CustomException{
        throw new CustomException(ExceptionClass.PRODUCT, HttpStatus.BAD_REQUEST,"getCustomException 메서드 호출");
    }
  • CustomException을 발생시키는 컨트롤러 메서드
  • CustomException을 throw 키워드로 던지면 커스텀 예외가 발생
  • HttpStatus를 통해 어떤 응답 코드를 사용할지와 세부 메시지를 전달

 

-Swagger 페이지

  • Response Body를 통해 예외 발생 지점에서 설정한 값이 담겨서 클라이언트로 응답 내용 출력

 

 

 

 

더보기

Quiz.

1.  기존의 유효성 검사의 문제점을 해결하기 위해 자바 진영에서 2009년부터 제공한 프레임워크의 이름은?

답: Bean Validation

 

2. 유효성 검사를 위한 조건을 설정하는 데에 사용되는 어노테이션 중 정규식을 검증하는 어노테이션은?

답: @Pattern

 

3. 유효성 검사를 지원하는 별도의 어노테이션으로 @Valid 어노테이션의 기능을 포함하는 어노테이션은?

답: @Validated

 

4. @Telephone 인터페이스 내부 요소들 중 유효성 검사를 사용하는 그룹으로 설정하는 역할을 하는 요소는?

답: groups()

 

5. 프로그래밍에서 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애프리케이션이 정상적으로 동작하지 못하는 상황을 의미하는 개념?

답: 예외(Exception)

 

6. Exception의 자식 클래스 중 반드시 예외에 대한 처리가 필요하며 컴파일 단계에서 확인이 가능한 예외 상황은?

답: Checked Exception

 

7. 예외 처리 방법 중 예외 처리를 회피할 때 사용하는 키워드로 어떤 예외가 발생했는지 호출부에 내용을 전달하는 것은?

답: throw

 

8. 다음은 유효성 검사 그룹을 설정하는 컨트롤러 클래스의 일부이다. 두 그룹을 모두 지정하도록 빈칸을 채우시오.

 @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. 해당 클래스에서만 국한하여 예외 처리를 하도록 아래의 빈 칸을 완성하시오

@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);
    }
}

 

 

 

 

 

[출처] 장정우, 스프링 부트 핵심 가이드』, 위키북스(2022), p.292-322.

 

 

ⓒ 구름

728x90

관련글 더보기