서문
Spring MVC를 넘어서 리액트와 스프링 풀스택 개발을 시도해보고 싶어졌습니다. 이를 위해서 Restful API를 설계해야하는데 혼자서 MVC 개발하던 패턴과는 아예 다른 방법으로 응답을 제공하더라구요.. 바로 ResponseEntity를 사용해서 stateCode, body 등을 반환했는데 이를 공부하면서 블로그에 정리하려고 합니다. 잘 몰라서 공부하며 정리한 것이라 만약에 틀린 부분이 있다면 댓글 부탁드립니다.. ㅜㅜ
ResponseEntity는 무엇이고, 왜 사용하는걸까?
Spring에서는 @ResponseBody로 DTO를 반환하면 알아서 Json으로 변환해 주기도 하고, 혼자 스프링 MVC로 개발할 때는 협업이 아닌 개인 개발이기 때문에 상태코드를 세밀하게 반환하지 않았어요. 적당한 예외처리를 하고, model을 통해 view로 넘겼습니다.
/**
* 게시판 글 등록
*/
@PostMapping("/notice/write")
public String NoticeInsert(HttpSession session, HttpServletRequest request, HttpServletResponse response,
ModelMap model) throws Exception {
String msg = "";
try {
// CmmUtil.nvl 메서드로 null 처리
String board_title = CmmUtil.nvl(request.getParameter("title")); //제목
String board_content = CmmUtil.nvl(request.getParameter("contents")); //내용
NoticeDTO pDTO = new NoticeDTO();
model.addAttribute("post_category", post_category);
pDTO.setPost_title(post_title);
pDTO.setContent(content);
noticeService.InsertNoticeInfo(pDTO);
//저장이 완료되면 사용자에게 보여줄 메시지
msg = "등록되었습니다.";
//변수 초기화(메모리 효율화 시키기 위해 사용함)
pDTO = null;
} catch (Exception e) {
//게시물 등록이 실패하면 사용자에게 보여줄 메시지
msg = "게시물 등록에 실패하였습니다.";
} finally {
//결과 메시지 전달 하기
model.addAttribute("msg", msg);
}
return "/notice/MsgToList";
}
하지만 프론트엔드와 협업하여 프로젝트를 진행할 땐 클라이언트가 요청에 대해 자세한 상태코드를 반환 해줘야 한다고해요.
ResponseEntity는 http 상태코드를 세밀하게 반환할 수 있도록 도와줍니다.
ResponseEntity의 구조는 다음과 같습니다.
ResponseEntity의 코드를 까보니 body, header, statueCode 순으로 생성자가 구성되어 있는걸 볼 수 있습니다.
body
body에는 클라이언트에게 보낼 데이터가 담겨져 있습니다. 실제 http 메시지 본문은 요청을 받았을 때, 응답할 때 body에 json, xml, html 형식의 데이터를 담아 전송합니다. 서버입장에서는 보통 응답 메시지 또는 DTO를 담아서 json을 반환하죠.
Header
헤더는 클라이언트 요청 또는 응답의 결과로 필요한 메타데이터를 담고 있다고해요.
보통 요청 헤더에는 Host, User-Agnet, Accpet 같은 것이 담겨져 있고,
GET /api/resource HTTP/1.1
Host: example.com
Accept: application/json
User-Agent: Mozilla/5.0
응답헤더에는 Content-Type, Server 등이 담겨 있습니다.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123
Server: Apache/2.4.1
statueCode
Http 상태코드는 클라이언트가 서버측에 보낸 요청에 대한 결과를 코드로 구분하여 알려줍니다.
1xx(정보) : 요청이 수신되었고 처리가 계속 되고 있음
2xx(성공) : 요청이 성공적으로 처리 되었음
3xx(리다이렉션): 요청 완료를 위해 클라이언트가 추가 작업을 수행해야 함.
4xx(클라이언트 오류) : 클라이언트의 요청에 오류가 있거나 권한이없거나 등등
5xx(서버 오류) : 서버가 요청을 처리하는 중에 오류가 발생함.
ReponseEntity 사용예제
// 댓글 불러오기
@GetMapping("getComments/{boardId}")
public ResponseEntity<?> getComments(@PathVariable("boardId") Long boardId) {
try {
List<ViewCommentDTO> comments = commentService.getComments(boardId);
return ResponseEntity.ok(comments);
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("댓글 정보를 불러오는 중 오류가 발생했습니다: " + e.getMessage());
}
}
위의 사용예제는 ResponseEntity<?> 와일드 카드를 사용하여 반환하고 있습니다. 이렇게 사용한 이유는 반환값이 DTO를 반환할 수도 있고, 에러 메시지 state code 500과 메시지를 반환할 수 있기 때문입니다.
ResponseEntity <?> 와일드 카드에 대해서
하지만 와일드카드는 구체적인 반환값이 명확하지 않아 유지보수가 어렵습니다. 때문에 실무에서 <?>를 사용하면 욕먹는다는 이야기가
곳곳에 들리더군요.. 하지만 간단한 스프린트 프로젝트 할 때는 개발 생산성을 올리기 위해서 와일드 카드를 사용할 때가 있긴 하다고해요.
근데 저는 궁금했습니다. 와일드카드를 사용하지 않으면 정해진 1가지 타입의 데이터만 보낼 수 있어요. 에러 응답은 제네릭 데이터형식과 맞지 않아서 보낼 수 없어요. 유지보수를 위해 어떻게 제네릭으로 명확한 데이터 타입은 유지하면서 예외처리까지 할 수 있을까? 궁금했습니다.
ResponseEntity를 잘 사용하는 법
찾아보니 아예 Spring Boot에서 권장하는 Restful API 에러 핸들러 방식이 있었습니다.
참고 블로그 : https://www.bezkoder.com/spring-boot-controlleradvice-exceptionhandler/
먼저 에러 메시지를 다음과 같이 생성합니다.
package com.example.demo.exception;
import java.util.Date;
public class ErrorMessage {
private int statusCode;
private Date timestamp;
private String message;
private String description;
public ErrorMessage(int statusCode, Date timestamp, String message, String description) {
this.statusCode = statusCode;
this.timestamp = timestamp;
this.message = message;
this.description = description;
}
// Getters and setters
public int getStatusCode() {
return statusCode;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}
예외클래스 정의
package com.example.demo.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
전역 에러 핸들러 클래스
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorMessage> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorMessage message = new ErrorMessage(
HttpStatus.NOT_FOUND.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
return new ResponseEntity<>(message, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorMessage> handleGlobalException(Exception ex, WebRequest request) {
ErrorMessage message = new ErrorMessage(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
new Date(),
ex.getMessage(),
request.getDescription(false)
);
return new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
예제 컨트롤러
package com.example.demo.controller;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.model.Tutorial;
import com.example.demo.repository.TutorialRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class TutorialController {
@Autowired
TutorialRepository tutorialRepository;
@GetMapping("/tutorials/{id}")
public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
Tutorial tutorial = tutorialRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));
return ResponseEntity.ok(tutorial);
}
}
이렇게 사용하면 반환할 자료형은 명확하게 제네릭으로 정의하면서 에러 발생 시 올바른 상태값과 메시지를 클라이언트에게 전송할 수 있습니다!
'Spring' 카테고리의 다른 글
[Spring Boot] Spring 대용량 데이터 페이징 처리하기 2탄(Spring Data JPA + PostgreSQL) (0) | 2025.02.20 |
---|---|
[Spring Boot] Spring 대용량 데이터 페이징 처리하기 1탄(Spring Data JPA + PostgreSQL) (0) | 2025.02.19 |
[Spring Boot]Gradle 의존성 implementation vs runtimeOnly vs api 차이 (0) | 2025.02.17 |
[Spring] WebClient를 사용 해야하는 이유 [RestTemplate vs WebClient] 성능비교 (0) | 2024.06.08 |
[Spring Boot] @PostConstruct 사용법 (0) | 2024.05.23 |