Skip to content

Commit e102c1d

Browse files
committed
feat: custom response body 정의
1 parent 1cfeaa3 commit e102c1d

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.loopers.interfaces.api;
2+
3+
import com.fasterxml.jackson.databind.JsonMappingException;
4+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
5+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
6+
import com.loopers.support.error.CoreException;
7+
import com.loopers.support.error.ErrorType;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.http.converter.HttpMessageNotReadableException;
11+
import org.springframework.web.bind.MissingServletRequestParameterException;
12+
import org.springframework.web.bind.annotation.ExceptionHandler;
13+
import org.springframework.web.bind.annotation.RestControllerAdvice;
14+
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
15+
import org.springframework.web.server.ServerWebInputException;
16+
import org.springframework.web.servlet.resource.NoResourceFoundException;
17+
18+
import java.util.Arrays;
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
import java.util.stream.Collectors;
22+
23+
@RestControllerAdvice
24+
@Slf4j
25+
public class ApiControllerAdvice {
26+
@ExceptionHandler
27+
public ResponseEntity<ApiResponse<?>> handle(CoreException e) {
28+
log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e);
29+
return failureResponse(e.getErrorType(), e.getCustomMessage());
30+
}
31+
32+
@ExceptionHandler
33+
public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatchException e) {
34+
String name = e.getName();
35+
String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown";
36+
String value = e.getValue() != null ? e.getValue().toString() : "null";
37+
String message = String.format("요청 파라미터 '%s' (타입: %s)의 값 '%s'이(가) 잘못되었습니다.", name, type, value);
38+
return failureResponse(ErrorType.BAD_REQUEST, message);
39+
}
40+
41+
@ExceptionHandler
42+
public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestParameterException e) {
43+
String name = e.getParameterName();
44+
String type = e.getParameterType();
45+
String message = String.format("필수 요청 파라미터 '%s' (타입: %s)가 누락되었습니다.", name, type);
46+
return failureResponse(ErrorType.BAD_REQUEST, message);
47+
}
48+
49+
@ExceptionHandler
50+
public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e) {
51+
String errorMessage;
52+
Throwable rootCause = e.getRootCause();
53+
54+
if (rootCause instanceof InvalidFormatException invalidFormat) {
55+
String fieldName = invalidFormat.getPath().stream()
56+
.map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
57+
.collect(Collectors.joining("."));
58+
59+
String valueIndicationMessage = "";
60+
if (invalidFormat.getTargetType().isEnum()) {
61+
Class<?> enumClass = invalidFormat.getTargetType();
62+
String enumValues = Arrays.stream(enumClass.getEnumConstants())
63+
.map(Object::toString)
64+
.collect(Collectors.joining(", "));
65+
valueIndicationMessage = "사용 가능한 값 : [" + enumValues + "]";
66+
}
67+
68+
String expectedType = invalidFormat.getTargetType().getSimpleName();
69+
Object value = invalidFormat.getValue();
70+
71+
errorMessage = String.format("필드 '%s'의 값 '%s'이(가) 예상 타입(%s)과 일치하지 않습니다. %s",
72+
fieldName, value, expectedType, valueIndicationMessage);
73+
74+
} else if (rootCause instanceof MismatchedInputException mismatchedInput) {
75+
String fieldPath = mismatchedInput.getPath().stream()
76+
.map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
77+
.collect(Collectors.joining("."));
78+
errorMessage = String.format("필수 필드 '%s'이(가) 누락되었습니다.", fieldPath);
79+
80+
} else if (rootCause instanceof JsonMappingException jsonMapping) {
81+
String fieldPath = jsonMapping.getPath().stream()
82+
.map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
83+
.collect(Collectors.joining("."));
84+
errorMessage = String.format("필드 '%s'에서 JSON 매핑 오류가 발생했습니다: %s",
85+
fieldPath, jsonMapping.getOriginalMessage());
86+
87+
} else {
88+
errorMessage = "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요.";
89+
}
90+
91+
return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
92+
}
93+
94+
@ExceptionHandler
95+
public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e) {
96+
String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : "");
97+
if (!missingParams.isEmpty()) {
98+
String message = String.format("필수 요청 값 '%s'가 누락되었습니다.", missingParams);
99+
return failureResponse(ErrorType.BAD_REQUEST, message);
100+
} else {
101+
return failureResponse(ErrorType.BAD_REQUEST, null);
102+
}
103+
}
104+
105+
@ExceptionHandler
106+
public ResponseEntity<ApiResponse<?>> handleNotFound(NoResourceFoundException e) {
107+
return failureResponse(ErrorType.NOT_FOUND, null);
108+
}
109+
110+
@ExceptionHandler
111+
public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
112+
log.error("Exception : {}", e.getMessage(), e);
113+
return failureResponse(ErrorType.INTERNAL_ERROR, null);
114+
}
115+
116+
private String extractMissingParameter(String message) {
117+
Pattern pattern = Pattern.compile("'(.+?)'");
118+
Matcher matcher = pattern.matcher(message);
119+
return matcher.find() ? matcher.group(1) : "";
120+
}
121+
122+
private ResponseEntity<ApiResponse<?>> failureResponse(ErrorType errorType, String errorMessage) {
123+
return ResponseEntity.status(errorType.getStatus())
124+
.body(ApiResponse.fail(errorType.getCode(), errorMessage != null ? errorMessage : errorType.getMessage()));
125+
}
126+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.loopers.interfaces.api;
2+
3+
public record ApiResponse<T>(Metadata meta, T data) {
4+
public record Metadata(Result result, String errorCode, String message) {
5+
public enum Result {
6+
SUCCESS, FAIL
7+
}
8+
9+
public static Metadata success() {
10+
return new Metadata(Result.SUCCESS, null, null);
11+
}
12+
13+
public static Metadata fail(String errorCode, String errorMessage) {
14+
return new Metadata(Result.FAIL, errorCode, errorMessage);
15+
}
16+
}
17+
18+
public static ApiResponse<Object> success() {
19+
return new ApiResponse<>(Metadata.success(), null);
20+
}
21+
22+
public static <T> ApiResponse<T> success(T data) {
23+
return new ApiResponse<>(Metadata.success(), data);
24+
}
25+
26+
public static ApiResponse<Object> fail(String errorCode, String errorMessage) {
27+
return new ApiResponse<>(
28+
Metadata.fail(errorCode, errorMessage),
29+
null
30+
);
31+
}
32+
}

0 commit comments

Comments
 (0)