Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 대신 그대로 노출하지 않는다.

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,7 +25,7 @@ public class GlobalExceptionHandler {
* BaseException을 상속받는 모든 커스텀 예외 처리
*/
@ExceptionHandler(BaseException.class)
protected ResponseEntity<ApiResponse<Void>> handleBaseException(BaseException e) {
protected ResponseEntity<ErrorResponse> handleBaseException(BaseException e) {
HttpStatus status = e.getErrorCode().getStatus();
// 5xx(서버/외부연동 장애)만 스택트레이스와 함께 error, 4xx(예상된 규칙 위반)는 warn
if (status.is5xxServerError()) {
Expand All @@ -35,54 +35,54 @@ protected ResponseEntity<ApiResponse<Void>> 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<ApiResponse<Void>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleBindException(BindException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
protected ResponseEntity<ErrorResponse> 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) {
Expand All @@ -99,54 +99,54 @@ private String getMissingParameterMessage(String parameterName) {
* HTTP 메시지 읽기 실패 예외 처리
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
protected ResponseEntity<ApiResponse<Void>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleNoResourceFound(NoResourceFoundException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleNoHandlerFoundException(NoHandlerFoundException e) {
protected ResponseEntity<ErrorResponse> 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<ApiResponse<Void>> handleException(Exception e) {
protected ResponseEntity<ErrorResponse> 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));
}
}
48 changes: 4 additions & 44 deletions src/main/java/com/matzip/common/response/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -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<T> {

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 <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.status("OK")
.timestamp(LocalDateTime.now())
.data(data)
.build();
}
Expand All @@ -44,7 +34,6 @@ public static <T> ApiResponse<T> success(T data) {
public static <T> ApiResponse<T> successWithoutData() {
return ApiResponse.<T>builder()
.status("OK")
.timestamp(LocalDateTime.now())
// data 필드는 null로 유지
.build();
}
Expand All @@ -55,36 +44,7 @@ public static <T> ApiResponse<T> successWithoutData() {
public static <T> ApiResponse<T> successWithEmptyList() {
return ApiResponse.<T>builder()
.status("OK")
.timestamp(LocalDateTime.now())
.data((T) emptyList()) // data 필드에 빈 리스트를 명시적으로 할당
.build();
}

/**
* 실패 응답 생성 (ErrorCode 사용)
*/
public static <T> ApiResponse<T> error(ErrorCode errorCode) {
return error(errorCode, errorCode.getMessage());
}

/**
* 실패 응답 생성 (ErrorCode + 상세 메시지)
*/
public static <T> ApiResponse<T> error(ErrorCode errorCode, String detailMessage) {
return ApiResponse.<T>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;
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/matzip/common/response/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -124,7 +124,7 @@ private void handleJwtException(HttpServletResponse response, JwtProvider.TokenV
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");

ApiResponse<Void> apiResponse = ApiResponse.error(errorCode);
ErrorResponse apiResponse = ErrorResponse.error(errorCode);
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
response.getWriter().write(jsonResponse);
}
Expand All @@ -133,7 +133,7 @@ private void handleUnauthorized(HttpServletResponse response) throws IOException
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");

ApiResponse<Void> apiResponse = ApiResponse.error(ErrorCode.UNAUTHORIZED);
ErrorResponse apiResponse = ErrorResponse.error(ErrorCode.UNAUTHORIZED);
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
response.getWriter().write(jsonResponse);
}
Expand Down
Loading