Develop/SpringBoot

[Springboot] Custom exception 설정하기

chea-young

Springboot 개발 시, 프런트앤드로 보내주는 예외 상황에 대해서 관리하고, 예외 발생 시, 전달해주는 데이터를 통일해주기 위해 Custom exception 설정을 사용한다. 

본문 글에서는 ErrorCode를 string으로 지정하였지만 편의에 따라 int로 지정해도 문제없다.


Springboot 버전
- Java 17
- Springboot 3.2.0

 

Gradle 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
 
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
 
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
 
    runtimeOnly 'com.h2database:h2'
 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testImplementation 'org.springframework.security:spring-security-test'
}
 
 


개발 순서
1. ErrorCode enum 생성 및 설정
2. custom exception 생성 및 설정
3. custom handler 생성 및 설정
4. springConfig 설정

5. exception 테스트

 

개발 폴더 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── common
│   ├── dto
│   │   └── ErrorDTO.java
│   ├── enumType
│   │   └── ErrorCode.java
│   ├── exception
│   │   └── CustomException.java
│   └── handler
│       └── CustomExceptionHandler.java
├── config
│   └── SecurityConfig.java
└── develop
    ├── controller
    │   └── DevelopController.java
 
cs

 


 

1. ErrorCode enum 생성 및 설정

springboot 에서 에러를 발생시켰을 떄, 전달할 에러 코드를 관리하는 enum 생성.

프런트앤드에서는 errorCode를 보고, 어떤 action을 할 지 케이스를 나눌 수 있음.

 - 생성 위치: package com.lchy.develop.common.enumType

- 코드 설명

  - code: 프런트앤드로 전달되는 데이터로, 전달받은 코드를 통해 해당 에러를 받았을 때, 어떠한 조치를 취해야할 지 구현하는 것을 도움

  - msg: 전달 받은 에러 코드에 대한 설명으로, 해당 에러가 어떠한 상황에 발생한 에러인지 알 수 있도록 도움

  - UNKNOWN("000_UNKNOWN", "알 수 없는 에러가 발생했습니다."): 의도하지 않은 에러 발생한 상황 시, 추후 logging 시스템에서 확인 후, 코드 수정을 돕기 위해 생성한 에러 코드

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
import lombok.Getter;
import lombok.RequiredArgsConstructor;
 
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
    // 400
    UNKNOWN("000_UNKNOWN""알 수 없는 에러가 발생했습니다."),
    ENCRYPTION_FAILED("001_ENCRYPTION_FAILED""암호화에 실패하셨습니다."),
    DECRYPTION_FAILED("002_DECRYPTION_FAILED""복호화에 실패하셨습니다."),
    DUPLICATED_EMAIL("003_DUPLICATED_EMAIL""이미 등록되어 있는 이메일입니다."),
    REGISTERED_EMAIL_FOR_THE_OTHER("004_REGISTERED_EMAIL_FOR_THE_OTHER""다른 서비스로 등록되어 있는 이메일입니다."),
    INVALID_PASSWORD("005_INVALID_PASSWORD""유효하지 않은 비밀번호 입니다."),
    NOT_EXISTED_EMAIL("006_NOT_EXISTED_EMAIL""존재하지 않는 회원입니다."),
    BLOCKED_EMAIL("007_BLOCKED_EMAIL""차단된 사용자 입니다."),
    WRONG_PASSWORD("008_WRONG_PASSWORD""틀린 비밀번호 입니다."),
    NOT_ALLOW_EMAIL("009_NOT_ALLOW_EMAIL""이메일 사용이 허용이 되지 않은 사용자입니다."),
 
    // 401
    INVALID_TOKEN("101_INVALID_TOKEN""유효하지 않은 토큰입니다."),
    EXPIRED_TOKEN("102_EXPIRED_TOKEN""만료된 토큰입니다."),
 
    // 403
    ACESS_DENIED_EMAIL("301_ACESS_DENIED_EMAIL""접근 권한이 없는 사용자 요청입니다.");
 
    private final String code;
    private final String msg;
}
cs
 

 


2. Custom Exception 생성 및 설정

exception을 예외를 전달할 때, 상세한 정보를 전달하기 위한 Exception을 생성

 

- 생성 위치: package com.lchy.develop.common.exception

- 코드 설명

  - private final HttpStatus status: 원하는 HttpStatus로 에러 전달하기 위한 데이터

  - private final ErrorCode errorCode: 발생한 에러 상황에 대해서 프런트로 코드와 하여 전달하기 위한 데이터

  - private final String detail: 만약, 명세화하지 않은 에러가 발생한 경우, 발생한 에러에 대한 원인을 전달하기 위한 데이터

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.compono.ibackend.common.exception;
 
import com.compono.ibackend.common.enumType.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
 
@Getter
public class CustomException extends RuntimeException {
    private final HttpStatus status;
    private final ErrorCode errorCode;
    private final String detail;
 
