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

feat: apiPayload 세팅 #1

Open
wants to merge 5 commits into
base: main
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
7 changes: 4 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(17)
}
}

Expand All @@ -24,11 +24,12 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
// runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.assertj:assertj-core'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.example.phrasebe.common.exception;

import com.example.phrasebe.common.response.ApiResponse;
import com.example.phrasebe.common.status.ErrorStatus;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler
public ResponseEntity<ApiResponse> validation(ConstraintViolationException e) {
String errorMessage = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return ApiResponse.onFailure(ErrorStatus.VALIDATION_ERROR, errorMessage);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
e.printStackTrace();

String errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
return ApiResponse.onFailure(ErrorStatus.VALIDATION_ERROR, errorMessage);
}

@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ApiResponse> handleMissingServletRequestParameterException(
MissingServletRequestParameterException e
) {
e.printStackTrace();

String errorMessage = e.getParameterType() + " 타입의 " + e.getParameterName() + " 파라미터가 없습니다.";
return ApiResponse.onFailure(ErrorStatus.VALIDATION_ERROR, errorMessage);
}

@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiResponse> handleNoResourceFoundException(NoResourceFoundException e) {
e.printStackTrace();

return ApiResponse.onFailure(ErrorStatus._NOT_FOUND);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleException(Exception e) {
e.printStackTrace();

return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(GeneralException.class)
public ResponseEntity<ApiResponse> handleGeneralException(GeneralException e) {
e.printStackTrace();

return ApiResponse.onFailure(e.getErrorStatus(), e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.phrasebe.common.exception;

import com.example.phrasebe.common.status.ErrorStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException{

private ErrorStatus errorStatus;

public GeneralException() {
super(ErrorStatus._INTERNAL_SERVER_ERROR.getMessage());
this.errorStatus = ErrorStatus._INTERNAL_SERVER_ERROR;
}

public GeneralException(String message) {
super(message);
this.errorStatus = ErrorStatus._INTERNAL_SERVER_ERROR;
}

public GeneralException(String message, Throwable cause) {
super(message, cause);
this.errorStatus = ErrorStatus._INTERNAL_SERVER_ERROR;
}

public GeneralException(Throwable cause) {
super(cause.getMessage(), cause);
this.errorStatus = ErrorStatus._INTERNAL_SERVER_ERROR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.example.phrasebe.common.response;

import com.example.phrasebe.common.status.ErrorStatus;
import com.example.phrasebe.common.status.SuccessStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
//import org.springframework.data.domain.Page;
//import org.springframework.data.domain.Slice;
import org.springframework.http.ResponseEntity;

@Getter
@RequiredArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse {

private final Boolean isSuccess;
private final String code;
private final String message;

@JsonInclude(Include.NON_NULL)
private final PageInfo pageInfo;
@JsonInclude(Include.NON_NULL)
private final Object result;


// 성공한 경우 응답 생성
public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, PageInfo pageInfo, Object result) {
return new ResponseEntity<>(
new ApiResponse(true, status.getCode(), status.getMessage(), pageInfo, result),
status.getHttpStatus()
);
}

// 성공 - 기본 응답
public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status) {
return onSuccess(status, null, null);
}

// 성공 - 데이터가 포함된 응답
public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, Object result) {
return onSuccess(status, null, result);
}

// 성공 - 페이지네이션에 대한 응답
// public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, Page<?> page) {
// PageInfo pageInfo = new PageInfo(page.getNumber(), page.getSize(), page.hasNext());
// return onSuccess(status, pageInfo, page.getContent());
// }
//
// public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, Slice<?> page) {
// PageInfo pageInfo = new PageInfo(page.getNumber(), page.getSize(), page.hasNext());
// return onSuccess(status, pageInfo, page.getContent());
// }


// 실패한 경우 응답 생성
public static ResponseEntity<ApiResponse> onFailure(ErrorStatus error) {
return new ResponseEntity<>(
new ApiResponse(false, error.getCode(), error.getMessage(), null, null),
error.getHttpStatus()
);
}

public static ResponseEntity<ApiResponse> onFailure(ErrorStatus error, String message) {
return new ResponseEntity<>(
new ApiResponse(false, error.getCode(), error.getMessage(message), null, null),
error.getHttpStatus()
);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/example/phrasebe/common/response/PageInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.phrasebe.common.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class PageInfo {
private Integer page;
private Integer size;
private Boolean hasNext;
}
38 changes: 38 additions & 0 deletions src/main/java/com/example/phrasebe/common/status/ErrorStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.phrasebe.common.status;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.util.Optional;
import java.util.function.Predicate;

@Getter
@AllArgsConstructor
public enum ErrorStatus {

// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "페이지를 찾을 수 없습니다."),

// 입력값 검증 관련 에러
VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "VALID401", "입력값이 올바르지 않습니다."),


// 멤버 관려 에러
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

public String getMessage(String message) {
return Optional.ofNullable(message)
.filter(Predicate.not(String::isBlank))
.orElse(this.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.phrasebe.common.status;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum SuccessStatus {

// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
26 changes: 26 additions & 0 deletions src/main/java/com/example/phrasebe/test/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.phrasebe.test;

import com.example.phrasebe.common.response.ApiResponse;
import com.example.phrasebe.common.status.SuccessStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
@Autowired
private TestService testService;

@GetMapping("/data")
public ResponseEntity<ApiResponse> getDataTest() {
String result = testService.testData();
return ApiResponse.onSuccess(SuccessStatus._OK, result);
}

@GetMapping("/error")
public ResponseEntity<ApiResponse> errorTest() {
testService.testError();
return ApiResponse.onSuccess(SuccessStatus._OK);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/example/phrasebe/test/TestService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.phrasebe.test;

import com.example.phrasebe.common.exception.GeneralException;
import com.example.phrasebe.common.status.ErrorStatus;
import org.springframework.stereotype.Service;

@Service
public class TestService {
public String testData() {
return "테스트 성공!";
}

public void testError() {
throw new GeneralException(ErrorStatus.MEMBER_NOT_FOUND);
}
}
47 changes: 47 additions & 0 deletions src/test/java/com/example/phrasebe/test/TestControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.phrasebe.test;

import com.example.phrasebe.common.status.ErrorStatus;
import com.example.phrasebe.common.status.SuccessStatus;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class TestControllerTest {
@Autowired
private MockMvc mockMvc;

@Test
@DisplayName("getData(): 데이터가 포함된 ApiResponse 객체가 Http Response body로 설정된다.")
void getDataTest() throws Exception {
String expectResult = "테스트 성공!";

mockMvc.perform(MockMvcRequestBuilders.get("/data")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isSuccess").value(true))
.andExpect(jsonPath("$.code").value(SuccessStatus._OK.getCode()))
.andExpect(jsonPath("$.message").value(SuccessStatus._OK.getMessage()))
.andExpect(jsonPath("$.result").value(expectResult));
}

@Test
@DisplayName("errorTest(): GeneralException 발생 시 에러 응답이 반환된다.")
void errorTest() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/error")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.isSuccess").value(false))
.andExpect(jsonPath("$.code").value(ErrorStatus.MEMBER_NOT_FOUND.getCode()))
.andExpect(jsonPath("$.message").value(ErrorStatus.MEMBER_NOT_FOUND.getMessage()));
}
}