8.0 8주차 워크북 학습 목표
이번 주차는 프로젝트를 위한 여러가지 설정을 적용하였다.
먼저 API 응답을 통일하고,
서버나 클라이언트에서 발생한 오류를 처리하는 방법을 알아보자.
1) Spring Boot에서 API 응답 통일 적용하기
2) Spring Boot에서 에러 핸들러 구축하기
8.1 API 응답 통일
API는 보통 함수를 호출하듯, 하나의 변수에 응답을 받게 되는데,
응답의 형태가 모두 다르게 되면, 프론트 개발자가 이를 이해하기 어려워진다.
예를 들어 API 응답이 성공인 경우와 실패인 경우의 양식이 다르다면,
각각의 응답을 처리하고 파악하기 힘들 것이다.
API 응답 형식
API 응답은 보통 아래의 형태를 따른다.
{
isSuccess : Boolean
code : String
message : String
result : { 응답으로 필요한 또 다른 json }
}
{
"isSuccess ": true,
"code" : "2000",
"message" : "OK",
"result" :
{
"testString" : "This is test!"
}
}
1) isSuccess
- 성공 여부를 알려주는 필드
2) code
- HTTP 상태 코드에 더해 세부적인 결과를 알려주는 필드
- 예를 들어 403 Unauthorized가 떴을 때, 왜 권한 오류가 없는지 자세히 알려줌
2) message
- code에 추가적으로 어떤 결과인지를 문자로 알려주는 필드
3) result
- 실제로 클라이언트에게 응답으로 필요한 json 반환
- 응답에 실패한 경우 null 반환
[ 참고 ] HTTP 상태코드
1. 200번대 - 문제 없음
- 200 OK : 성공
- 201 Created : 받은 데이터를 가지고 새로운 리소스를 만듦
2. 400번대 - 클라이언트에 의한 에러
- 400 Bad Request : 필요한 정보가 누락되는 등 잘못된 요청
- 401 Unauthorized : 로그인이 안되어 있는 증 인증이 안됨
- 403 Forbidden : 관리자 페이지 접근과 같이 권한 없음
- 404 NotFound : 요청한 정보가 없음
3. 500번대 - 서버에 의한 에러
- 500 Internal Server Error : 서버 터짐..
- 504 Gateway Timeout : 서버가 응답을 안줌
API 응답 통일
먼저 apiPayload 라는 패키지를 생성하고,
ApiResponse 클래스를 만든 후 다음과 같이 코드를 작성한다.
ApiResponse 클래스는 API 응답 정보를 정하는 클래스이다.
package com.example.umcstudy.apiPayload;
import com.example.umcstudy.apiPayload.code.BaseCode;
import com.example.umcstudy.apiPayload.code.status.SuccessStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;
@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<>(true, code, message, data);
}
}
1) @JsonPropertyOrder
- 클래스의 인스턴스를 JSON으로 변환할 시, 속성의 순서를 직렬화
2) ApiResponse <T>
- T라는 타입 매개변수를 가진 제네릭 클래스 선언
3) @JsonInclude(JsonInclude.Include.NON_NULL)
- 값이 null일 경우 result 필드는 해당 JSON에서 제외
4) private T result
- result 값이 어떤 형태로 올지 알 수 없으므로, 제네릭 타입 선언
다음으로 apiPayload > code > status 패키지를 생성한 후
ErrorStatus, SuccessStatus enum을 작성해야 한다.
보통 응답은 Code 라는 이름의 enum으로 형태로
code와 message 형식을 관리한다.
(성공과 실패 응답을 통일할 수도 있고, 분리할 수도 있음)
일단 Code enum으로 구체화 코드를 작성하기 전에,
이를 위한 BaseCode와 BaseErrorCode 인터페이스를 작성해보자.
[ BaseCode ] - 인터페이스
package com.example.umcstudy.apiPayload.code;
public interface BaseCode {
public ReasonDTO getReason();
public ReasonDTO getReasonHttpStatus();
}
[ BaseErrorCode ] - 인터페이스
package com.example.umcstudy.apiPayload.code;
public interface BaseErrorCode {
public ErrorReasonDTO getReason();
public ErrorReasonDTO getReasonHttpStatus();
}
해당 인터페이스의 두개의 메소드는
이를 구체화하는 아래의 enum Status에서 반드시 오버라이드 해야 한다.
이제 앞선 BaseCode와 BaseErrorCode 인터페이스를 구현하는
ErrorStatus 와 SuccessStatus enum을 작성해보자.
[ ErrorStatus ]
package com.example.umcstudy.apiPayload.code.status;
import com.example.umcstudy.apiPayload.code.BaseErrorCode;
import com.example.umcstudy.apiPayload.code.ErrorReasonDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
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", "게시글이 없습니다."),
// Ror test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"),
// FoodCategory Error
FOOD_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "FOOD_CATEGORY4001", "음식 카테고리가 없습니다.");
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()
;
}
}
[ SuccessStatus ]
package com.example.umcstudy.apiPayload.code.status;
import com.example.umcstudy.apiPayload.code.BaseCode;
import com.example.umcstudy.apiPayload.code.ReasonDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {
// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");
// 멤버 관련 응답
// ~~~ 관련 응답
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ReasonDTO getReason() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.build();
}
@Override
public ReasonDTO getReasonHttpStatus() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.httpStatus(httpStatus)
.build()
;
}
}
임시 API 만들어보기
임시 API를 만들어서 앞서 작성한 코드가
어떻게 동작하는지 확인해보자.
먼저 API response body 데이터는 다음과 같다.
[ GET /temp/test ]
{
"isSuccess ": true,
"code ":"2000",
"message" : "OK",
"result" :
{
"testString" : "This is test!"
}
}
먼저 TempResponse DTO를 다음과 같이 작성한다.
package com.example.umcstudy.web.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class TempResponse {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempTestDTO{
String testString;
}
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempExceptionDTO{
Integer flag;
}
}
[ 참고 ] public static class란?
- DTO의 경우 수많은 곳에서 사용되므로, static class로 만드는 것이 좋음
- 매번 class 파일을 새로 만들지 않아도, 범용적으로 사용 가능
- 따라서 큰 클래스 묶음을 만들고, 내부적으로 static 클래스를 작성
[ 참고 ] DTO에 적용되는 빌더 패턴
- 작성하는 대부분의 인스턴스는 빌더 패턴이 적용됨
- requestDTO의 경우 프론트의 객체를 단순히 받게 되므로, 빌더 패턴을 적용하지 않음
다음으로 TempConverter 클래스를 생성하여
repository에서 받아온 엔티티를 dto로 바꾸는 과정 수행해보자
package com.example.umcstudy.converter;
import com.example.umcstudy.web.dto.TempResponse;
public class TempConverter {
public static TempResponse.TempTestDTO toTempTestDTO(){
return TempResponse.TempTestDTO.builder()
.testString("This is Test!")
.build();
}
public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag){
return TempResponse.TempExceptionDTO.builder()
.flag(flag)
.build();
}
}
앞서 TempResponse에서 작성한
TempTestDTO와 TempExcetionDTO를 생성하고 반환할 수 있도록 한다.
마지막으로 Controller를 작성해보자.
package com.example.umcstudy.web.controller;
import com.example.umcstudy.apiPayload.ApiResponse;
import com.example.umcstudy.converter.TempConverter;
import com.example.umcstudy.web.dto.TempResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(){
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
}
1) @RestController
- @Controller + @Responsebody
- JSON 형태로 객체 데이터 반환
2) @RequiredArgsConstructor
- 초기화 되지 않은 final 필드나, @NonNull이 붙은 필드에 대해 생성자 생성
- 새로운 필드를 추가할 때 다시 생성자를 만들어서 관리하지 않아도 됨
- @Autowired를 사용하지 않고 의존성 주입
실제로 스프링 부트를 실행하고 localhost:8080/temp/test에 접속하면
아래와 같이 응답 데이터가 뜨는 것을 확인할 수 있다.
(원격 서버에 Spring Boot를 구축한 경우,
nginx에 도달 후 리버스 프록시를 통해 SpringBoot로 요청이 감)
최용욱 'UMC Server 8주차 워크북' 내용을 기반으로 작성하였습니다.
'[UMC Ewha 5th] Server - SpringBoot' 카테고리의 다른 글
[UMC Server] Chapter 7. JPA를 통한 엔티티 설계, 매핑 & 프로젝트 파일 구조 이해 (1) | 2023.11.22 |
---|---|
[UMC Server] Chapter 6. API URL의 설계 & 프로젝트 세팅 (0) | 2023.11.15 |
[UMC Server] Chapter 5. 실전 SQL - 어떤 Query를 작성해야 할까? (0) | 2023.11.07 |
[UMC Server] Chapter 4. Database 설계 & AWS RDS 설정 (1) | 2023.11.01 |
[UMC Server] Chapter 3. Web Server & Web application Server(WAS), Reverse Proxy (1) | 2023.10.09 |