    public CustomException(HttpStatus status, ErrorCode errorCode) {
        this.status = status;
        this.errorCode = errorCode;
        this.detail = "";
    }
 
    public CustomException(HttpStatus status, ErrorCode errorCode, String detail) {
        this.status = status;
        this.errorCode = errorCode;
        this.detail = detail;
    }
 
    public CustomException(HttpStatus status, ErrorCode errorCode, Throwable cause) {
        this.status = status;
        this.errorCode = errorCode;
        this.detail = cause.getMessage();
    }
 
    public CustomException(HttpStatus status, CustomException customException) {
        this.status = status;
        this.errorCode = customException.getErrorCode();
        this.detail = customException.getDetail();
    }
 
    public CustomException(HttpStatus status, Throwable cause) {
        this.status = status;
        this.errorCode = ErrorCode.UNKNOWN;
        this.detail = cause.getMessage();
    }
 
    public CustomException(Exception exception) {
        if (exception.getClass() == CustomException.class) {
            CustomException customException = (CustomException) exception;
            this.status = customException.getStatus();
            this.errorCode = customException.getErrorCode();
            this.detail = customException.getMessage();
        } else {
            this.status = HttpStatus.BAD_REQUEST;
            this.errorCode = ErrorCode.UNKNOWN;
            this.detail = exception.getMessage();
        }
    }
}
 
cs

 


3. Custom handler 생성 및 설정

CutomException 발생 시, 프런트로 보낼 ErrorDTO 를 생성하고, ErrorDTO를 전달하는 handler를 생성

1.Custom handler 생성
- 생성 위치: package com.lchy.develop.common.handler

- 코드 설명

  - CustomException 생성 시, ErrorDTO에 구현된 데이터 구조로 프런트앤드로 데이터가 보내지게 됨.

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.compono.ibackend.common.exception.CustomException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
 
@ControllerAdvice
public class CustomExceptionHandler {
 
    @ExceptionHandler(CustomException.class)
    protected ResponseEntity<ErrorDTO> handleCustom400Exception(CustomException ex) {
        return ErrorDTO.toResponseEntity(ex);
    }
}
s

 

2.ErrorDTO 생성
- 생성 위치: package com.lchy.develop.common.dto

- 코드 설명

  - CustomException에 저장된 HttpStatus와 ErrorCode를 통해 ErrorDTO를 생성하여 하나의 파일로 여러가지 HttpStatus와 ErrorCode에 대한 상황을 관리할 수 있도록 함.

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
import lombok.Builder;
import lombok.Data;
import org.springframework.http.ResponseEntity;
 
@Data
@Builder
public class ErrorDTO {
 
    private String code;
    private String msg;
    private String detail;
 
    public static ResponseEntity<ErrorDTO> toResponseEntity(CustomException ex) {
        ErrorCode errorType = ex.getErrorCode();
        String detail = ex.getDetail();
 
        return ResponseEntity
            .status(ex.getStatus())
            .body(ErrorDTO.builder()
                .code(errorType.getCode())
                .msg(errorType.getMsg())
                .detail(detail)
                .build());
    }
}
cs

4.  springConfig 설정

Spring security가 설정되어 있는 경우, Custom exception은 모두 "/error/**" 라우팅되어 프런트앤드로 전달되기 때문에 "/error/**" path 허용 설정이 필요.
- 생성 위치: package com.lchy.develop.config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
 
    private static final String[] DEFAULT_WHITELIST = {
            "/status""/images/**""/error/**"
    };
 
    private static final String[] DEVELOP_TEST_PATH = {
            "api/develop/**",
    };
 
    @Bean
    protected SecurityFilterChain config(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(request -> request
                .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
                .requestMatchers(DEFAULT_WHITELIST).permitAll()
                .requestMatchers(DEVELOP_TEST_PATH).permitAll()
                .anyRequest().authenticated()
        );
        return http.build();
    }
}
s

 

5.  exception 테스트

설정한 Custom exception이 의도한 대로 exception 처리를 하는지 확인
 

1. test 용 controller 생성

- 생성 위치: package com.lchy.develop.develop.controller
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
31
32
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequiredArgsConstructor
@RequestMapping("api/develop/")
public class DevelopController {
 
    @GetMapping("v1/bad-request")
    public ResponseEntity<Object> test400Error() {
        try{
            throw new CustomException(HttpStatus.BAD_REQUEST, ErrorCode.INVALID_PASSWORD);
        }
        catch(Exception ex) {
            throw new CustomException(ex);
        }
    }
 
    @GetMapping("v1/unauthorized")
    public ResponseEntity<Object> test401Error() {
        try{
            throw new CustomException(HttpStatus.UNAUTHORIZED, ErrorCode.EXPIRED_TOKEN);
        }
        catch(Exception ex) {
            throw new CustomException(ex);
        }
    }
}
cs

 

2. Postman 테스트

의도한 Response가 전달된 것을 확인 가능