[Spring Boot Tutorial] 12. Spring Boot REST Api에 ResponseEntity 적용하기

2020. 4. 22. 10:05Spring/Spring Boot Tutorial

300x250
반응형

지금까지 demo프로젝트를 구성할 때, @ResponseBody의 result와 reason 값에 api 요청 결과를 출력했습니다.

그러나, http 프로토콜을 이용하는 통신에는 응답결과를 body이외에 http 상태 코드와 header 를 설정하여 보다 세밀하게 response를 표현할 수 있습니다.

ResponseEntityHtttHeaders headers와 T body, HttpStatus status를 포함한 클래스입니다
ResponseEntity를 이용하여 demo 프로젝트를 HTTP response 규약을 지키는 rest api로 만들어봅시다.

  1. 이론
    1-1) HTTP 상태 코드
    1-2) 전략
    1-3) 기존 api (변경 전)
    1-4) 새로 변경될 api
  2. 1단계 : Response 클래스 적용
  3. 2단계 : ResponseEntity 적용

1. 이론

1-1) HTTP 상태 코드

  • api 요청이 성공적으로 완료된 경우
    • 2로 시작되는 상태 코드
    • 200(OK) : 요청 성공의 기본 상태 코드
    • 201(No Content) : body에 응답 내용이 없을 경우 이용
  • 클라이언트 측 에러 응답
    • 4로 시작되는 상태 코드
    • 400(Bad Request) : request 형식 틀렸을 경우
    • 401(Unathorized) : 리소스 접근 권한 없는 경우
    • 403(Forbidden) : 해당 리소스에 접근하는 것이 허락되지 않을 경우
    • 404(Not Found) : 존재하지 않는 URI
    • 405(Method Not Allowed) : 존재하지 않는 request method
    • 406(Not Acceptable) : request의 Accept header에 설정한 MIME 타입이 지원 불가능한 경우
    • 414(URI Too Long) : 요청한 URI가 너무 김
  • 서버 측 에러 응답
    • 5로 시작되는 상태 코드
    • 500(Internal Server Error) : 서버에서 에러가 발생한 경우에 설정되는 기본 상태 코드
    • 503(Service Unavailable) : 외부 서비스가 현재 멈춘 상태 이거나 이용할 수 없는 서비스

※ HTTP 상태코드에 더 알고 싶다면 MDN web docs를 참고해주세요.


기존의 demo api에서는 조회할 정보가 없을 경우 result, reason 값을 이용하여 상태를 표현했습니다.

78

http 상태코드를 별도로 설정하지 않아, result가 FAIL 이면서 http상태코드는 200 OK로 표현되고 있었습니다.


1-2) 전략

  • 반환타입을 ResponseEntity로 감싼다.
  • 조회/갱신/삭제할 대상이 없을 경우에는 404 Not Found를 응답한다.
  • 추가 중 에러가 발생했을 경우 500 Internal Server Error를 응답한다.
  • 추가/갱신/삭제 시 201 No Content로 응답한다.

1-3) 기존 api (변경 전)

82

조회 성공했을 때, result에 "SUCCESS", user에 내용을 담았습니다.


83

조회할 값이 없을 경우, result에 "FAIL", reason에 에러 원인을 담았습니다. result가 FAIL이지만, Http 상태코드는 여전히 200로 설정되었습니다.


1-4) 새로 변경될 api

80

조회 성공했을 때, data에 내용을 담고, data의 길이를 count에 담았습니다.


81

조회 실패했을 경우, errorMessage와 errorCode를 body에 담았습니다. Http 상태코드는 404 입니다.


2. 1단계 : Response 클래스 적용

ResponseEntity를 이용하기 위한 전략을 모두 짰으니, 본격적으로 코드 작성을 시작해봅시다.

지금까지의 demo api에서는 응답 클래스를 별도로 생성하지 않고, 상황에 따라 필요한 값들을 Map에 넣어 아래와 같이 반환했었습니다.

@PostMapping("")
public Map<String, Object> save(@RequestBody UserValue value) {
	Map<String, Object> response = new HashMap<>();

	User user = userService.save(value);
	if(user != null) {
		response.put("result", "SUCCESS");
		response.put("user", user);
	} else {
		response.put("result", "FAIL");
		response.put("reason", "회원 가입 실패");
	}

	return response;
}

@GetMapping("/{id}")
public Map<String, Object> findById(@PathVariable("id") long id) {
	Map<String, Object> response = new HashMap<>();

	Optional<User> oUser = userService.findById(id);
	if(oUser.isPresent()) {
		response.put("result", "SUCCESS");
		response.put("user", oUser.get());
	} else {
		response.put("result", "FAIL");
		response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
	}

	return response;
}

물론, ResponseEntity에 Map을 넣어도 되지만, 공통적으로 이용되는 key를 Map에 설정하기 위해 반복적으로 put을 이용하여 key를 직접 설정하는 것은 매우 번거로운 일입니다.

