HTTP 통신의 Status 코드는 매우 한정적이다.
HTTP 통신은 자체적으로 status 코드를 가지고 있다. (2xx, 4xx, 5xx)
하지만 이는 서비스를 구현할 때 많은 에러를 표현하기에는 매우 한정적이다.
예를 들어 "사용자를 못 찾는다", "사용자의 상태가 휴먼상태다", "회원탈퇴한 사용자다" 등
이러한 에러들은 Http Status를 가지고 설명하기엔 굉장히 힘들다.
그렇기에 서비스 내부의 status 코드를 정의하고 사용하여 여러 상황을 보다 구체적으로 표현할 필요가 있다.
더불어 오픈 API를 보면 API의 공통 스펙을 보통 가지고 있다. (클라이언트와의 약속된 통신 스펙을 정의)
따라서 이번엔 프로젝트의 공통 스펙을 정의하고 해당 클래스를 Wrapping해서 응답할 수 있도록
API 클래스를 만들어보려고 한다.
원하는 API 공통 스펙
원하는 API 공통 스펙은 result 안에는 결과 관련 데이터가 있고, 응답 데이터는 body 안에 넣어서 보내려고 한다.
Api 클래스
package org.delivery.api.common.api;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Api<T> {
private Result result;
@Valid
private T body;
public static <T> Api<T> OK(T data) {
Api<T> api = new Api<T>();
api.result = Result.OK();
api.body = data;
return api;
}
}
body에는 우리가 원하는 데이터가 들어가게 하기 위해 <T> 제네릭타입을 사용한다.
주의 : body에 들어가는 데이터를 검증할 @Valid를 사용한다. (잊지 말것!)
해당 어노테이션을 붙이지 않으면 검증을 하지 않는다.
API의 응답을 좀 더 편리하게 하기 위해 static 메서드(OK) 를 추가했다.
Result 클래스
package org.delivery.api.common.api;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Result {
private Integer resultCode;
private String resultMessage;
private String resultDescription;
public static Result OK() {
return Result.builder()
.resultCode(200)
.resultMessage("OK")
.resultDescription("성공")
.build();
}
}
Api 공통 스펙을 기준으로 result 부분인 결과 코드,메시지, 설명을 가진 Result 클래스다.
API의 static 메서드(OK)에 사용할 Result.OK 메서드를 추가했다.
Api 클래스 사용 예시
// 로그인
@PostMapping("/login")
public Api<TokenResponse> login(
@Valid
@RequestBody Api<UserLoginRequest> request
) {
var response = userBusiness.login(request.getBody());
return Api.OK(response);
}
클라이언트한테 요청을 받을 때도 API의 공통 스펙으로 통신을 할 것이기에,
요청을 받을 때도 @RequestBody에도 Api<T>로 감싼 요청 데이터를 받고,
응답할 때도 앞서 만든 Api.OK(data)를 넣어서 응답 데이터를 보내서
클라이언트와 서버의 통신이 항시 API 스펙에 맞게끔 할 수 있게 되었다.
앞서 API의 공통 스펙을 정의해보았다.
이번엔 프로젝트의 여러 에러에 대응할 수 있는 API Error code를 구현해보려고 한다.
프로젝트 개발을 하면서 여러 상황에서의 에러가 발생한다.
이를 하나의 클래스(ex. Enum)에서 관리하는 것은 무리가 있기에,
에러 관련 상태코드나 설명을 할 수 있는 인터페이스를 인터페이스로 뽑으려 한다.
API Error Code
앞서 말했듯이, 프로젝트의 모든 ErrorCode를 하나의 Enum 클래스에서 관리하는 것은 힘들기에 인터페이스를 만든다.
ErrorCodeIfs
package org.delivery.api.common.error;
public interface ErrorCodeIfs {
Integer getHttpStatusCode();
Integer getErrorCode(); // 우리 서비스 자체적인 error code
String getDescription();
}
해당 인터페이스의 메서드명을 getXX으로 한 이유는 ErrorCodeIfs를 구현할 때 @Getter를 통해 손쉽게 오버라이딩하기 위해 사용했다.
ErrorCode
package org.delivery.api.common.error;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum ErrorCode implements ErrorCodeIfs {
OK(HttpStatus.OK.value(), 200, "성공"),
BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), 400, "잘못된 요청"),
SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), 500, "서버 에러"),
NULL_POINT(HttpStatus.INTERNAL_SERVER_ERROR.value(), 512, "Null point 에러"),
;
private final Integer httpStatusCode;
private final Integer errorCode; // 우리 서비스의 error code
private final String description;
}
이렇게 @Getter를 통해 메소드 오버라이딩되면서 보다 편리하게 구현할 수 있다.
더불어 인터페이스를 구현했기에 인터페이스를 받아서 언제든지 원하는 구현체(ErrorCode 등)을 사용할 수 있다.(+다형성)
Result 클래스 수정(+ErrorCode 관련 내용)
package org.delivery.api.common.api;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.delivery.api.common.error.ErrorCode;
import org.delivery.api.common.error.ErrorCodeIfs;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Result {
private Integer resultCode;
private String resultMessage;
private String resultDescription;
public static Result OK() {
return Result.builder()
.resultCode(ErrorCode.OK.getErrorCode())
.resultMessage(ErrorCode.OK.getDescription())
.resultDescription("성공") // 추후 상세메시지로 사용
.build();
}
public static Result ERROR(ErrorCodeIfs errorCodeIfs) {
return Result.builder()
.resultCode(errorCodeIfs.getErrorCode())
.resultMessage(errorCodeIfs.getDescription())
.resultDescription("에러 발생") // 추후 상세메시지로 사용
.build();
}
public static Result ERROR(ErrorCodeIfs errorCodeIfs, Throwable tx) {
return Result.builder()
.resultCode(errorCodeIfs.getErrorCode())
.resultMessage(errorCodeIfs.getDescription())
.resultDescription(
tx.getLocalizedMessage()) // tx.getLocalizedMessage는 서버에 모든 stack trace가 내려가기에 비추 (일단은 넘어가자)
.build();
}
public static Result ERROR(ErrorCodeIfs errorCodeIfs, String description) {
return Result.builder()
.resultCode(errorCodeIfs.getErrorCode())
.resultMessage(errorCodeIfs.getDescription())
.resultDescription(description)
.build();
}
}
static ERROR 메서드에서는 ErrorCodeIfs 인터페이스를 인자로 받아서 인터페이스를 구현한 클래스를 받는다.
- - ErrorCodeIfs 인터페이스를 받아서 처리
- - ErrorCodeIfs 인터페이스와 Throwable 인터페이스를 받아서 처리 (description에 stackk trace가 내려가기에 비추)
- - ErrorCodeIfs 인터페이스와 에러 관련 메시지를 받아서 처리(가장 자주 사용)
이렇게 Error 메서드를 3가지를 추가했다.
Api 클래스 수정(+ErrorCode 관련 내용)
package org.delivery.api.common.api;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.delivery.api.common.error.ErrorCodeIfs;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Api<T> {
private Result result;
@Valid
private T body;
public static <T> Api<T> OK(T data) {
Api<T> api = new Api<T>();
api.result = Result.OK();
api.body = data;
return api;
}
// ERROR의 경우엔 body에 넣을 내용이 없다.
public static Api<Object> ERROR(Result result) {
Api<Object> api = new Api<>();
api.result = result;
return api;
}
public static Api<Object> ERROR(ErrorCodeIfs errorCodeIfs) {
Api<Object> api = new Api<>();
api.result = Result.ERROR(errorCodeIfs);
return api;
}
public static Api<Object> ERROR(ErrorCodeIfs errorCodeIfs, Throwable tx) {
Api<Object> api = new Api<>();
api.result = Result.ERROR(errorCodeIfs, tx);
return api;
}
public static Api<Object> ERROR(ErrorCodeIfs errorCodeIfs, String description) {
Api<Object> api = new Api<>();
api.result = Result.ERROR(errorCodeIfs, description);
return api;
}
}
에러가 날 경우는 body에 넣을 내용이 없다.
그렇기 때문에 return 타입을 Api<Object>로 지정했다. 즉, 제네릭 타입을 Object로 지정했다.
제네릭의 경고를 없애기 위해서 제네릭 타입을 Object로 지정했다.
하지만 좋은 방법은 아니기에 다음 포스트에서 이를 ExceptionHandler가 처리할 것이다.
Api 클래스에선 Result 클래스에 추가한 메서드에 대응하는 ERROR 메서드 3개와 Result를 받아서 처리하는 ERROR 메서드를 작성했다.
수정한 Api 클래스(+ErrorCode 관련 내용) 사용 예시
@GetMapping("/me2")
public Api<Object> m2e() {
AccountMeResponse response = AccountMeResponse.builder()
.name("홍길동")
.email("a@gmail.com")
.registeredAt(LocalDateTime.now())
.build();
return Api.ERROR(UserErrorCode.USER_NOT_FOUND, response.getName() + "이라는 사용자는 없음");
}
정리
API 공통 스펙과 에러코드 처리는 프로젝트마다 다르고 방식도 다르다는 점을 유의하고 있어야 한다.
다만, 이러한 방식으로 에러 코드를 처리한다면 보다 편리하다는 것을 알고 있어야,
추후 다른 프로젝트에서도 다른 방식이더라도 개발하는데 많은 도움을 주기에 이 글을 작성했다.
중요한 점은 API 공통 스펙을 Api<T> 클래스로 정의하고 클라이언트와 서버 간의 공통 스펙으로 요청과 응답을 하도록 한 것과 여러 프로젝트의 에러 상황을 대처할 수 있게끔 에러 코드 관련 클래스를 만들고 이를 Api<T> 클래스에 적용했다는 것이다.
추후 에러 코드 관련하여 ExceptionHandler를 통해서 처리하는 것도 정리해보려고 한다. (작성 완료 2024.04.12)
https://k9want.tistory.com/entry/Spring-ControllerAdvice-ExceptionHandler
[Spring] 스프링 부트에서의 예외 처리 - @ControllerAdvice, @ExceptionHandler(feat. 프로젝트에 맞는 CustomExcep
예외가 발생했을 때 어떻게 처리하는 것이 좋은 방법일까? - 예외 복구 : 예외가 발생하면 예외 상황에 맞게 처리하여 다시 복구한다. (ex. try, catch) - 예외 회피 : 예외를 직접 처리하지 않고 예외
k9want.tistory.com