Spring이 기본적으로 제공하는 ResponseEntity
를 응답으로 사용해도 문제 없지만, 커스텀 응답을 만들어 사용하면 일관된 API 응답 형식을 사용할 수 있어 자주 사용한다.
장점
단점
ResponseEntity
로 래핑된 구조가 아니면 HttpStatus
코드나 헤더 설정을 직접 관리해야 한다.커스텀 응답 클래스를 사용하기로 했으면 ResponseEntity
를 같이 사용할 지 결정해야 한다.
ResponseEntity를 사용하지 않고 별도 클래스로 사용하기
return new ApiResponse(HttpStatus.OK.value(), "성공", data);
코드가 간결하고 응답 구조가 일관적이다.
별도의 처리가 없으면 ApiResponse
에 statusCode
를 넣는다고 실제로 상태코드가 변하진 않는다.
응답의 상태코드, 헤더 조작을 위해 추가적인 처리가 필요하다.
ResponseEntity 자체를 배제하지는 않고 내부적으로 활용하는 경우
return ResponseEntity.ok(new ApiResponse<>(HttpStatus.OK.value(), "성공", data));
상태코드, 헤더를 ResponseEntity
로 설정할 수 있다.
ResponseEntity
가 표준이긴 하지만, 개인적으로 헤더까지 활용하지 않는 경우에는 단일로 사용하는 방식이 나은 것 같다.
ResponseEntity
를 같이 쓰면 상태 코드 정보가 중복되는게 가장 큰 이유인데, 개발자의 실수로 응답 body에 포함된 statusCode
와 실제 응답 코드가 달라질 수도 있고 실수로 ApiResponse
에 잘못 적어도 알아차리기 어렵다.
또, ResponseEntity.noContent()
를 사용하면 body가 없어서 204 NO CONTENT
로 응답할 땐 응답 형식이 유지되지 않는다.
물론, 정해진 답은 없으므로 더 좋아 보이는 방식을 선택하면 된다. 커스텀 응답에ResponseEntity
를 활용하는 것도 좋은 절충안일 수 있다.
@Getter
@AllArgsConstructor
public class ApiResponse<T> {
@NonNull
private int statusCode;
@NonNull
private String message;
@NonNull
private T data;
public ApiResponse(int statusCode, String message) {
this(statusCode, message, (T) new Empty());
}
}
커스텀 응답 클래스는 필요에 맞게 설정하면 된다.
일반적으로 상태코드, 메시지, 응답에 포함될 데이터를 추가한다.
제네릭을 사용하면 응답 시에 잘못된 타입을 정적 분석이나 컴파일 에러로 쉽게 찾을 수 있어서 좋다.
statusCode
를 문자열로 사용해서 "200-1", "200-2" 같이 같은 OK 응답이라도 경우에 따라 구분하도록 할 수도 있다.
응답 데이터가 없는 경우 null
을 응답하도록 해도 되지만, 응답할 내용이 없는 클래스로 대체해 응답에 data 자체가 나오지 않게 했다.
public class Empty {
}
응답할 때 ResponseEntity
를 사용하지 않으면 실제 상태 코드는 항상 200으로 응답된다.
Aspect로 ApiResponse.statusCode
에 따라 응답의 상태 코드를 변경할 수 있다.
@Aspect
@Component
@RequiredArgsConstructor
public class ApiResponseAspect {
private final HttpServletResponse response;
@Around("""
(
within
(
@org.springframework.web.bind.annotation.RestController *
)
&&
(
@annotation(org.springframework.web.bind.annotation.GetMapping)
||
@annotation(org.springframework.web.bind.annotation.PostMapping)
||
@annotation(org.springframework.web.bind.annotation.PutMapping)
||
@annotation(org.springframework.web.bind.annotation.DeleteMapping)
)
)
||
@annotation(org.springframework.web.bind.annotation.ResponseBody)
""")
public Object responseAspect(ProceedingJoinPoint joinPoint) throws Throwable {
Object rst = joinPoint.proceed();
if(rst instanceof ApiResponse apiResponse) {
int statusCode = apiResponse.getStatusCode();
response.setStatus(statusCode);
}
return rst;
}
}
이제 ApiResponse.statusCode
에 따라 실제 응답 코드가 설정된다.
예외 핸들링에도, 핸들러에서 커스텀 응답으로 응답하도록 설정하면 오류 응답도 일관적으로 처리할 수 있다.