Skip to content

Commit

Permalink
Merge pull request #9 from taco-official/KL-64/global-response-세팅
Browse files Browse the repository at this point in the history
feat(KL-64): Global Response 세팅
  • Loading branch information
min3m authored Jul 19, 2024
2 parents 1b0e3dd + e65b6cd commit d7df44d
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/main/java/taco/klkl/global/error/ErrorResponse.java
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 src/main/java/taco/klkl/global/error/GlobalExceptionHandler.java
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);
}
}
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 src/main/java/taco/klkl/global/error/exception/ErrorCode.java
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 src/main/java/taco/klkl/global/response/GlobalResponse.java
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 src/main/java/taco/klkl/global/response/GlobalResponseAdvice.java
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 src/test/java/taco/klkl/global/error/GlobalExceptionHandlerTest.java
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());
}
}
Loading

0 comments on commit d7df44d

Please sign in to comment.