Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] 공통으로 사용될 예외 처리 및 응답 생성 #5

Open
wants to merge 4 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/docs/asciidoc/common/apiResponse.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
== Successful Response

include::{snippets}/common/success-response/curl-request.adoc[]
include::{snippets}/common/success-response/http-request.adoc[]
include::{snippets}/common/success-response/http-response.adoc[]
include::{snippets}/common/success-response/response-body.adoc[]
19 changes: 19 additions & 0 deletions src/docs/asciidoc/common/exception.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
== Exception Handling

=== Controller Exception
include::{snippets}/common/exception/controller-exception/curl-request.adoc[]
include::{snippets}/common/exception/controller-exception/http-request.adoc[]
include::{snippets}/common/exception/controller-exception/http-response.adoc[]
include::{snippets}/common/exception/controller-exception/response-body.adoc[]

=== Service Exception
include::{snippets}/common/exception/service-exception/curl-request.adoc[]
include::{snippets}/common/exception/service-exception/http-request.adoc[]
include::{snippets}/common/exception/service-exception/http-response.adoc[]
include::{snippets}/common/exception/service-exception/response-body.adoc[]

=== Repository Exception
include::{snippets}/common/exception/repository-exception/curl-request.adoc[]
include::{snippets}/common/exception/repository-exception/http-request.adoc[]
include::{snippets}/common/exception/repository-exception/http-response.adoc[]
include::{snippets}/common/exception/repository-exception/response-body.adoc[]
7 changes: 7 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
= ReadUp API Documentation
:toc: left
:toclevels: 3
:snippets: ../../../../build/generated-snippets

include::./src/docs/asciidoc/common/apiResponse.adoc[]
include::./src/docs/asciidoc/common/exception.adoc[]
16 changes: 16 additions & 0 deletions src/main/java/com/readup/server/common/dto/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.readup.server.common.dto;

public record ApiResponse<T>(boolean success, T data, String message) {

public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(true, data, message);
}

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, "Request succeeded.");
}

