API 응답의 일관성을 위해 사용하는 공통 응답 객체입니다. 모든 API 엔드포인트에서 동일한 구조의 응답을 제공하여 프론트엔드에서 처리하기 쉽게 만들어줍니다.
@RestController
public class ScheduleController {
@GetMapping("/schedules/{id}")
public ResponseEntity<ScheduleResponseDto> getSchedule(@PathVariable Long id) {
return ResponseEntity.ok(scheduleService.getSchedule(id));
}
@PostMapping("/schedules")
public ResponseEntity<Map<String, Object>> createSchedule(@RequestBody ScheduleRequestDto request) {
ScheduleResponseDto schedule = scheduleService.createSchedule(request);
Map<String, Object> response = new HashMap<>();
response.put("message", "일정이 생성되었습니다.");
response.put("data", schedule);
return ResponseEntity.ok(response);
}
@DeleteMapping("/schedules/{id}")
public ResponseEntity<String> deleteSchedule(@PathVariable Long id) {
scheduleService.deleteSchedule(id);
return ResponseEntity.ok("일정이 삭제되었습니다.");
}
}
문제점:
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String timestamp;
// 생성자를 private으로 제한
private ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
// 성공 응답 생성 메서드들
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "성공", data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> successWithoutData() {
return new ApiResponse<>(true, "성공", null);
}
public static <T> ApiResponse<T> successWithoutData(String message) {
return new ApiResponse<>(true, message, null);
}
// 실패 응답 생성 메서드들
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
public static <T> ApiResponse<T> error(String message, T data) {
return new ApiResponse<>(false, message, data);
}
// Getter 메서드들
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public T getData() { return data; }
public String getTimestamp() { return timestamp; }
}
@RestController
@RequestMapping("/api/schedules")
public class ScheduleController {
private final ScheduleService scheduleService;
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<ScheduleResponseDto>> getSchedule(@PathVariable Long id) {
ScheduleResponseDto schedule = scheduleService.getSchedule(id);
return ResponseEntity.ok(ApiResponse.success(schedule));
}
@GetMapping
public ResponseEntity<ApiResponse<List<ScheduleResponseDto>>> getSchedules() {
List<ScheduleResponseDto> schedules = scheduleService.getAllSchedules();
return ResponseEntity.ok(ApiResponse.success("일정 목록 조회 성공", schedules));
}
@PostMapping
public ResponseEntity<ApiResponse<ScheduleResponseDto>> createSchedule(
@Valid @RequestBody ScheduleRequestDto request) {
ScheduleResponseDto schedule = scheduleService.createSchedule(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success("일정이 생성되었습니다.", schedule));
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<ScheduleResponseDto>> updateSchedule(
@PathVariable Long id,
@Valid @RequestBody ScheduleRequestDto request) {
ScheduleResponseDto schedule = scheduleService.updateSchedule(id, request);
return ResponseEntity.ok(ApiResponse.success("일정이 수정되었습니다.", schedule));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteSchedule(@PathVariable Long id) {
scheduleService.deleteSchedule(id);
return ResponseEntity.ok(ApiResponse.successWithoutData("일정이 삭제되었습니다."));
}
}
public class PageResponse<T> {
private List<T> content;
private PageInfo pageInfo;
public PageResponse(Page<T> page) {
this.content = page.getContent();
this.pageInfo = PageInfo.from(page);
}
@Getter
public static class PageInfo {
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean first;
private boolean last;
private PageInfo(int page, int size, long totalElements, int totalPages, boolean first, boolean last) {
this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.first = first;
this.last = last;
}
public static PageInfo from(Page<?> page) {
return new PageInfo(
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
);
}
}
}
// 페이징 응답 사용
@GetMapping
public ResponseEntity<ApiResponse<PageResponse<ScheduleResponseDto>>> getSchedulesWithPaging(
@PageableDefault(size = 10) Pageable pageable) {
Page<ScheduleResponseDto> schedulePage = scheduleService.getSchedulesWithPaging(pageable);
PageResponse<ScheduleResponseDto> pageResponse = new PageResponse<>(schedulePage);
return ResponseEntity.ok(ApiResponse.success("일정 목록 조회 성공", pageResponse));
}
public class ErrorDetail {
private String field;
private Object rejectedValue;
private String message;
public ErrorDetail(String field, Object rejectedValue, String message) {
this.field = field;
this.rejectedValue = rejectedValue;
this.message = message;
}
// getters...
}
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private List<ErrorDetail> errors;
private String timestamp;
// 유효성 검사 실패 응답
public static <T> ApiResponse<T> validationError(String message, List<ErrorDetail> errors) {
ApiResponse<T> response = new ApiResponse<>(false, message, null);
response.errors = errors;
return response;
}
public List<ErrorDetail> getErrors() { return errors; }
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleEntityNotFoundException(EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(DuplicateException.class)
public ResponseEntity<ApiResponse<Void>> handleDuplicateException(DuplicateException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException e) {
List<ErrorDetail> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ErrorDetail(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()
))
.collect(Collectors.toList());
return ResponseEntity.badRequest()
.body(ApiResponse.validationError("입력값 검증에 실패했습니다.", errors));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e) {
log.error("Unexpected error occurred", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("서버 내부 오류가 발생했습니다."));
}
}
// GET /api/schedules/1
{
"success": true,
"message": "성공",
"data": {
"id": 1,
"title": "회의",
"content": "팀 회의",
"createdAt": "2024-01-15T10:30:00"
},
"timestamp": "2024-01-15T10:30:00"
}
// POST /api/schedules
{
"success": true,
"message": "일정이 생성되었습니다.",
"data": {
"id": 2,
"title": "새 일정",
"content": "새로운 일정입니다",
"createdAt": "2024-01-15T11:00:00"
},
"timestamp": "2024-01-15T11:00:00"
}
// DELETE /api/schedules/1
{
"success": true,
"message": "일정이 삭제되었습니다.",
"data": null,
"timestamp": "2024-01-15T11:15:00"
}
// 404 Not Found
{
"success": false,
"message": "일정을 찾을 수 없습니다. ID: 999",
"data": null,
"timestamp": "2024-01-15T11:30:00"
}
// 400 Validation Error
{
"success": false,
"message": "입력값 검증에 실패했습니다.",
"data": null,
"errors": [
{
"field": "title",
"rejectedValue": "",
"message": "제목은 필수입니다."
},
{
"field": "content",
"rejectedValue": null,
"message": "내용은 필수입니다."
}
],
"timestamp": "2024-01-15T11:45:00"
}
// GET /api/schedules?page=0&size=5
{
"success": true,
"message": "일정 목록 조회 성공",
"data": {
"content": [
{
"id": 1,
"title": "일정 1",
"content": "내용 1",
"createdAt": "2024-01-15T10:00:00"
},
{
"id": 2,
"title": "일정 2",
"content": "내용 2",
"createdAt": "2024-01-15T11:00:00"
}
],
"pageInfo": {
"page": 0,
"size": 5,
"totalElements": 12,
"totalPages": 3,
"first": true,
"last": false
}
},
"timestamp": "2024-01-15T12:00:00"
}
// 공통 응답 타입 정의
interface ApiResponse<T> {
success: boolean;
message: string;
data: T | null;
errors?: ErrorDetail[];
timestamp: string;
}
interface ErrorDetail {
field: string;
rejectedValue: any;
message: string;
}
// API 호출 함수
async function fetchSchedule(id: number): Promise<Schedule> {
const response = await fetch(`/api/schedules/${id}`);
const apiResponse: ApiResponse<Schedule> = await response.json();
if (!apiResponse.success) {
throw new Error(apiResponse.message);
}
return apiResponse.data!;
}
// 에러 처리
async function createSchedule(request: ScheduleRequest): Promise<Schedule> {
try {
const response = await fetch('/api/schedules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
});
const apiResponse: ApiResponse<Schedule> = await response.json();
if (!apiResponse.success) {
if (apiResponse.errors) {
// 유효성 검사 에러 처리
apiResponse.errors.forEach(error => {
console.log(`${error.field}: ${error.message}`);
});
}
throw new Error(apiResponse.message);
}
return apiResponse.data!;
} catch (error) {
console.error('일정 생성 실패:', error);
throw error;
}
}
public enum ResponseCode {
SUCCESS("S001", "성공"),
CREATED("S002", "생성 성공"),
BAD_REQUEST("E001", "잘못된 요청"),
UNAUTHORIZED("E002", "인증 필요"),
FORBIDDEN("E003", "권한 없음"),
NOT_FOUND("E004", "리소스를 찾을 수 없음"),
CONFLICT("E005", "데이터 충돌"),
INTERNAL_SERVER_ERROR("E999", "서버 내부 오류");
private final String code;
private final String message;
ResponseCode(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() { return code; }
public String getMessage() { return message; }
}
// ApiResponse에 코드 추가
public class ApiResponse<T> {
private boolean success;
private String code;
private String message;
private T data;
private String timestamp;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, ResponseCode.SUCCESS.getCode(),
ResponseCode.SUCCESS.getMessage(), data);
}
public static <T> ApiResponse<T> error(ResponseCode responseCode) {
return new ApiResponse<>(false, responseCode.getCode(),
responseCode.getMessage(), null);
}
}
public class ApiResponse<T> {
public static class Builder<T> {
private boolean success;
private String code;
private String message;
private T data;
private List<ErrorDetail> errors;
public Builder<T> success(boolean success) {
this.success = success;
return this;
}
public Builder<T> message(String message) {
this.message = message;
return this;
}
public Builder<T> data(T data) {
this.data = data;
return this;
}
public ApiResponse<T> build() {
return new ApiResponse<>(this);
}
}
public static <T> Builder<T> builder() {
return new Builder<>();
}
}
// 사용 예시
ApiResponse<Schedule> response = ApiResponse.<Schedule>builder()
.success(true)
.message("일정 조회 성공")
.data(schedule)
.build();
Common Response DTO를 도입하면:
처음에는 간단하게 시작해서 점진적으로 기능을 확장해나가는 것이 좋습니다! 🚀