1. ResponseDto class
package api.common.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
    private T data;
    private String message;
}
  1. ApiErrorCode enum
package api.enums;

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

import java.util.Arrays;

/**
 * ApiErrorCode 주석
 *
 * ERROR_CODE 넘버링 : DOMAIN1(1) / DOMAIN2(2) / DOMAIN3(3) / ... / COMMON(8) / EXTERNAL(9)
*/
@Getter
public enum ApiErrorCode {
    // 1. DOMAIN1
		// 1.1. sub-domain1
    // 1.2. sub-domain2
    // 1.3. sub-domain3
		SUBDOMAIN_ONE_NOT_FOUND("1101", HttpStatus.NOT_FOUND, "error message 1"),
		SUBDOMAIN_TWO_ERROR("1201", HttpStatus.NOT_FOUND, "error message 2"),
    AUTH_INVALID("1301", HttpStatus.UNAUTHORIZED, "error message 3"),

    // 8. COMMON
    METHOD_NOT_ALLOWED("8001", HttpStatus.METHOD_NOT_ALLOWED, "Not Allowed Request"),
    TOO_MANY_REQUESTS("8002", HttpStatus.TOO_MANY_REQUESTS, "Too Many Requests"),
    OTHER_ERROR("8003", HttpStatus.INTERNAL_SERVER_ERROR, "Unknown Server Error"),
    SERVICE_UNAVAILABLE("8004", HttpStatus.SERVICE_UNAVAILABLE, "Service Under Inspection"),
    INVALID_TYPE("8005", HttpStatus.BAD_REQUEST, "Unavailable Type"),

    // 9. EXTERNAL
    // 9.1. DB
    // 9.2. CONVERTOR
    // 9.3. ES
    CONNECT_TIMEOUT("9003", HttpStatus.INTERNAL_SERVER_ERROR, "Connection Delay to External Server"),
    RESPONSE_TIMEOUT("9004", HttpStatus.INTERNAL_SERVER_ERROR, "Response Delay to External Server"),

    DB_ERROR("9101", HttpStatus.INTERNAL_SERVER_ERROR, "Database Error occur"),

    PDF_CONVERSION_ERROR("9201", HttpStatus.INTERNAL_SERVER_ERROR, "Pdf Conversion Error"),
    HTML_CONVERSION_ERROR("9202", HttpStatus.INTERNAL_SERVER_ERROR, "Html Conversion Error"),
    UNKNOWN_CONVERSION_ERROR("9203", HttpStatus.INTERNAL_SERVER_ERROR, "Unknown Conversion Error"),

    UNKNOWN_ES_ERROR("9301", HttpStatus.INTERNAL_SERVER_ERROR, "Unknown ES Error"),

    OTHER_NETWORK_ERROR("9999", HttpStatus.INTERNAL_SERVER_ERROR, "Unknown Error to External Server");

    private final String errorCode;

    private final HttpStatus httpStatus;

    private final String message;

    ApiErrorCode(String errorCode, HttpStatus httpStatus, String message) {
        this.errorCode = errorCode;
        this.httpStatus = httpStatus;
        this.message = message;
    }

    public static ApiErrorCode findByErrorCode(String errorCode){
        return Arrays.stream(values())
                .filter(e -> e.getErrorCode().equals(errorCode))
                .findAny()
                .orElse(null);
    }
}
  1. ErrorResponseDto class
package api.common.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponseDto {
    private String timeStamp;
    private String errorCode;
    private String errorMessage;
    private String moreInfo;
}
  1. ExceptionController class
package api.exception;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import io.jsonwebtoken.JwtException;
import api.common.dto.ErrorResponseDto;
import api.enums.ApiErrorCode;
import api.common.dto.ResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.persistence.EntityNotFoundException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 전역 Exception Handler
 * */
@Slf4j
@RestControllerAdvice
public class ExceptionController{

    private final ObjectMapper objectMapper;

    public ExceptionController(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @ExceptionHandler(IOException.class)
    public ResponseEntity<ErrorResponseDto> handleIOException(IOException e) {
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
                ErrorResponseDto.builder()
                        .timeStamp(String.valueOf(LocalDateTime.now()))
                        .errorCode(e.getMessage())
                        .errorMessage(ApiErrorCode.FILE_NOT_FOUND.getMessage())
                        .build()
        );
    }

    @ExceptionHandler({
            NoMatchExtException.class,
            NoSuchElementException.class,
            EntityNotFoundException.class,
            NotFoundException.class,
            IllegalArgumentException.class,
            JwtException.class,
            IllegalStateException.class,
            DuplicateException.class,
    })
    public ResponseEntity<ErrorResponseDto> handleNoMatchExtException(RuntimeException e) {
        String paramCode = e.getMessage();
        ApiErrorCode apiErrorCode = ApiErrorCode.findByErrorCode(paramCode);

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        Enumeration<String> parameterList = request.getParameterNames();
        while(parameterList.hasMoreElements()){
            String parameter = parameterList.nextElement();
            log.error("Request Parameter Name : " + parameter + "\\tRequest Parameter Value : " + request.getParameter(parameter));
        }

        return ResponseEntity.status(apiErrorCode.getHttpStatus()).body(
                ErrorResponseDto.builder()
                        .timeStamp(String.valueOf(LocalDateTime.now()))
                        .errorCode(apiErrorCode.getErrorCode())
                        .errorMessage(apiErrorCode.getMessage()).build()
        );
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ResponseDto<Object>> handleRuntimeException(RuntimeException e){
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ResponseDto.builder()
                        .data(e.getMessage())
                .build()
        );
    }

    @ExceptionHandler(FeignException.class)
    public ResponseEntity<ErrorResponseDto> handlePaperConvertException(FeignException feignException){
        log.error(feignException.getMessage(), feignException);

        ApiErrorCode apiErrorCode = ApiErrorCode.UNKNOWN_CONVERSION_ERROR;

        try {
            String responseError = feignException.contentUTF8();
            Map<String, Object> responseMap = objectMapper.readValue(responseError, Map.class);
            ArrayList<String> detailMessage = (ArrayList<String>) responseMap.get("detailMessage");
            String errorMessage = detailMessage.stream().filter(message -> message.contains("[ERROR]")).collect(Collectors.joining("\\n"));

            return ResponseEntity.status(apiErrorCode.getHttpStatus()).body(
                    ErrorResponseDto.builder()
                            .timeStamp(String.valueOf(LocalDateTime.now()))
                            .errorCode(apiErrorCode.getErrorCode())
                            .errorMessage(errorMessage).build()
            );
        } catch (IOException  ex) { // json 파싱 실패

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
                    ErrorResponseDto.builder()
                            .timeStamp(String.valueOf(LocalDateTime.now()))
                            .errorCode(ApiErrorCode.OTHER_NETWORK_ERROR.getErrorCode())
                            .errorMessage(ex.getMessage()).build());
        }
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ResponseDto<Object>> exception(RuntimeException e){
        log.error(e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ResponseDto.builder()
                .data(e.getMessage())
                .build()
        );
    }
}