public static <T> ApiResponse<T> failure(String message) {
return new ApiResponse<>(false, null, message);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/readup/server/common/dto/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.readup.server.common.dto;

import com.readup.server.common.exception.ErrorCode;

public record ErrorResponse(int status, String error, String message) {

public ErrorResponse(ErrorCode errorCode) {
this(errorCode.getHttpStatus().value(), errorCode.name(), errorCode.getMessage());
}

public ErrorResponse(ErrorCode errorCode, String customMessage) {
this(errorCode.getHttpStatus().value(), errorCode.name(), customMessage);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.readup.server.common.exception;

import lombok.Getter;

@Getter
public class ApplicationException extends RuntimeException {
private final ErrorCode errorCode;

public ApplicationException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public ApplicationException(ErrorCode errorCode, String customMessage) {
super(customMessage);
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.readup.server.common.exception;

public class ControllerException extends ApplicationException {

public ControllerException(ErrorCode errorCode) {
super(errorCode);
}

public ControllerException(ErrorCode errorCode, String customMessage) {
super(errorCode, customMessage);
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/readup/server/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.readup.server.common.exception;

import org.springframework.http.HttpStatus;

import lombok.Getter;

@Getter
public enum ErrorCode {
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "필수 요청 파라미터가 누락되었습니다."),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다.");

private final HttpStatus httpStatus;
private final String message;

ErrorCode(HttpStatus httpStatus, String message) {
this.httpStatus = httpStatus;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.readup.server.common.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.readup.server.common.dto.ErrorResponse;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ControllerException.class)
public ResponseEntity<ErrorResponse> handleControllerException(ControllerException ex) {
log.error("[ControllerException] {} - {}", ex.getErrorCode(), ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage());
return new ResponseEntity<>(errorResponse, ex.getErrorCode().getHttpStatus());
}

@ExceptionHandler(ServiceException.class)
public ResponseEntity<ErrorResponse> handleServiceException(ServiceException ex) {
log.error("[ServiceException] {} - {}", ex.getErrorCode(), ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage());
return new ResponseEntity<>(errorResponse, ex.getErrorCode().getHttpStatus());
}

@ExceptionHandler(RepositoryException.class)
public ResponseEntity<ErrorResponse> handleRepositoryException(RepositoryException ex) {
log.error("[RepositoryException] {} - {}", ex.getErrorCode(), ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage());
return new ResponseEntity<>(errorResponse, ex.getErrorCode().getHttpStatus());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
log.error("[Unexpected Exception] {} - {}", ex, ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.readup.server.common.exception;

public class RepositoryException extends ApplicationException {

public RepositoryException(ErrorCode errorCode) {
super(errorCode);
}

public RepositoryException(ErrorCode errorCode, String customMessage) {
super(errorCode, customMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.readup.server.common.exception;

public class ServiceException extends ApplicationException {

public ServiceException(ErrorCode errorCode) {
super(errorCode);
}

public ServiceException(ErrorCode errorCode, String customMessage) {
super(errorCode, customMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.readup.server.common.integration;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.readup.server.common.integration.controller.TestCommonController;

@WebMvcTest(TestCommonController.class)
@AutoConfigureMockMvc(addFilters = false)
@ExtendWith(RestDocumentationExtension.class)
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
public class ApiResponseTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private WebApplicationContext context;

@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(documentationConfiguration(restDocumentation))
.build();
}

@Test
@DisplayName("정상 응답 반환 테스트")
public void whenNormalEndpointCalled_thenReturnsSuccessResponse() throws Exception {
mockMvc.perform(get("/test/success"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data").value("Success Response Data"))
.andExpect(jsonPath("$.message").value("Successful response."))
.andDo(document("common/success-response"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.readup.server.common.integration.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.readup.server.common.dto.ApiResponse;
import com.readup.server.common.exception.ControllerException;
import com.readup.server.common.exception.ErrorCode;
import com.readup.server.common.exception.RepositoryException;
import com.readup.server.common.exception.ServiceException;

@RestController
public class TestCommonController {

@GetMapping("/test/controller")
public void throwControllerException() {
throw new ControllerException(ErrorCode.INVALID_REQUEST, "Controller exception occurred");
}

@GetMapping("/test/service")
public void throwServiceException() {
throw new ServiceException(ErrorCode.RESOURCE_NOT_FOUND, "Service exception occurred");
}

@GetMapping("/test/repository")
public void throwRepositoryException() {
throw new RepositoryException(ErrorCode.INTERNAL_SERVER_ERROR, "Repository exception occurred");
}

@GetMapping("/test/success")
public ApiResponse<String> getSuccessResponse() {
return ApiResponse.success("Success Response Data", "Successful response.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.readup.server.common.integration.exception.exception;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.readup.server.common.integration.controller.TestCommonController;

@WebMvcTest(TestCommonController.class)
@AutoConfigureMockMvc(addFilters = false)
@ExtendWith(RestDocumentationExtension.class)
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
public class GlobalExceptionHandlerTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private WebApplicationContext context;

@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(documentationConfiguration(restDocumentation))
.build();
}

@Test
@DisplayName("컨트롤러 예외 발생 시 올바른 에러 응답 반환")
public void whenControllerExceptionThrown_thenReturnsProperErrorResponse() throws Exception {
mockMvc.perform(get("/test/controller"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("INVALID_REQUEST"))
.andExpect(jsonPath("$.message").value("Controller exception occurred"))
.andDo(document("common/exception/controller-exception"));
}

@Test
@DisplayName("서비스 예외 발생 시 올바른 에러 응답 반환")
public void whenServiceExceptionThrown_thenReturnsProperErrorResponse() throws Exception {
mockMvc.perform(get("/test/service"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("RESOURCE_NOT_FOUND"))
.andExpect(jsonPath("$.message").value("Service exception occurred"))
.andDo(document("common/exception/service-exception"));
}

@Test
@DisplayName("레포지토리 예외 발생 시 올바른 에러 응답 반환")
public void whenRepositoryExceptionThrown_thenReturnsProperErrorResponse() throws Exception {
mockMvc.perform(get("/test/repository"))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.error").value("INTERNAL_SERVER_ERROR"))
.andExpect(jsonPath("$.message").value("Repository exception occurred"))
.andDo(document("common/exception/repository-exception"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.readup.server.common.unit;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.readup.server.common.exception.ControllerException;
import com.readup.server.common.exception.ErrorCode;
import com.readup.server.common.exception.RepositoryException;
import com.readup.server.common.exception.ServiceException;

public class CustomExceptionTest {

@Test
@DisplayName("컨트롤러 예외 생성 및 메시지 검증")
public void testControllerException() {
ControllerException ex = new ControllerException(ErrorCode.INVALID_REQUEST, "Test controller error");
assertEquals(ErrorCode.INVALID_REQUEST, ex.getErrorCode());
assertEquals("Test controller error", ex.getMessage());
}

@Test
@DisplayName("서비스 예외 생성 및 메시지 검증")
public void testServiceException() {
ServiceException ex = new ServiceException(ErrorCode.RESOURCE_NOT_FOUND, "Test service error");
assertEquals(ErrorCode.RESOURCE_NOT_FOUND, ex.getErrorCode());
assertEquals("Test service error", ex.getMessage());
}

@Test
@DisplayName("레포지토리 예외 생성 및 메시지 검증")
public void testRepositoryException() {
RepositoryException ex = new RepositoryException(ErrorCode.INTERNAL_SERVER_ERROR, "Test repository error");
assertEquals(ErrorCode.INTERNAL_SERVER_ERROR, ex.getErrorCode());
assertEquals("Test repository error", ex.getMessage());
}
}
Loading