Skip to content

Commit f14f65a

Browse files
authored
Merge pull request #5 from prgrms-web-devcourse-final-project/feature/common-exception
전역 예외 처리 및 공통 예외 구조 구현
2 parents fbbf50b + ecf432b commit f14f65a

File tree

14 files changed

+365
-2
lines changed

14 files changed

+365
-2
lines changed

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ repositories {
2828

2929
dependencies {
3030
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
31-
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
32-
implementation("org.springframework.boot:spring-boot-starter-security")
31+
// implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
32+
// implementation("org.springframework.boot:spring-boot-starter-security")
3333
implementation("org.springframework.boot:spring-boot-starter-validation")
3434
implementation("org.springframework.boot:spring-boot-starter-web")
3535
implementation("org.springframework.boot:spring-boot-starter-data-redis")
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.example.log4u.common.advice;
2+
3+
import java.util.List;
4+
import java.util.stream.Collectors;
5+
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.http.HttpStatusCode;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.lang.NonNull;
10+
import org.springframework.validation.BindException;
11+
import org.springframework.web.bind.MethodArgumentNotValidException;
12+
import org.springframework.web.bind.annotation.ExceptionHandler;
13+
import org.springframework.web.bind.annotation.RestControllerAdvice;
14+
import org.springframework.web.context.request.ServletWebRequest;
15+
import org.springframework.web.context.request.WebRequest;
16+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
17+
18+
import com.example.log4u.common.exception.ApiErrorResponse;
19+
import com.example.log4u.common.exception.CommonErrorCode;
20+
import com.example.log4u.common.exception.base.ErrorCode;
21+
import com.example.log4u.common.exception.base.ServiceException;
22+
23+
import jakarta.servlet.http.HttpServletRequest;
24+
import lombok.extern.slf4j.Slf4j;
25+
26+
@RestControllerAdvice
27+
@Slf4j
28+
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
29+
30+
@Override
31+
public ResponseEntity<Object> handleMethodArgumentNotValid(
32+
MethodArgumentNotValidException e,
33+
@NonNull HttpHeaders headers,
34+
@NonNull HttpStatusCode status,
35+
@NonNull WebRequest request) {
36+
HttpServletRequest servletRequest = ((ServletWebRequest)request).getRequest();
37+
38+
String requestUrl = servletRequest.getRequestURI();
39+
String httpMethod = servletRequest.getMethod();
40+
List<String> errors = e.getBindingResult()
41+
.getFieldErrors()
42+
.stream()
43+
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
44+
.collect(Collectors.toList());
45+
46+
log.warn("Validation failed for request to {} {}. Errors: {}",
47+
httpMethod, requestUrl, errors);
48+
CommonErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
49+
return handleExceptionInternal(e, errorCode);
50+
}
51+
52+
@ExceptionHandler(IllegalArgumentException.class)
53+
public ResponseEntity<ApiErrorResponse> handleIllegalArgument(IllegalArgumentException e) {
54+
String location = getExceptionLocation(e);
55+
56+
log.warn("Illegal argument encountered at {}: {}", location, e.getMessage());
57+
58+
CommonErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
59+
return handleExceptionInternal(errorCode);
60+
}
61+
62+
@ExceptionHandler(ServiceException.class)
63+
public ResponseEntity<ApiErrorResponse> handleGiveMeTiConException(ServiceException e) {
64+
String location = getExceptionLocation(e);
65+
log.warn("Error invoke in our app at {}: {} ErrorCode: {}", location, e.getMessage(), e.getErrorCode());
66+
ErrorCode errorCode = e.getErrorCode();
67+
return handleExceptionInternal(errorCode);
68+
}
69+
70+
@ExceptionHandler({Exception.class})
71+
public ResponseEntity<ApiErrorResponse> handleAllException(Exception e) {
72+
String location = getExceptionLocation(e);
73+
log.warn("Unhandled exception occurred at {}: {}", location, e.getMessage());
74+
75+
CommonErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;
76+
return handleExceptionInternal(errorCode);
77+
}
78+
79+
private ResponseEntity<ApiErrorResponse> handleExceptionInternal(ErrorCode errorCode) {
80+
return ResponseEntity.status(errorCode.getHttpStatus())
81+
.body(makeErrorResponse(errorCode));
82+
}
83+
84+
private ApiErrorResponse makeErrorResponse(ErrorCode errorCode) {
85+
return ApiErrorResponse.builder()
86+
.errorMessage(errorCode.getErrorMessage())
87+
.errorCode(errorCode.getHttpStatus().value())
88+
.build();
89+
}
90+
91+
private ResponseEntity<Object> handleExceptionInternal(BindException e, ErrorCode errorCode) {
92+
return ResponseEntity.status(errorCode.getHttpStatus())
93+
.body(makeErrorResponse(e, errorCode));
94+
}
95+
96+
private ApiErrorResponse makeErrorResponse(BindException e, ErrorCode errorCode) {
97+
List<ApiErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
98+
.getFieldErrors()
99+
.stream()
100+
.map(ApiErrorResponse.ValidationError::of)
101+
.collect(Collectors.toList());
102+
103+
return ApiErrorResponse.builder()
104+
.errorMessage(errorCode.getErrorMessage())
105+
.errorCode(errorCode.getHttpStatus().value())
106+
.errors(validationErrorList)
107+
.build();
108+
}
109+
110+
private String getExceptionLocation(Exception e) {
111+
StackTraceElement element = e.getStackTrace()[0];
112+
return element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber();
113+
}
114+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.example.log4u.common.exception;
2+
3+
import java.util.List;
4+
5+
import org.springframework.validation.FieldError;
6+
7+
import com.fasterxml.jackson.annotation.JsonInclude;
8+
9+
import lombok.Builder;
10+
import lombok.Getter;
11+
import lombok.RequiredArgsConstructor;
12+
13+
@Getter
14+
@Builder
15+
@RequiredArgsConstructor
16+
public class ApiErrorResponse {
17+
private final String errorMessage;
18+
private final int errorCode;
19+
20+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
21+
private final List<ValidationError> errors;
22+
23+
public record ValidationError(String field, String message) {
24+
25+
public static ValidationError of(final FieldError fieldError) {
26+
return new ValidationError(fieldError.getField(), fieldError.getDefaultMessage());
27+
}
28+
}
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.example.log4u.common.exception;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
import com.example.log4u.common.exception.base.ErrorCode;
6+
7+
import lombok.Getter;
8+
import lombok.RequiredArgsConstructor;
9+
10+
@Getter
11+
@RequiredArgsConstructor
12+
public enum CommonErrorCode implements ErrorCode {
13+
14+
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"),
15+
UNAUTHENTICATED(HttpStatus.UNAUTHORIZED,"로그인이 필요한 기능입니다."),
16+
FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다"),
17+
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "요청 정보를 찾을 수 없습니다"),
18+
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다. 관리자에게 문의하세요.")
19+
;
20+
21+
private final HttpStatus httpStatus;
22+
private final String message;
23+
24+
@Override
25+
public HttpStatus getHttpStatus() {
26+
return this.httpStatus;
27+
}
28+
29+
@Override
30+
public String getErrorMessage() {
31+
return this.message;
32+
}
33+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.log4u.common.exception.base;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
public interface ErrorCode {
6+
String name();
7+
HttpStatus getHttpStatus();
8+
String getErrorMessage();
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.example.log4u.common.exception.base;
2+
3+
4+
import lombok.Getter;
5+
6+
@Getter
7+
public class ServiceException extends RuntimeException {
8+
9+
private final ErrorCode errorCode;
10+
11+
public ServiceException(ErrorCode errorCode) {
12+
super(errorCode.getErrorMessage());
13+
this.errorCode = errorCode;
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.log4u.common.external;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.client.RestTemplate;
6+
7+
import com.example.log4u.common.external.hanlder.ApiResponseErrorHandler;
8+
9+
@Configuration
10+
public class ClientConfig {
11+
12+
@Bean
13+
public RestTemplate restTemplate() {
14+
RestTemplate restTemplate = new RestTemplate();
15+
restTemplate.setErrorHandler(new ApiResponseErrorHandler());
16+
return restTemplate;
17+
}
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.example.log4u.common.external.exception;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Getter
7+
@RequiredArgsConstructor
8+
public class ExternalApiRequestException extends RuntimeException{
9+
10+
private final String statusCode;
11+
private final String message;
12+
13+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.example.log4u.common.external.hanlder;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.util.stream.Collectors;
7+
8+
import org.springframework.http.client.ClientHttpResponse;
9+
import org.springframework.web.client.ResponseErrorHandler;
10+
11+
import com.example.log4u.common.external.exception.ExternalApiRequestException;
12+
13+
import lombok.extern.slf4j.Slf4j;
14+
15+
@Slf4j
16+
public class ApiResponseErrorHandler implements ResponseErrorHandler {
17+
@Override
18+
public boolean hasError(ClientHttpResponse response) throws IOException {
19+
return !response.getStatusCode().is2xxSuccessful();
20+
}
21+
22+
@Override
23+
public void handleError(ClientHttpResponse response) throws IOException {
24+
String body;
25+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody()))) {
26+
body = reader.lines().collect(Collectors.joining("\n"));
27+
}
28+
29+
log.error("API 호출 중 에러 발생: HTTP 상태 코드: {}, 응답 본문: {}", response.getStatusCode().value(), body);
30+
31+
throw new ExternalApiRequestException(response.getStatusCode().toString(),
32+
"API 호출 중 에러 발생: " + response.getStatusCode().value() + " 응답 본문: " + body);
33+
}
34+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.example.log4u.domain.comment.exception;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
import com.example.log4u.common.exception.base.ErrorCode;
6+
7+
import lombok.Getter;
8+
import lombok.RequiredArgsConstructor;
9+
10+
@Getter
11+
@RequiredArgsConstructor
12+
public enum CommentErrorCode implements ErrorCode {
13+
14+
NOT_FOUND_COMMENT(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");
15+
16+
private final HttpStatus httpStatus;
17+
private final String message;
18+
19+
@Override
20+
public HttpStatus getHttpStatus() {
21+
return httpStatus;
22+
}
23+
24+
@Override
25+
public String getErrorMessage() {
26+
return message;
27+
}
28+
}

0 commit comments

Comments
 (0)