api 응답에 필요한 필드값들을 가지고 있는 Response 클래스를 별도로 만들어 코드의 재사용성을 높이도록 합시다.

response의 형태를 위와같은 형태로 변경하기 위해서는, api 요청 성공시 이용할 Response와, 실패시 이용할 Response 2개의 Response 클래스를 생성해야 합니다.

두 Response의 부모클래스인 껍데기 abstract class를 먼저 만듭니다.

public abstract class BasicResponse {
}

그리고, BasicResponse를 상속받는 CommonResponse와 ErrorResponse를 선언합니다.

@Getter @Setter
public class CommonResponse<T> extends BasicResponse {
	private int count;
	private T data;

	public CommonResponse(T data) {
		this.data = data;
		if(data instanceof List) {
			this.count = ((List<?>)data).size();
		} else {
			this.count = 1;
		}
	}
}
@Getter @Setter
public class ErrorResponse extends BasicResponse {
	private String errorMessage;
	private String errorCode;

	public ErrorResponse(String errorMessage) {
		this.errorMessage = errorMessage;
		this.errorCode = "404";
	}
	public ErrorResponse(String errorMessage, String errorCode) {
		this.errorMessage = errorMessage;
		this.errorCode = errorCode;
	}
}

api 요청 성공시엔 반환하고자 하는 데이터를 담은 T data와, 그 데이터의 길이인 int count를,
api 요청 실패시엔 errorMessage와 errorCode를 반환합니다.

ResponseEntity를 적용하기 이전에 반환 타입을 Map에서 위에서 생성한 Response 클래스로 바꿔봅시다.

@PostMapping("")
public BasicResponse save(@RequestBody UserValue value) {
	User user = userService.save(value);
	if(user == null) {
		return new ErrorResponse("회원 가입 실패");
	}
	return new CommonResponse<User>(user);
}

@GetMapping("/{id}")
public BasicResponse findById(@PathVariable("id") long id) {
	Optional<User> oUser = userService.findById(id);
	if(!oUser.isPresent()) {
		return new ErrorResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
	}		
	return new CommonResponse<User>(oUser.get());
}

api 코드가 매우 깔끔해졌습니다.

84

api를 테스트해보면 우리가 새로 변경하고자 하는 응답 response가 나오는 것을 확인할 수 있습니다.

그러나, Http 상태코드는 200으로 출력됨을 알 수 있습니다.
이제, 이 Response클래스를 ResponseEntity로 wrapping한 후 반환하여 HTTP 상태코드를 적용해보도록 합시다


3. 2단계 : ResponseEntity 적용

ResponseEntity를 wrapping해봅시다.
위의 전략에서 작성하려 했던 HTTP 상태코드는 아래와 같았습니다.

  • 조회/갱신/삭제할 대상이 없을 경우에는 404
  • 추가/갱신/삭제 시 201
  • 추가 중 에러가 발생했을 경우 500
  • 조회 대상이 있을 경우에는 200

우리가 세운 전략대로 HTTP 상태코드를 설정해봅시다.


3-1) 조회/갱신/삭제할 대상이 없을 경우에는 404(Not Found)

@GetMapping("/{id}")
public ResponseEntity<? extends BasicResponse> select(@PathVariable("id") long id) {
	Optional<User> oUser = userService.findById(id);
	if(!oUser.isPresent()) {
		return ResponseEntity.status(HttpStatus.NOT_FOUND)
				.body(new ErrorResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요."));
	}		
	return ResponseEntity.ok().body(new CommonResponse<User>(oUser.get()));
}

body에 담고자하는 Response객체를 담고, status에 상태코드를 담습니다.


3-2) 추가/갱신/삭제 시 201(No Content)

@PatchMapping("/{id}")
public ResponseEntity<? extends BasicResponse> patch(@PathVariable("id") long id, @RequestBody UserValue value) {
	if(!userService.patch(id, value)) {
		return ResponseEntity.status(HttpStatus.NOT_FOUND)
				.body(new ErrorResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요."));
	}
	return ResponseEntity.noContent().build();
}

추가, 갱신, 삭제시 별도로 반환해야할 데이터가 없을 경우에는 201 코드를 이용합니다.


3-3) 추가 중 에러가 발생했을 경우 500(Internal Server Error)

@PostMapping("")
public ResponseEntity<? extends BasicResponse> save(@RequestBody UserValue value) {
	User user = userService.save(value);
	if(user == null) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
				.body(new ErrorResponse("회원 가입 실패"));
	}
	return ResponseEntity.noContent().build();
}

ResponseEntity 적용을 마친 후 다시 api 테스트를 해보면 우리가 만들고자 했던 api 응답을 하는 것을 확인할 수 있습니다.

GitHub에서 demo 프로젝트를 다운받아 볼 수 있습니다.

300x250
반응형