📜응답 메세지 정규화
MSA를 만들면 다루는 서비스가 각자 다르다. 그럼 우리는 응답 메시지를 하나로 통합해야 할 것이다.
그리고 API를 사용하는 클라이언트를 위한 배려이자, 예측 가능하고 안정적인 시스템을 만들기 위해서 필수적인 설계 패턴이 되는 것이다.
📜HTTP Status
- 정보 전달 -> 작업을 진행하고 있음을 의미한다.
- 성공 -> 작업을 성공적 처리했음을 의미한다.
- 리다이랙션 -> 요청 완료를 위해 리다이랙션이 이루어져야 한다는 의미
- 클라이언트오류
- 서버 오류
- 정의되지 않는다. -> 사용자 정의 영역에 해당
이렇게 총 100번대부터 600번대까지 있다.
이런 응답코드를 각 서비스마다 정규화를 시켜줘야된다.
필자는 모든 포맷팅을 JSON으로 맞춰줄 것이다.
{
“code” : “OK”, “MisiingParameter”
“message”: “정상처리 되었습니다.” 또는 “파라미터가 누락되었습니다.”
}
와 같이 프로젝트 초기단계에서 셋팅을 해주고 갈 것이다.
정규화 즉 공통으로 사용될 목적으로 포함되는 것이기때문에 root프로젝트 디렉토리에서
common으로 만들어서 진행할 목적이다.
ApiResponseDto.java
package com.welab.backend_user.common.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@NoArgsConstructor()
@ToString
public class ApiResponseDto<T> {
private String code;
private String message;
// data는 제네릭 타입으로 String, 객체 따로 있을 수 있으니까
private T data;
//
private ApiResponseDto(String code, String message){
this.code = code;
this.message = message;
}
private ApiResponseDto(String code, String message, T data){
this.message = message;
this.code = code;
this.data = data;
}
public static <T> ApiResponseDto<T> createOk(T data){
return new ApiResponseDto<>("OK", "요청 성공", data);
}
public static ApiResponseDto<String> defaultOk(){
return ApiResponseDto.createOk(null);
}
public static ApiResponseDto<String> createError(String code, String message){
return new ApiResponseDto<>(code, message);
}
}
Setter로 생성자를 만들지 않고 직접 생성자를 private로 만들어서 new를 통해 객체를 직접 생성하는 것을 막고,
정적 팩토리 메서드를 통해서만 객체를 생성하도록 유도하는 설계방식을 택했다.
CreateOk는 파라미터 3개인 생성자를 받고
createError는 파라미터 2개인 생성자를 받게 해서 외부에서 직접 생성을 막는 것이다.
공통 API Exception 설계하기
공통 Exception을 만들어주는 이유는 개별 서비스 메소드에서 try-catch로 처리 하는 것은 코드가 매우 지저분 해지고 중복코드를 양산하며, 유지보수를 어렵게 만들어주기때문에 공통 Exception을 만들어주는것이다.
ApiError.java
package com.welab.backend_user.common.exception;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
//API, 비즈니스 로직에서 발생한 에러는 runTimeException 을 사용함
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiError extends RuntimeException{
protected String errorCode;
protected String errorMessage;
}
//protected가 있선언되어있으면 매개변수 없는 생성자 생성 불가
BadParameter.java
package com.welab.backend_user.common.exception;
public class BadParameter extends ClientError {
public BadParameter(String errorMessage) {
this.errorCode = "BadParameter";
this.errorMessage = errorMessage;
}
}
ClientError.java
package com.welab.backend_user.common.exception;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ClientError extends ApiError{
}
NotFound.java
package com.welab.backend_user.common.exception;
public class NotFound extends ClientError{
public NotFound(String message){
this.errorCode = "Not Found";
this.errorMessage = message;
}
}
이렇게 까지 구성해주면 공통 ResponseDto와 Exception처리는 끝나는 것이다.
📜AOP Advice
서버에 에러는 사용자 UI에 뜨게 하면 매우 위험할 수 있기때문에 사용자 UI에 뜨게해서는 안된다.
그래서 잘못된 URL이 넘어왔을 때나 어떤 Exception이 뜨는지 알 수 있게 중단점을 맞춰주는 것이다.
3단계 ApiError, ClientError, NotFound로 나눈 이유는 클라이언트와 서버 상태 코드가 다르기때문에
Http 상태값을 세분화 해서 더 자세하게 전달가능하게 만들어줄려고 한다.
Exception은 ErrorCode를 만드는 것이 굉장히 중요한데 Error가 났을 때 어떤 code가 나가는지가 굉장히 중요하다.
별도로 처리를 할 때는 handler를 따로 나눠서 처리를 하되, 상위 client Error는 놔두는 것이 좋다.
package com.welab.backend_user.advice;
import com.welab.backend_user.common.dto.ApiResponseDto;
import com.welab.backend_user.common.exception.BadParameter;
import com.welab.backend_user.common.exception.ClientError;
import com.welab.backend_user.common.exception.NotFound;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@Slf4j
@Order(value = 1)
@RestControllerAdvice
public class ApiCommonAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ClientError.class})
public ApiResponseDto<String> handleClientError(ClientError e){
return ApiResponseDto.createError(
e.getErrorCode(),
e.getErrorMessage()
);
}
// 모든 예외를 받을 수 있는 핸들러
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler({Exception.class})
public ApiResponseDto<String> handleException(Exception e) {
return ApiResponseDto.createError(
"serverError",
"서버 에러입니다.");
}
// 리소스 foundexception
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler({NoResourceFoundException.class})
public ApiResponseDto<String> handleNoResourceFoundException(NoResourceFoundException e){
return ApiResponseDto.createError(
"No Resource",
"잘못된 URL입니다."
);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({BadParameter.class})
public ApiResponseDto<String> handleBadParameter(BadParameter e){
return ApiResponseDto.createError(
e.getErrorCode(),
e.getErrorMessage()
);
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler({NotFound.class})
public ApiResponseDto<String> handleNotFound(NotFound e){
return ApiResponseDto.createError(
e.getErrorCode(),
e.getErrorMessage()
);
}
}
다음과 같이 Order(value = 1)로 우선순위를 1순위로 Common AOP를 맞춰주면 다른 Exception이 발생하는 것보다
해당 Exception이 가로채서 먼저 발생한다.
이렇게 설정해주면 공통 응답과 Exception이 발생했을 때 공통으로 처리하는 역할을 가능하게 하는 것이다.
'MSA' 카테고리의 다른 글
[MSA 5일차] - 서버 사이드 서비스 디스커버리로 서비스 로직 작성하기 (1) | 2025.06.14 |
---|---|
[MSA 3일차] - OpenFeign적용해서 RestTemplate과 차이 알기 (2) | 2025.06.08 |
[MSA 2일차] - 스프링 클라우드 서비스 디스커버리 알아보자 (1) | 2025.06.04 |
[MSA 2일차] - 스프링 클라우드로 MSA 구조 찍먹하기 (0) | 2025.06.01 |
[MSA 2일차] - 도커 개념과 실습 찍먹하기 (1) | 2025.05.31 |