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 설정
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 테스트
1. test 용 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 |
의도한 Response가 전달된 것을 확인 가능
'Develop > SpringBoot' 카테고리의 다른 글
[Springboot] 외부 라이브러리 jar 추가하기 (2) | 2024.11.17 |
---|---|
[Springboot] Google calendar API 이용해서 공휴일 데이터 받기 (0) | 2023.12.17 |