공통 응답 처리는 애플리케이션의 응답을 일관되게 관리하여 클라이언트와의 상호작용을 더 효율적으로 만들기 위해 중요하다.
이를 통해 얻을 수 있는 주요 장점과 필요성은 다음과 같다.
응답 형식의 일관성: 모든 API 응답이 동일한 형식을 따르도록 함으로써 서버를 호출하고 사용해야 하는 클라이언트가 응답을 예측하고 쉽게 처리할 수 있다.
만약 응답이 다 다르다면 해당 서버의 특정 API를 호출할 때마다 프론트 엔드 개발자 혹은 클라이언트는 응답/요청 데이터 형식을 확인하기 위해 API 문서를 다 확인해야할 수도 있다.
공통 API 응답의 형식으로는 예를 들어, 모든 응답에 일관되게 status, message, data 필드를 포함시킬 수 있다.
{
"status": "success",
"message": "요청 성공",
"data": { ... }
}
{
"status": "error",
"message": "에러 발생",
"data": null
}
public class MemberService {
private final CourseRepository courseRepository;
private final MemberJpaRepository memberRepository;
/**
* 회원 저장 서비스
*/
public MemberCreateResponseDto createMember(MemberCreateRequestDto data) {
// 조회: 수업 아이디로 수업 조회
Optional<Course> foundCourseOptional = courseRepository.findById(data.getCourseId());
Course foundCourse = foundCourseOptional.get();
// 생성: 회원 엔티티
Member newMember = Member.createNewMember(
foundCourse,
data.getEmail(),
data.getName()
);
// 저장: 회원
Member savedMember = memberRepository.save(newMember);
// --- 공통 API 응답 부재로 서비스에서 직접 반환할 응답(상태 코드, message, 데이터 등) 생성(모든 메서드마다 응답 생성으로 인한 코드 중복도 발생) ---
// 반환 응답
return new MemberCreateResponseDto(
"created",
201,
savedMember.getId()
);
}
이제 직접 공통 응답으로 사용할 ApiResponse라는 Dto 클래스를 정의해보자.
ApiResponse.java
@Getter
@AllArgsConstructor
public class ApiResponse<T> {
private final String message;
private final Integer statusCode;
// 데이터가 Dto로 들어오는데 제네릭이기 때문에 어떤 Dto가 들어오든 상관 X
private final T data;
/**
* 성공 응답 생성
*/
public static <T> ApiResponse<T> createSuccess(String message, Integer statusCode, T data) { // 요청 성공시 응답 메서드
return new ApiResponse<>(message, statusCode, data);
}
/**
* 에러 응답 생성
*/
public static <T> ApiResponse<T> createError(String message, Integer statusCode) { // 요청 실패시 응답 메서드
return new ApiResponse<>(message, statusCode, null);
}
}
공통으로 사용되는 ResponseDto 클래스인 ApiReponse를 만들고
/**
* 수업 목록 조회 응답 DTO
*/
@Getter
@AllArgsConstructor
public class CourseListResponseDto {
// private String message;
// private Integer statusCode;
List<CourseDto> courseDtoList;
}
기존 Dto에 정의되어 있던 Http 응답 관련 필드들은 이제 공통 응답 Dto에 정의되어 있기 때문에 삭제(주석처리)하고
@Slf4j
@Service
@RequiredArgsConstructor
public class CourseService {
private final CourseRepository courseRepository;
/**
* 수업 생성 서비스
*/
public CourseCreateResponseDto createCourse(CourseCreateRequestDto data) {
// 검증: 수업 이름이 중복은 허용 안하겠다.
boolean flag = courseRepository.existsByNameAndIsDeleted(data.getName(), false);
if (flag) {
throw new DuplicateCourseNameException();
}
// 생성: 수업(course) 엔티티
Course newCourse = Course.createNewCourse(data.getName());
// 저장: 코스 저장
Course savedCourse = courseRepository.save(newCourse);
// ------ API 공통 응답 처리로 코드 변화된 부분 ------
// 공통 응답으로 처리하기로 했기 때문에 HTTP 응답 관련 코드를 주석 처리
return new CourseCreateResponseDto(
savedCourse.getId()
// "created",
// 201
);
}
}
서비스 계층에서도 상태 코드, message 등 이제 더 이상 반환할 필요 없는 부분을 제거한다.
@RestController
@RequestMapping("/course")
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
/**
* ---- API 공통 응답 적용 전 수업(course) 생성 API ----
*/
// @PostMapping
// public CourseCreateResponseDto createCourseAPI(@RequestBody CourseCreateRequestDto request) {
// return courseService.createCourse(request);
// }
/**
* ---- API 공통 응답 적용 후 수업(course) 생성 API ----
*/
@PostMapping
public ApiResponse<CourseCreateResponseDto> createCourseAPI(@RequestBody CourseCreateRequestDto request) {
CourseCreateResponseDto courseCreateResponseDto = courseService.createCourse(request);
return ApiResponse.createSuccess(
HttpStatus.CREATED.getReasonPhrase(), // (HTTP) message
HttpStatus.CREATED.value(), // (HTTP) status code
courseCreateResponseDto // (응답) 데이터
);
}
}
이제 Controller에서 ApiResponse에 정의된 message, statusCode, data를 채운 후 응답으로 반환하면
{
"message" : "Created",
"statusCode" : "201",
"data" : {
"courseId" : 1
}
}
위의 예시와 같은 형식으로 출력되는 것을 확인할 수 있을 것이다. 해당 메서드만 그런 게 아니라 컨트롤러에서 ApiResponse으로 Dto를 감싸서 반환한 모든 메서드에서 동일한 형식으로 출력된다.
[ApiResponse 적용한 예외 처리]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateCourseNameException.class)
public ApiResponse<?> handleCourseNameException(DuplicateCourseNameException e) {
// return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
return ApiResponse.createError(e.getMessage(), HttpStatus.BAD_REQUEST.value());
}
}
{
"message" : "수업 이름 중복",
"statusCode" : 400,
"data" : null
}
하지만 스프링 프레임워크에는 ResponseEntity라는 이미 구현된 API 공통 응답 처리 클래스가 존재한다.
주로 REST API에서 응답 상태 코드
, 응답 본문
, 응답 헤더
등을 설정할 때 사용된다.
public class HttpEntity<T> {
private final HttpHeaders headers;
@Nullable
private final T body;
}
HTTP 요청(request) 또는 응답(response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스인 HttpEntity가 존재하는데
public class RequestEntity<T> extends HttpEntity<T>
public class ResponseEntity<T> extends HttpEntity<T>
HttpEntity
클래스를 상속받아 구현한 클래스가 바로 RequestEntity
, ResponseEntity
클래스이다.
ResponseEntity
는 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스이므로 HttpStatus
, HttpHeaders
, HttpBody
도 ResponseEntity에 포함된다.
200 OK
, 201 Created
)뿐만 아니라, 에러(400 Bad Request
, 401 Unauthorized
, 404 Not Found
, 500 Internal Server Error
등)에 대해서도 지정하고 싶은 적절한 상태 코드를 반환할 수 있게 한다.JSON
, XML
, 문자열
등 다양한 형식의 데이터를 응답 본문에 포함시킬 수 있다.ResponseEntity< T >
: 제네릭 타입 T는 응답 본문의 데이터 유형을 나타내며 이는 String, JSON, 또는 커스텀 객체 등 다양한 형식이 될 수 있다.ResponseEntity.ok()
: 응답 상태 코드를 요청에 대한 성공을 나타내는 200 OK
로 설정하고, 응답 본문을 추가할 수 있다.ResponseEntity<String> response = ResponseEntity.ok("Hello, World!");
@RestController
public class UserController {
@GetMapping("/users")
public ResponseEntity<List<User>> getUsers() {
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 28)
);
return ResponseEntity.ok(users); // 리스트 형태로 사용자 객체 반환
}
}
ResponseEntity.status(HttpStatus)
: 특정 상태 코드를 설정하여 응답을 생성할 수 있다.
예를 들어 404 Not Found 상태를 반환할 때 사용할 수 있다.
ResponseEntity<String> response = ResponseEntity.status(HttpStatus.NOT_FOUND).body("Resource not found");
ResponseEntity.headers()
: 응답 헤더를 설정할 수 있다.
HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "value");
ResponseEntity<String> response = ResponseEntity.ok().headers(headers).body("Response with custom header");
@RestController
public class MyController {
@GetMapping("/greeting")
public ResponseEntity<String> greeting() {
return ResponseEntity.ok("Hello, World!"); // 200 OK 응답
}
@RestController
public class MyController {
@GetMapping("/data")
public ResponseEntity<String> getData(@RequestParam(required = false) String param) {
if (param == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Missing required parameter");
}
return ResponseEntity.ok("Valid parameter: " + param);
}
}
@RestController
public class MyController {
@GetMapping("/header")
public ResponseEntity<String> withHeader() {
HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "CustomValue");
return ResponseEntity.ok()
.headers(headers)
.body("Response with custom header");
}
}