2024. 1. 3. 19:33ㆍ개발/🖤 UMC 백엔드 과정 🖤
1. API 응답 통일
API 응답의 가장 대표적인 형태
{
isSuccess : Boolean
code : String
message : String
result : {응답으로 필요한 또 다른 json}
}
- isSuccess : 성공인지 아닌지 알려주는 필드
- code : HTTP 상태 코드 외에 더 세부적인 결과를 알려주기 위해 사용
- message : code에 추가적으로 어떤 결과인지 알려주기 위해 사용 ( 우리에게 익숙한 문자로 )
- result : 실제로 클라이언트에게 필요한 데이터가 담김, 대체로 null
HTTP 상태 코드
1. 200번대 : 문제 없음
a. 200 : 성공
b. 201 : Created - 제공된 데이터로 적절한 과정을 통해 새로운 리소스 생성함
2. 400번대 : 클라이언트 측 잘못으로 인한 에러
a. 400 : Bad Request - 요청 과정에서 필수 정보 누락
b. 401 : Unauthorized - 인증 필요
c. 403 : Forbidden - 권한 없음
d. 404 : NotFound - 요청한 정보 없음
3. 500번대 : 서버 측 잘못으로 인한 에러
a. 500 : Internal Server Error - 서버 터짐
b. 504 : Gateway Timeout - 서버가 응답을 안 줌
API 응답 통일
💡 프론트엔드와 소통하기 위해서는 성공 / 실패했을 때 API의 응답을 통일해줄 필요가 있다
따라서 API 응답 통일을 위한 클래스들을 작성해주겠다
우선 실패했을 경우 API 응답 통일을 진행하겠다
ApiResponse Class
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
// 성공한 경우 응답 생성
//public static <T> ApiResponse<T> onSuccess(T result){
// return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result);
// }
//}
//public static <T> ApiResponse<T> of(BaseCode code, T result){
// return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result);
//}
// 실패한 경우 응답 생성
public static <T> ApiResponse<T> onFailure(String code, String message, T data){
return new ApiResponse<>(false, code, message, data);
}
}
🔆 기본 지식 및 어노테이션 설명
서버와 클라이언트가 원활하게 데이터를 주고 받기 위해 JSON이라는 통일된 데이터 객체 구조를 사용한다
서로 다른 프로그램 간의 통신이 필요할 때도 JSON 형식을 사용한다
이때 Java 객체를 JSON 형식으로 변환하는 것을 직렬화, JSON을 Java 객체로 변환하는 것을 역직렬화라 한다
Java 객체의 직렬화를 수행해주는 어노테이션이 @JsonPropertyOrder 이다
Java는 convention에 따라 카멜 케이스로 작성이 되고
JSON은 스네이크 케이스로 작성이 된다
따라서 Key값을 주고 받을 때 오류가 발생할 수 있다
이때
카멜 케이스로 작성된 필드를 스네이크 케이스로 변경해주는 어노테이션이 @JsonProperty 이다@JsonInclude는 JSON으로 직렬화할 때 제외할 필드의 조건을 설정해주는 어노테이션이다
+ ) @JsonInclude 상세 설명
https://ynzu-dev.tistory.com/entry/JAVA-jackson-JsonInclude-속성-null-empty등의-데이터-제외하기
ErrorReasonDTO - API가 실패했을 때 응답 DTO
@Getter
@Builder
public class ErrorReasonDTO {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final String code;
private final String message;
public boolean isSuccess(){return isSuccess;}
}
httpStatus, isSuccess, code, message 포함
BaseErrorCode Interface - ErrorStatus에 getReason()과 getReasonHttpStatus()를 Override하도록
public interface BaseErrorCode {
public ErrorReasonDTO getReason();
public ErrorReasonDTO getReasonHttpStatus();
}
ErrorStatus Enum - Error가 발생했을 때의 HTTP Status를 하나의 Enum Class로 저장
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
// 멤버 관련 응답
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
// 예시
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build();
}
}
🔆 코드 설명
API가 실패했을 때 응답으로 ErrorReasonDTO 객체를 생성하며 이를 ApiResponse Class에서 직렬화하여 return한다
이때 ErrorReasonDTO에는 isSuccess, code, message, (httpStatus)가 포함된다
ErrorStatus Class에는 다양한 error 상황에서의 ErrorStatus Enum이 저장되어 있다
또한 BaseErrorCode Interface를 상속받은 ErrorStatus Class에서
getReason() 혹은 getReasonHttpStatus() 메서드를 통해 ErrorReasonDTO를 생성한다
API가 성공했을 때의 경우도 위와 구조가 동일하다
간단한 API 구현
구현할 내용
클라이언트에게 받을 정보는 없기 때문에 Response DTO만 생성하면 된다
TempResponse - testString 포함
public class TempResponse {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempTestDTO{
String testString;
}
}
1. API 구현 과정에서 생성하는 인스턴스들은 대부분 Builder 형태로
이때 Request는 프론트엔드에서 보내주는 객체를 받기만 하면 되기 때문에 Builder 형태로 받지 않아도 된다
2. DTO는 여기저기에서 쓰일 일이 많기 때문에 DTO를 public static class로 생성하는 것이 좋다
이때 DTO class는 관련 DTO들을 묶어주도록 하는 것이 좋다
ex) MemberDTO class 내에 MemberPreferDTO, MemberInfoDTO 등을 public static class로 넣기
TempConverter - TempTestDTO 생성
public class TempConverter {
public static TempResponse.TempTestDTO toTempTestDTO(){
return TempResponse.TempTestDTO.builder()
.testString("This is Test!")
.build();
}
}
TempRestController
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(){
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
}
@RestController : Json 형태로 객체 반환
@RequestMapping : 요청받을 url 설정
@{}Mapping : url에 대한 HTTP 요청 설정 -> {} : Get, Post, Put, Delete
2. 에러 핸들러 (ing...)
Error Enum 규칙
1. common 에러는 COMMON000으로 둔다. <- 잘 안쓰지만 마땅하지 않을 때 사용
2. 관련된 경우마다 code에 명시적으로 표현한다.
- 예를 들어 멤버 관련이면 MEMBER001 이런 식으로
3. 2번에 이어서 4000번대를 붙인다. 서버측 잘못은 그냥 COMMON 에러의 서버 에러를 쓰면 됨.
- MEMBER400_1 아니면 MEMBER4001 이런 식으로
에러 핸들러의 의의
참고 : https://velog.io/@yenicall/What-is-Error-Handling
What is Error Handling?
사용자가 소프트웨어를 이용하면서 모든 경우에서 의도에 맞게 잘 흘러가면 좋겠지만 에러와 예외는 늘 발생한다. 정상적인 사용 흐름이 막히게 된다. 에러가 발생하는 이유는 너무나도 다양하
velog.io
에러 핸들러는 에러 핸들링과 예외 핸들링으로 나뉘는데
에러 핸들링은 컴퓨터에서 발생한 문법적인 오류를 처리하는 것을 의미하고
예외 핸들링은 사용자가 개발자의 의도대로 행동하지 않은 경우를 처리하는 것을 의미한다
개발자는 사용자가 발생시킬 예외 행동들을 예측해놓고
예외 핸들러를 통해 예외 상황에서 어떻게 처리할지 미리 구현해놓는 것이다
Error Status Enum Class
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
// 멤버 관려 에러
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
// 예시,,,
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
}
GeneralException Class
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
ExceptionAdvice
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
}
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
});
return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
}
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();
return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
}
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
// e.printStackTrace();
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
return super.handleExceptionInternal(
e,
body,
headers,
status,
request
);
}
private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
WebRequest request, Map<String, String> errorArgs) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
}
'개발 > 🖤 UMC 백엔드 과정 🖤' 카테고리의 다른 글
🖤 UMC 서버 7주차 스터디 - [ Springboot ] JPA를 통한 엔티티 설계, 매핑 & 프로젝트 파일 구조 이해 🖤 (0) | 2023.11.22 |
---|---|
🖤 UMC 서버 6주차 스터디 - API URL의 설계 & 프로젝트 세팅 🖤 (0) | 2023.11.15 |
🖤 UMC 서버 3주차 스터디 - Web Server & Web Application Server(WAS), Reverse Proxy 🖤 (0) | 2023.10.12 |
🖤 UMC 서버 2주차 스터디 - AWS (VPC & Internet Gateway & EC2) 🖤 (1) | 2023.10.07 |
🖤 서브넷팅, 서브넷, 서브 마스크 🖤 (1) | 2023.10.07 |