1. 통일된 응답 포맷 만들기 : ApiResponse<T>
API 마다 응답 형태가 다르면 프론트 개발자는 매번
if (response.data) {...}
else if (response.content) {...}
이런 식으로 응답을 파싱해야 한다.
그래서 프로젝트에서는 모든 응답을 하나의 포맷으로 통일하기로 했다.
{
"status" : 200,
"message" : "성공 메시지",
"data" : {...}
}
해결 방법 : 제네릭 기반 응답 래퍼 만들기
@AllArgsConstructor
public class ApiResponse<T> {
private int status;
private String message;
private T data;
public static <T> ApiResponse<T> success(int status, String message, T data) {
return new ApiResponse<>(status, message, data);
}
public static <T> ApiResponse<T> error(int status, String message) {
return new ApiResponse<>(status, message, null);
}
}
응답 유틸 클래스 : ResponseUtil
public class ResponseUtil {
public static <T> ResponseEntity<ApiResponse<T>> ok(String message, T data) {
return ResponseEntity.ok(ApiResponse.success(200, message, data));
}
public static <T> ResponseEntity<ApiResponse<T>> created(String message, T data) {
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(201, message, data));
}
public static ResponseEntity<ApiResponse<Void>> noContent() {
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResponse.success(204, "처리 완료", null));
}
public static ResponseEntity<ApiResponse<Void>> error(int status, String message) {
return ResponseEntity.status(status).body(ApiResponse.error(status, message));
}
}
사용 예시
return ResponseUtil.ok("일정 조회 성공", dto);
return ResponseUtil.created("일정 등록 성공", dto);
return ResponseUtil.noContent();
2. 공통 페이징 DTO 만들기
문제
기본 Page<T>를 그대로 응답하면 pageable, sort, empty 등 너무 많은 필드가 붙는다.
{
"content": [ ... ],
"pageable": { "sort": { ... }, ... },
"totalElements": 150,
"totalPages": 15,
"last": false,
...
}
그래서 필요한 정보만 따로 담는 Page용 DTO를 만들기로 했다.
public class PageUtil<T> {
private List<T> content;
private int number;
private int size;
private int totalPages;
private boolean isLast;
public PageUtil(Page<T> pageData) {
this.content = pageData.getContent(); // 실제 데이터 리스트
this.number = pageData.getNumber(); // 현재 페이지 번호
this.size = pageData.getSize(); // 페이지 당 개수
this.totalPages = pageData.getTotalPages(); // 전체 페이지 수
this.isLast = pageData.isLast(); // 마지막 페이지 여부
}
}
동작흐름
Controller -> Service -> Repository
↓
Page<DTO> 결과 반환
↓
PageUtil<T>로 감싸기
↓
ApiResponse.success(...)로 통일된 포맷으로 응답
{
"status": 200,
"message": "조회 성공",
"data": {
"content": [ ... ],
"number": 0,
"size": 10,
"totalPages": 3,
"isLast": false
}
}
3. 전역 예외 처리 + ErrorCode 관리
서비스나 컨트롤러에서 예외가 터지면 기본적으로는 이런 HTML 응답이 날라온다.
Whitelabel Error Page
This application has no explicit mapping for /error...
또는 500 에러만 떨어지고 왜 실패했는지 알기 어렵다.
그래서 예외처리 또한 일관된 JSON 에러 응답을 주기로 했다,
해결 전략
1. ErrorCode enum : 상태 코드 + 메시지 일괄 관리
2. CustomException : enum 기반 예외 객체
3. @RestControllerAdvice : 모든 예외를 JSON으로 변환
EnumCode
@Getter
@AllArgsConstructor
public enum ErrorCode {
// 공통
INTERNAL_SERVER_ERROR(500, "서버 내부 오류가 발생했습니다."),
INVALID_INPUT_VALUE(400, "입력값이 올바르지 않습니다."),
// 유저
NOT_FOUND_EMAIL(404, "이메일이 존재하지 않습니다."),
NOT_FOUND_USER(404, "해당 유저를 찾을 수 없습니다."),
PASSWORD_IS_NOT_VALID(403, "비밀번호가 일치하지 않습니다."),
// 일정
NOT_FOUND_SCHEDULE(404, "해당 일정을 찾을 수 없습니다."),
NOT_SCHEDULE_OWNER(403, "해당 일정에 대한 권한이 없습니다."),
// 댓글
NOT_FOUND_COMMENT(404, "해당 댓글을 찾을 수 없습니다."),
REPLY_DEPTH_EXCEEDED(400, "대댓글에는 다시 댓글을 달 수 없습니다."),
NOT_COMMENT_OWNER(403, "해당 일정에 대한 권한이 없습니다.");
private final int status;
private final String message;
}
예외 클래스
@Getter
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
예외 처리기
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<ApiResponse<Void>> handleCustomException(CustomException e) {
ErrorCode code = e.getErrorCode();
return ResponseUtil.error(code.getStatus(), code.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
e.printStackTrace();
return ResponseUtil.error(500, "서버 내부 오류가 발생했습니다");
}
}
동작 흐름
Service 내부에서 예외 발생
→ throw new CustomException(ErrorCode.NOT_FOUND_SCHEDULE);
예외 발생
↓
@ExceptionHandler(CustomException.class) 포착
↓
에러 코드 → status + message 추출
↓
ApiResponse.error(...)로 응답 포장
↓
프론트로 JSON 에러 응답 전송
실제 응답 예시
정상 조회
{
"status": 200,
"message": "일정 조회 성공",
"data": {
"id": 1,
"title": "캠핑 일정",
"author": "sungho"
}
}
예외 발생 시
{
"status": 404,
"message": "해당 일정을 찾을 수 없습니다.",
"data": null
}'Project' 카테고리의 다른 글
| 복합 유니크 제약 조건의 필요성 (0) | 2025.04.04 |
|---|---|
| Spring Security와 JWT로 로그인 구현하기 (0) | 2025.04.03 |
| 결제 시나리오 3. 악의적 선점 - 데이터 정합성 문제(2) (0) | 2025.03.17 |
| 결제 시나리오 3. 악의적 선점 - 데이터 정합성 문제(1) (0) | 2025.03.17 |
| 결제 시나리오 3. 악의적 예약 선점 (0) | 2025.03.17 |