diff --git a/CLAUDE.md b/CLAUDE.md index dbe611c..08f323c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ controller -> service -> repository -> DB ### DTO -- 모든 DTO는 `Dto` 접미사를 붙인다. 유일한 예외는 응답 래퍼 `ApiResponse`. +- 모든 DTO는 `Dto` 접미사를 붙인다. 유일한 예외는 응답 래퍼 `ApiResponse`, `ErrorResponse`다. - HTTP 요청/응답 DTO는 `dto/request`·`dto/response`. 요청/응답이 공유하는 조각은 `dto/` 루트. 외부 API 응답 파싱 DTO는 해당 `client/.../dto`. - 프레임워크·엔티티 타입을 응답 DTO 대신 그대로 노출하지 않는다. diff --git a/src/main/java/com/matzip/common/exception/GlobalExceptionHandler.java b/src/main/java/com/matzip/common/exception/GlobalExceptionHandler.java index 1493077..d881444 100644 --- a/src/main/java/com/matzip/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/matzip/common/exception/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ package com.matzip.common.exception; import com.matzip.common.exception.code.ErrorCode; -import com.matzip.common.response.ApiResponse; +import com.matzip.common.response.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -25,7 +25,7 @@ public class GlobalExceptionHandler { * BaseException을 상속받는 모든 커스텀 예외 처리 */ @ExceptionHandler(BaseException.class) - protected ResponseEntity> handleBaseException(BaseException e) { + protected ResponseEntity handleBaseException(BaseException e) { HttpStatus status = e.getErrorCode().getStatus(); // 5xx(서버/외부연동 장애)만 스택트레이스와 함께 error, 4xx(예상된 규칙 위반)는 warn if (status.is5xxServerError()) { @@ -35,54 +35,54 @@ protected ResponseEntity> handleBaseException(BaseException e) } return ResponseEntity .status(status) - .body(ApiResponse.error(e.getErrorCode(), e.getMessage())); + .body(ErrorResponse.error(e.getErrorCode(), e.getMessage())); } /** * @Valid 검증 실패 예외 처리 */ @ExceptionHandler(MethodArgumentNotValidException.class) - protected ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.warn("MethodArgumentNotValidException: {}", e.getMessage()); String detail = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); return ResponseEntity .status(ErrorCode.VALIDATION_ERROR.getStatus()) - .body(ApiResponse.error(ErrorCode.VALIDATION_ERROR, detail)); + .body(ErrorResponse.error(ErrorCode.VALIDATION_ERROR, detail)); } /** * BindException 처리 */ @ExceptionHandler(BindException.class) - protected ResponseEntity> handleBindException(BindException e) { + protected ResponseEntity handleBindException(BindException e) { log.warn("BindException: {}", e.getMessage()); String detail = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); return ResponseEntity .status(ErrorCode.VALIDATION_ERROR.getStatus()) - .body(ApiResponse.error(ErrorCode.VALIDATION_ERROR, detail)); + .body(ErrorResponse.error(ErrorCode.VALIDATION_ERROR, detail)); } /** * HTTP 메서드 지원하지 않는 예외 처리 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - protected ResponseEntity> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + protected ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.warn("HttpRequestMethodNotSupportedException: {}", e.getMessage()); return ResponseEntity .status(ErrorCode.METHOD_NOT_ALLOWED.getStatus()) - .body(ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED)); + .body(ErrorResponse.error(ErrorCode.METHOD_NOT_ALLOWED)); } /** * 요청 파라미터 누락 예외 처리 */ @ExceptionHandler(MissingServletRequestParameterException.class) - protected ResponseEntity> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { + protected ResponseEntity handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { log.warn("MissingServletRequestParameterException: {}", e.getMessage()); String message = getMissingParameterMessage(e.getParameterName()); return ResponseEntity .status(ErrorCode.INVALID_INPUT_VALUE.getStatus()) - .body(ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE, message)); + .body(ErrorResponse.error(ErrorCode.INVALID_INPUT_VALUE, message)); } private String getMissingParameterMessage(String parameterName) { @@ -99,54 +99,54 @@ private String getMissingParameterMessage(String parameterName) { * HTTP 메시지 읽기 실패 예외 처리 */ @ExceptionHandler(HttpMessageNotReadableException.class) - protected ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + protected ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { log.warn("HttpMessageNotReadableException: {}", e.getMessage()); return ResponseEntity .status(ErrorCode.INVALID_INPUT_VALUE.getStatus()) - .body(ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE, "잘못된 요청 본문입니다.")); + .body(ErrorResponse.error(ErrorCode.INVALID_INPUT_VALUE, "잘못된 요청 본문입니다.")); } /** * 메서드 인자 타입 불일치 예외 처리 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) - protected ResponseEntity> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { log.warn("MethodArgumentTypeMismatchException: {}", e.getMessage()); return ResponseEntity .status(ErrorCode.INVALID_TYPE_VALUE.getStatus()) - .body(ApiResponse.error(ErrorCode.INVALID_TYPE_VALUE, e.getMessage())); + .body(ErrorResponse.error(ErrorCode.INVALID_TYPE_VALUE, e.getMessage())); } /** * 매핑/정적 리소스 모두 없을 때 */ @ExceptionHandler(NoResourceFoundException.class) - protected ResponseEntity> handleNoResourceFound(NoResourceFoundException e) { + protected ResponseEntity handleNoResourceFound(NoResourceFoundException e) { log.warn("NoResourceFoundException: {}", e.getMessage()); return ResponseEntity .status(ErrorCode.NOT_FOUND.getStatus()) - .body(ApiResponse.error(ErrorCode.NOT_FOUND, ErrorCode.NOT_FOUND.getMessage())); + .body(ErrorResponse.error(ErrorCode.NOT_FOUND, ErrorCode.NOT_FOUND.getMessage())); } /** * 핸들러를 찾을 수 없는 예외 처리 */ @ExceptionHandler(NoHandlerFoundException.class) - protected ResponseEntity> handleNoHandlerFoundException(NoHandlerFoundException e) { + protected ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException e) { log.warn("NoHandlerFoundException: {}", e.getMessage()); return ResponseEntity .status(ErrorCode.INVALID_INPUT_VALUE.getStatus()) - .body(ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE, "요청한 리소스를 찾을 수 없습니다.")); + .body(ErrorResponse.error(ErrorCode.INVALID_INPUT_VALUE, "요청한 리소스를 찾을 수 없습니다.")); } /** * 기타 모든 예외 처리 */ @ExceptionHandler(Exception.class) - protected ResponseEntity> handleException(Exception e) { + protected ResponseEntity handleException(Exception e) { log.error("Unexpected Exception: {}", e.getMessage(), e); return ResponseEntity .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) - .body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR)); + .body(ErrorResponse.error(ErrorCode.INTERNAL_SERVER_ERROR)); } } diff --git a/src/main/java/com/matzip/common/response/ApiResponse.java b/src/main/java/com/matzip/common/response/ApiResponse.java index 23551d5..ce6433e 100644 --- a/src/main/java/com/matzip/common/response/ApiResponse.java +++ b/src/main/java/com/matzip/common/response/ApiResponse.java @@ -1,38 +1,28 @@ package com.matzip.common.response; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; -import com.matzip.common.exception.code.ErrorCode; import lombok.Builder; import lombok.Getter; -import java.time.LocalDateTime; - import static java.util.Collections.*; + @Getter @Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class ApiResponse { - + private final String status; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private final LocalDateTime timestamp; - + @JsonInclude(JsonInclude.Include.ALWAYS) private final T data; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private final ErrorInfo error; - + /** * 성공 응답 생성 */ public static ApiResponse success(T data) { return ApiResponse.builder() .status("OK") - .timestamp(LocalDateTime.now()) .data(data) .build(); } @@ -44,7 +34,6 @@ public static ApiResponse success(T data) { public static ApiResponse successWithoutData() { return ApiResponse.builder() .status("OK") - .timestamp(LocalDateTime.now()) // data 필드는 null로 유지 .build(); } @@ -55,36 +44,7 @@ public static ApiResponse successWithoutData() { public static ApiResponse successWithEmptyList() { return ApiResponse.builder() .status("OK") - .timestamp(LocalDateTime.now()) .data((T) emptyList()) // data 필드에 빈 리스트를 명시적으로 할당 .build(); } - - /** - * 실패 응답 생성 (ErrorCode 사용) - */ - public static ApiResponse error(ErrorCode errorCode) { - return error(errorCode, errorCode.getMessage()); - } - - /** - * 실패 응답 생성 (ErrorCode + 상세 메시지) - */ - public static ApiResponse error(ErrorCode errorCode, String detailMessage) { - return ApiResponse.builder() - .status("ERROR") - .timestamp(LocalDateTime.now()) - .error(ErrorInfo.builder() - .code(errorCode.name()) - .message(detailMessage) - .build()) - .build(); - } - - @Getter - @Builder - public static class ErrorInfo { - private final String code; - private final String message; - } } diff --git a/src/main/java/com/matzip/common/response/ErrorResponse.java b/src/main/java/com/matzip/common/response/ErrorResponse.java new file mode 100644 index 0000000..7971ff9 --- /dev/null +++ b/src/main/java/com/matzip/common/response/ErrorResponse.java @@ -0,0 +1,38 @@ +package com.matzip.common.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.matzip.common.exception.code.ErrorCode; +import lombok.Builder; +import lombok.Getter; + + +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private final String status; + + private final ErrorInfo error; + + public static ErrorResponse error(ErrorCode errorCode) { + return error(errorCode, errorCode.getMessage()); + } + + public static ErrorResponse error(ErrorCode errorCode, String detailMessage) { + return ErrorResponse.builder() + .status("ERROR") + .error(ErrorInfo.builder() + .code(errorCode.name()) + .message(detailMessage) + .build()) + .build(); + } + + @Getter + @Builder + public static class ErrorInfo { + private final String code; + private final String message; + } +} diff --git a/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java index bec7983..589af25 100644 --- a/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/matzip/common/security/jwt/filter/JwtAuthenticationFilter.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.matzip.common.exception.code.ErrorCode; -import com.matzip.common.response.ApiResponse; +import com.matzip.common.response.ErrorResponse; import com.matzip.common.security.jwt.JwtProvider; import com.matzip.common.security.UserPrincipal; import io.jsonwebtoken.JwtException; @@ -124,7 +124,7 @@ private void handleJwtException(HttpServletResponse response, JwtProvider.TokenV response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); - ApiResponse apiResponse = ApiResponse.error(errorCode); + ErrorResponse apiResponse = ErrorResponse.error(errorCode); String jsonResponse = objectMapper.writeValueAsString(apiResponse); response.getWriter().write(jsonResponse); } @@ -133,7 +133,7 @@ private void handleUnauthorized(HttpServletResponse response) throws IOException response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); - ApiResponse apiResponse = ApiResponse.error(ErrorCode.UNAUTHORIZED); + ErrorResponse apiResponse = ErrorResponse.error(ErrorCode.UNAUTHORIZED); String jsonResponse = objectMapper.writeValueAsString(apiResponse); response.getWriter().write(jsonResponse); }