-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from taco-official/KL-64/global-response-세팅
feat(KL-64): Global Response 세팅
- Loading branch information
Showing
8 changed files
with
451 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package taco.klkl.global.error; | ||
|
||
public record ErrorResponse(String code, String message) { | ||
public static ErrorResponse of(String code, String message) { | ||
return new ErrorResponse(code, message); | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
src/main/java/taco/klkl/global/error/GlobalExceptionHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package taco.klkl.global.error; | ||
|
||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatusCode; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.HttpRequestMethodNotSupportedException; | ||
import org.springframework.web.bind.MethodArgumentNotValidException; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
import org.springframework.web.context.request.WebRequest; | ||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import taco.klkl.global.error.exception.CustomException; | ||
import taco.klkl.global.error.exception.ErrorCode; | ||
import taco.klkl.global.response.GlobalResponse; | ||
|
||
@Slf4j | ||
@RestControllerAdvice | ||
@RequiredArgsConstructor | ||
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { | ||
|
||
@Override | ||
protected ResponseEntity<Object> handleMethodArgumentNotValid( | ||
MethodArgumentNotValidException ex, | ||
HttpHeaders headers, | ||
HttpStatusCode statusCode, | ||
WebRequest request | ||
) { | ||
log.error("MethodArgumentNotValid : {}", ex.getMessage(), ex); | ||
final ErrorCode errorCode = ErrorCode.METHOD_ARGUMENT_INVALID; | ||
final ErrorResponse errorResponse = ErrorResponse.of(errorCode.getCode(), errorCode.getMessage()); | ||
final GlobalResponse globalResponse = GlobalResponse.error(errorCode.getCode(), errorResponse); | ||
return ResponseEntity.status(errorCode.getStatus()).body(globalResponse); | ||
} | ||
|
||
@Override | ||
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported( | ||
HttpRequestMethodNotSupportedException ex, | ||
HttpHeaders headers, | ||
HttpStatusCode status, | ||
WebRequest request | ||
) { | ||
log.error("HttpRequestMethodNotSupported : {}", ex.getMessage(), ex); | ||
final ErrorCode errorCode = ErrorCode.METHOD_NOT_ALLOWED; | ||
final ErrorResponse errorResponse = ErrorResponse.of(errorCode.getCode(), errorCode.getMessage()); | ||
final GlobalResponse globalResponse = GlobalResponse.error(errorCode.getCode(), errorResponse); | ||
return ResponseEntity.status(errorCode.getStatus()).body(globalResponse); | ||
} | ||
|
||
@Override | ||
protected ResponseEntity<Object> handleExceptionInternal( | ||
Exception ex, | ||
Object body, | ||
HttpHeaders headers, | ||
HttpStatusCode statusCode, | ||
WebRequest request | ||
) { | ||
log.error("ExceptionInternal : {}", ex.getMessage(), ex); | ||
final ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; | ||
final ErrorResponse errorResponse = ErrorResponse.of(errorCode.getCode(), errorCode.getMessage()); | ||
final GlobalResponse globalResponse = GlobalResponse.error(errorCode.getCode(), errorResponse); | ||
return ResponseEntity.status(errorCode.getStatus()).body(globalResponse); | ||
} | ||
|
||
@ExceptionHandler(CustomException.class) | ||
public ResponseEntity<Object> handleCustomException(CustomException ex) { | ||
log.error("CustomException : {}", ex.getMessage(), ex); | ||
final ErrorCode errorCode = ex.getErrorCode(); | ||
final ErrorResponse errorResponse = ErrorResponse.of(errorCode.getCode(), errorCode.getMessage()); | ||
final GlobalResponse globalResponse = GlobalResponse.error(errorCode.getCode(), errorResponse); | ||
return ResponseEntity.status(errorCode.getStatus()).body(globalResponse); | ||
} | ||
|
||
@ExceptionHandler(Exception.class) | ||
public ResponseEntity<Object> handleException(Exception ex) { | ||
log.error("InternalServerError : {}", ex.getMessage(), ex); | ||
final ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; | ||
final ErrorResponse errorResponse = ErrorResponse.of(errorCode.getCode(), errorCode.getMessage()); | ||
final GlobalResponse globalResponse = GlobalResponse.error(errorCode.getCode(), errorResponse); | ||
return ResponseEntity.status(errorCode.getStatus()).body(globalResponse); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/taco/klkl/global/error/exception/CustomException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package taco.klkl.global.error.exception; | ||
|
||
import lombok.Getter; | ||
|
||
@Getter | ||
public class CustomException extends RuntimeException { | ||
private final ErrorCode errorCode; | ||
|
||
public CustomException(ErrorCode errorCode) { | ||
super(errorCode.getMessage()); | ||
this.errorCode = errorCode; | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
src/main/java/taco/klkl/global/error/exception/ErrorCode.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package taco.klkl.global.error.exception; | ||
|
||
import org.springframework.http.HttpStatus; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum ErrorCode { | ||
// Common | ||
METHOD_ARGUMENT_INVALID(HttpStatus.BAD_REQUEST, "C010", "유효하지 않은 method 인자 입니다."), | ||
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C011", "지원하지 않는 HTTP method 입니다."), | ||
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C012", "서버에 문제가 발생했습니다. 관리자에게 문의해주세요."), | ||
|
||
// User | ||
|
||
// Product | ||
|
||
// Like | ||
|
||
// Comment | ||
|
||
// Region | ||
|
||
// Category | ||
|
||
// Filter | ||
|
||
// Notification | ||
|
||
// Search | ||
|
||
// Sample | ||
SAMPLE_ERROR(HttpStatus.BAD_REQUEST, "C999", "샘플 에러입니다."), | ||
; | ||
|
||
private final HttpStatus status; | ||
private final String code; | ||
private final String message; | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/taco/klkl/global/response/GlobalResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package taco.klkl.global.response; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
import taco.klkl.global.error.ErrorResponse; | ||
|
||
public record GlobalResponse(boolean isSuccess, String code, Object data, LocalDateTime timestamp) { | ||
public static GlobalResponse ok(String code, Object data) { | ||
return new GlobalResponse(true, code, data, LocalDateTime.now()); | ||
} | ||
|
||
public static GlobalResponse error(String code, ErrorResponse errorResponse) { | ||
return new GlobalResponse(false, code, errorResponse, LocalDateTime.now()); | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
src/main/java/taco/klkl/global/response/GlobalResponseAdvice.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package taco.klkl.global.response; | ||
|
||
import org.springframework.core.MethodParameter; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.converter.HttpMessageConverter; | ||
import org.springframework.http.server.ServerHttpRequest; | ||
import org.springframework.http.server.ServerHttpResponse; | ||
import org.springframework.http.server.ServletServerHttpResponse; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; | ||
|
||
import jakarta.servlet.http.HttpServletResponse; | ||
|
||
@RestControllerAdvice(basePackages = "taco.klkl") | ||
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> { | ||
@Override | ||
public boolean supports( | ||
MethodParameter returnType, | ||
Class<? extends HttpMessageConverter<?>> converterType | ||
) { | ||
return true; | ||
} | ||
|
||
@Override | ||
public Object beforeBodyWrite( | ||
Object body, | ||
MethodParameter returnType, | ||
MediaType selectedContentType, | ||
Class<? extends HttpMessageConverter<?>> selectedConverterType, | ||
ServerHttpRequest request, | ||
ServerHttpResponse response | ||
) { | ||
ServletServerHttpResponse servletServerHttpResponse = (ServletServerHttpResponse)response; | ||
HttpServletResponse httpServletResponse = servletServerHttpResponse.getServletResponse(); | ||
int status = httpServletResponse.getStatus(); | ||
HttpStatus resolve = HttpStatus.resolve(status); | ||
|
||
if (resolve == null || body instanceof String) { | ||
return body; | ||
} | ||
if (resolve.is2xxSuccessful()) { | ||
return GlobalResponse.ok("C000", body); | ||
} | ||
return body; | ||
} | ||
} |
139 changes: 139 additions & 0 deletions
139
src/test/java/taco/klkl/global/error/GlobalExceptionHandlerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package taco.klkl.global.error; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
import static org.mockito.Mockito.*; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.HttpRequestMethodNotSupportedException; | ||
import org.springframework.web.bind.MethodArgumentNotValidException; | ||
import org.springframework.web.context.request.WebRequest; | ||
|
||
import taco.klkl.global.error.exception.CustomException; | ||
import taco.klkl.global.error.exception.ErrorCode; | ||
import taco.klkl.global.response.GlobalResponse; | ||
|
||
class GlobalExceptionHandlerTest { | ||
|
||
private GlobalExceptionHandler globalExceptionHandler; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
globalExceptionHandler = new GlobalExceptionHandler(); | ||
} | ||
|
||
@Test | ||
@DisplayName("MethodArgumentNotValidException이 발생한 경우") | ||
void methodArgumentNotValidOccurred() { | ||
// given | ||
MethodArgumentNotValidException exception = mock(MethodArgumentNotValidException.class); | ||
HttpHeaders headers = new HttpHeaders(); | ||
HttpStatus status = HttpStatus.BAD_REQUEST; | ||
WebRequest request = mock(WebRequest.class); | ||
|
||
// when | ||
ResponseEntity<Object> response = globalExceptionHandler.handleMethodArgumentNotValid( | ||
exception, headers, status, request); | ||
|
||
// then | ||
assertNotNull(response); | ||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); | ||
assertInstanceOf(GlobalResponse.class, response.getBody()); | ||
GlobalResponse globalResponse = (GlobalResponse)(response.getBody()); | ||
assertInstanceOf(ErrorResponse.class, globalResponse.data()); | ||
ErrorResponse errorResponse = (ErrorResponse)(globalResponse.data()); | ||
assertEquals("C010", errorResponse.code()); | ||
assertEquals("유효하지 않은 method 인자 입니다.", errorResponse.message()); | ||
} | ||
|
||
@Test | ||
@DisplayName("HttpRequestMethodNotSupported가 발생한 경우") | ||
void httpRequestMethodNotSupportedOccurred() { | ||
// given | ||
HttpRequestMethodNotSupportedException exception = mock(HttpRequestMethodNotSupportedException.class); | ||
HttpHeaders headers = new HttpHeaders(); | ||
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; | ||
WebRequest request = mock(WebRequest.class); | ||
|
||
// when | ||
ResponseEntity<Object> response = globalExceptionHandler.handleHttpRequestMethodNotSupported( | ||
exception, headers, status, request); | ||
|
||
// then | ||
assertNotNull(response); | ||
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode()); | ||
assertInstanceOf(GlobalResponse.class, response.getBody()); | ||
GlobalResponse globalResponse = (GlobalResponse)(response.getBody()); | ||
assertInstanceOf(ErrorResponse.class, globalResponse.data()); | ||
ErrorResponse errorResponse = (ErrorResponse)(globalResponse.data()); | ||
assertEquals("C011", errorResponse.code()); | ||
assertEquals("지원하지 않는 HTTP method 입니다.", errorResponse.message()); | ||
} | ||
|
||
@Test | ||
@DisplayName("ExceptionInternal이 발생한 경우") | ||
void ExceptionInternalOccurred() { | ||
// given | ||
Exception exception = new RuntimeException("Test exception"); | ||
HttpHeaders headers = new HttpHeaders(); | ||
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; | ||
WebRequest request = mock(WebRequest.class); | ||
|
||
// when | ||
ResponseEntity<Object> response = globalExceptionHandler.handleExceptionInternal( | ||
exception, null, headers, status, request); | ||
|
||
// then | ||
assertNotNull(response); | ||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); | ||
assertInstanceOf(GlobalResponse.class, response.getBody()); | ||
GlobalResponse globalResponse = (GlobalResponse)(response.getBody()); | ||
assertInstanceOf(ErrorResponse.class, globalResponse.data()); | ||
ErrorResponse errorResponse = (ErrorResponse)(globalResponse.data()); | ||
assertEquals("C012", errorResponse.code()); | ||
assertEquals("서버에 문제가 발생했습니다. 관리자에게 문의해주세요.", errorResponse.message()); | ||
} | ||
|
||
@Test | ||
@DisplayName("CustomException이 발생한 경우") | ||
void customExceptionOccurred() { | ||
// given | ||
CustomException exception = new CustomException(ErrorCode.SAMPLE_ERROR); | ||
|
||
// when | ||
ResponseEntity<Object> response = globalExceptionHandler.handleCustomException(exception); | ||
|
||
// then | ||
assertNotNull(response); | ||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); | ||
assertInstanceOf(GlobalResponse.class, response.getBody()); | ||
GlobalResponse globalResponse = (GlobalResponse)(response.getBody()); | ||
assertInstanceOf(ErrorResponse.class, globalResponse.data()); | ||
ErrorResponse errorResponse = (ErrorResponse)(globalResponse.data()); | ||
assertEquals("C999", errorResponse.code()); | ||
assertEquals("샘플 에러입니다.", errorResponse.message()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Exception이 발생한 경우") | ||
void handleException() { | ||
// given | ||
Exception exception = new RuntimeException("Unexpected exception"); | ||
|
||
// when | ||
ResponseEntity<Object> response = globalExceptionHandler.handleException(exception); | ||
|
||
// then | ||
assertNotNull(response); | ||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); | ||
assertNotNull(response.getBody()); | ||
GlobalResponse globalResponse = (GlobalResponse)(response.getBody()); | ||
ErrorResponse errorResponse = (ErrorResponse)(globalResponse.data()); | ||
assertEquals("C012", errorResponse.code()); | ||
assertEquals("서버에 문제가 발생했습니다. 관리자에게 문의해주세요.", errorResponse.message()); | ||
} | ||
} |
Oops, something went wrong.