ResponseEntity
스프링에서 제공하는 클래스로 HTTP 응답의 상태 코드, 헤더, 바디 등을 직접 제어할 수 있는 스프링 MVC에서 컨트롤러 메서드가 반환할 수 있는 표준적인 응답 타입
- ResponseEntity 사용 예시
@GetMapping("/user/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.notFound().build();
}
}
ResponseEntity를 사용하면 String으로 반환할 때에 비해서 다음과 같은 장점이 있습니다
- HTTP 상태코드, 헤더, 본문을 쉽게 조작
- Spring에서 기본 제공되므로 Spring 생태계에서 사용과 통합이 편리
하지만 다음과 같은 단점 역시 존재합니다
- 일관된 응답 구조를 강제하기 어려움
- 프로젝트에 요구사항의 변화에 따른 응답구조 유지보수가 어려움
- ResponseEntity 단점 예시
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final UsersService usersService;
@GetMapping("/user/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginDto loginDto) {
User user = userService.login(loginDto);
if (user != null) {
Map<String, Object> response = new HashMap<>();
response.put("data", user);
response.put("message", "Login Success");
return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Login is Failed!");
}
}
이 예시에서 알 수 있듯이 바디에 User만 담을 수도 메시지도 같이 담을 수도 있기에 이 Response를 사용하는 입장에서는 일관되지 않은 형식으로 사용에 어려움을 느끼거나 실수할 가능성이 높아집니다
또한, 현재 방식으로는 로그인 실패를 아이디가 틀린 경우와 비밀번호가 틀린 경우로 나누지 않았습니다
프로젝트의 요구사항이 로그인 실패 사유를 나누어서 처리하는 것이라면 현재 응답구조보다는 좀 더 알맞게 커스텀을 하는 것이 좋아보입니다
이러한 점을 개선하고 싶다면 프로젝트에 알맞는 BaseResponse를 정의해서 사용하는 것이 좋습니다
BaseResponse
BaseResponse의 설계 및 구현 방법을 알아봅시다
1. BaseResponse 정의
기본적으로 사용되는 HTTP 상태코드, 데이터 이외에도 상태를 좀 더 세분화해서 다룰 수 있는 이름과 그에 알맞는 메시지를 담을 수 있는 BaseResponse를 만들어보겠습니다
@Getter
@Setter
public class BaseResponse<T> {
private String name;
private HttpStatus status;
private String message;
private T data;
@Builder
public BaseResponse(String name, HttpStatus status, String message, T data) {
this.name = name;
this.status = status;
this.message = message;
this.data = data;
}
}
2. 성공 관련 응답을 Enum으로 관리
성공 관련 응답들의 사용 편의성과 유지보수 용이성을 위해 Enum으로 한 클래스에서 관리하도록 하겠습니다
@Getter
public enum BaseSuccessResponse {
FIND_USER_SUCCESS(HttpStatus.OK, "유저 검색에 성공했습니다!"),
LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다!"),
;
private final HttpStatus status;
private final String message;
BaseSuccessResponse(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
}
3. BaseResponseService 사용으로 코드 반복 감소
BaseResponse를 생성하는 코드를 BaseResponseService에 메서드로 만들어 반복을 줄여줍니다
public interface BaseResponseService {
<T> ResponseEntity<BaseResponse<?>> getSuccessResponse(BaseSuccessResponse baseResponseStatus, T data);
ResponseEntity<BaseResponse<?>> getSuccessResponse(BaseSuccessResponse baseResponseStatus);
}
@Service
public class BaseResponseServiceImpl implements BaseResponseService{
public <T> BaseResponse<?> getSuccessResponse(BaseSuccessResponse baseResponseStatus, T data) {
return BaseResponse<?> baseResponse = BaseResponse.builder()
.name(baseResponseStatus.name())
.status(baseResponseStatus.getStatus())
.message(baseResponseStatus.getMessage())
.data(data)
.build();
}
public <T> BaseResponse<?> getSuccessResponse(BaseSuccessResponse baseResponseStatus) {
return BaseResponse<?> baseResponse = BaseResponse.builder()
.name(baseResponseStatus.name())
.status(baseResponseStatus.getStatus())
.message(baseResponseStatus.getMessage())
.data(null)
.build();
}
}
4. 실패 관련 응답을 Enum으로 관리
실패 관련 응답들의 사용 편의성과 유지보수 용이성을 위해 Enum으로 한 클래스에서 관리하도록 하겠습니다
@Getter
public enum BaseFailureResponse {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다!"),
ID_NOT_CORRECT(HttpStatus.UNAUTHORIZED, "아이디가 틀렸습니다!!"),
PASSWORD_NOT_CORRECT(HttpStatus.UNAUTHORIZED, "비밀번호가 틀렸습니다!"),
;
private final HttpStatus status;
private final String message;
BaseFailureResponse(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
5. ExceptionHandler 사용으로 예외 처리 코드 반복 감소
BaseFailureResponse를 BaseException으로 Runtime 에러로 만든 후 이를 ExceptionHandler로 관리함으로 유지보수의 용이성을 높이고 코드의 반복을 줄여줍시다
@Getter
@RequiredArgsConstructor
public class BaseException extends RuntimeException {
private final BaseFailureResponse baseFailureResponse;
}
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
protected BaseResponse<?> handleBaseException(BaseException ex) {
return BaseResponse<?> baseResponse = BaseResponse.builder()
.name(ex.baseFailureResponse().name())
.status(ex.baseFailureResponse().getStatus())
.message(ex.baseFailureResponse().getMessage())
.data(null)
.build();
}
자 지금까지 만든 BaseResponse를 활용하여 기존의 코드를 수정해 봅시다
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
private final UsersService usersService;
private final BaseResponseService baseResponseService;
@GetMapping("/{id}")
public BaseResponse<?> getUser(@PathVariable Long id) {
Map<String, Object> data = userService.findById(id);
return baseResponseService.getSuccessResponse(BaseSuccessResponse.FIND_USER_SUCCESS, data);
}
@GetMapping("/login")
public BaseResponse<?> login(@RequestBody LoginDto loginDto) {
Map<String, Object> data = userService.login(loginDto);
return baseResponseService.getSuccessResponse(BaseSuccessResponse.LOGIN_SUCCESS, data);
}
@Service
@RequiredArgsConstructor
public class UsersService {
private final UserRepository userRepository;
public Map<String, Object> findById(Long id) {
Optional<User> user = userRepository.findById(id);
if (user.isEmpty()) {
throw new BaseException(BaseFailureResponse.USER_NOT_FOUND);
}
return convertUserToMap(user.get());
}
public Map<String, Object> login(LoginDto loginDto) {
Optional<User> user = userRepository.findByUsername(loginDto.getUsername());
if (user.isEmpty()) {
throw new BaseException(BaseFailureResponse.ID_NOT_CORRECT);
}
if (!user.get().getPassword().equals(loginDto.getPassword())) {
throw new BaseException(BaseFailureResponse.PASSWORD_NOT_CORRECT);
}
return convertUserToMap(user.get());
}
private Map<String, Object> convertUserToMap(User user) {
return Map.of(
"id", user.getId(),
"username", user.getUsername(),
"email", user.getEmail()
);
}
}
BaseResponse를 사용함으로 저희는 다음과 같은 장점을 얻게 됐습니다
- 모든 API 응답이 일관된 구조를 가짐
- 프로젝트의 요구사항에 알맞은 응답 구조 사용 가능
- 응답 구조를 한 곳에서 정의하고 관리할 수 있어 유지보수가 용이
물론 이 방법 역시 단점이 존재합니다
- 구현 및 유지보수에 추가 작업 필요
- Spring의 기본 기능과 통합 시 추가 설정 필요
ResponseEntity와 BaseResponse의 장단점을 고려하여 더 적절한 응답형식을 선택해봅시다
추가적으로 두 방법을 혼합한 ResponseEntity<BaseResponse>와 같은 응답 구조도 있는데
응답 형식의 구조가 조금 복잡해지지만 두 가지 방식의 장점을 모두 사용할 수 있습니다
'프로젝트' 카테고리의 다른 글
Nginx를 이용하여 HTTP를 HTTPS로 리다이렉트하는 방법 (0) | 2024.09.29 |
---|---|
GitLab, Jenkins, Docker, S3, EC2를 활용한 CI/CD 파이프라인 구축 - (1) Webhook 설정하기 (0) | 2024.09.15 |
Spring Security에서 Exception Handler(예외 처리) 사용 하기 (0) | 2024.08.04 |