[Spring] 23. ResponseEntity란?
이전 글에서는 @ResponseBody와 @RestController에 대해 살펴봤습니다. @ResponseBody는 Controller 메서드의 반환값을 View 이름으로 보지 않고, HTTP 응답 Body에 직접 담아 반환하게 해주는 어노테이션입니다.
이번 글에서는 API 응답을 더 세밀하게 제어할 수 있는 ResponseEntity에 대해 정리하겠습니다. 단순히 데이터만 반환하는 것이 아니라, HTTP 상태 코드와 Header, Body를 함께 다룰 수 있습니다.
핵심은 간단합니다. ResponseEntity는 HTTP 응답 전체를 표현하는 객체입니다.
1. ResponseEntity란?
ResponseEntity는 Spring에서 HTTP 응답을 직접 제어할 때 사용하는 클래스입니다. 응답 Body뿐만 아니라 상태 코드와 Header까지 함께 설정할 수 있습니다.
HTTP 응답
├─ Status Code
├─ Header
└─ Body
일반적으로 @RestController에서 객체를 반환하면 Spring이 자동으로 JSON 응답을 만들어줍니다.
@GetMapping("/api/boards/1")
public BoardDto detail() {
return new BoardDto(1L, "Spring", "ResponseEntity 정리");
}
이 방식도 충분히 사용할 수 있습니다. 하지만 상태 코드를 명확하게 지정하고 싶거나, Header를 함께 내려야 할 때는 ResponseEntity를 사용할 수 있습니다.
@GetMapping("/api/boards/1")
public ResponseEntity<BoardDto> detail() {
BoardDto board = new BoardDto(1L, "Spring", "ResponseEntity 정리");
return ResponseEntity.ok(board);
}
위 코드는 BoardDto를 응답 Body로 보내면서 HTTP 상태 코드는 200 OK로 응답합니다.
2. 왜 ResponseEntity가 필요할까?
API에서는 단순히 데이터만 보내는 것으로 끝나지 않습니다. 요청이 성공했는지, 생성되었는지, 잘못된 요청인지, 찾는 데이터가 없는지 같은 상태를 클라이언트에게 알려줘야 합니다.
| 상태 코드 | 의미 | 예시 상황 |
200 OK |
요청 성공 | 목록 조회, 상세 조회 성공 |
201 Created |
리소스 생성 성공 | 게시글 등록 성공 |
400 Bad Request |
잘못된 요청 | 필수 입력값 누락 |
404 Not Found |
리소스를 찾을 수 없음 | 존재하지 않는 게시글 조회 |
500 Internal Server Error |
서버 오류 | 서버 처리 중 예외 발생 |
ResponseEntity를 사용하면 이런 상태 코드를 Controller에서 명확하게 지정할 수 있습니다.
성공
→ 200 OK
등록 성공
→ 201 Created
요청 값 오류
→ 400 Bad Request
데이터 없음
→ 404 Not Found
3. ResponseEntity 기본 구조
ResponseEntity는 제네릭 타입을 사용합니다. 응답 Body에 담을 데이터 타입을 지정할 수 있습니다.
ResponseEntity<BoardDto>
이 코드는 응답 Body에 BoardDto 타입의 데이터를 담겠다는 의미입니다.
@GetMapping("/api/boards/{boardNo}")
public ResponseEntity<BoardDto> detail(@PathVariable Long boardNo) {
BoardDto board = new BoardDto(boardNo, "Spring", "상세 조회");
return ResponseEntity.ok(board);
}
응답 흐름은 다음과 같습니다.
BoardDto 객체 생성
→ ResponseEntity.ok(board)
→ HTTP 200 OK
→ Body에 board JSON 응답
4. ResponseEntity.ok()
ResponseEntity.ok()는 200 OK 응답을 만들 때 사용합니다. 조회 API에서 가장 자주 볼 수 있습니다.
@RestController
@RequestMapping("/api/boards")
public class BoardApiController {
@GetMapping("/{boardNo}")
public ResponseEntity<BoardDto> detail(@PathVariable Long boardNo) {
BoardDto board = new BoardDto(boardNo, "Spring", "ResponseEntity");
return ResponseEntity.ok(board);
}
}
클라이언트는 대략 다음과 같은 응답을 받을 수 있습니다.
{
"boardNo": 1,
"title": "Spring",
"content": "ResponseEntity"
}
상태 코드는 200 OK입니다.
HTTP/1.1 200 OK
5. Body 없이 상태 코드만 반환하기
응답 Body가 필요 없는 경우도 있습니다. 예를 들어 삭제 처리가 성공했을 때 별도 데이터를 보내지 않아도 될 수 있습니다.
@DeleteMapping("/{boardNo}")
public ResponseEntity<Void> delete(@PathVariable Long boardNo) {
return ResponseEntity.noContent().build();
}
ResponseEntity.noContent()는 204 No Content 응답을 만들 때 사용합니다. Body 없이 성공 상태만 전달할 수 있습니다.
DELETE /api/boards/10
→ 삭제 처리 성공
→ 204 No Content
응답 Body가 없다는 의미를 타입으로 표현하기 위해 ResponseEntity<Void>를 사용할 수 있습니다.
6. 201 Created 응답
새로운 리소스를 생성하는 API에서는 201 Created 상태 코드를 사용할 수 있습니다. 예를 들어 게시글 등록 API입니다.
@PostMapping
public ResponseEntity<BoardDto> write(@RequestBody BoardDto boardDto) {
BoardDto savedBoard = new BoardDto(10L, boardDto.getTitle(), boardDto.getContent());
return ResponseEntity.status(HttpStatus.CREATED).body(savedBoard);
}
위 코드는 게시글 등록이 성공했고, 생성된 게시글 정보를 Body로 응답하는 구조입니다.
POST /api/boards
→ 게시글 등록
→ 201 Created
→ 생성된 게시글 정보 응답
조금 더 REST스럽게 작성하려면 생성된 리소스의 위치를 Header에 담을 수도 있습니다.
@PostMapping
public ResponseEntity<BoardDto> write(@RequestBody BoardDto boardDto) {
BoardDto savedBoard = new BoardDto(10L, boardDto.getTitle(), boardDto.getContent());
URI location = URI.create("/api/boards/" + savedBoard.getBoardNo());
return ResponseEntity
.created(location)
.body(savedBoard);
}
ResponseEntity.created(location)을 사용하면 201 Created 상태 코드와 함께 Location Header를 설정할 수 있습니다.
7. 400 Bad Request 응답
클라이언트가 잘못된 값을 보낸 경우에는 400 Bad Request를 사용할 수 있습니다. 예를 들어 제목이 비어 있는 게시글 등록 요청을 생각해보겠습니다.
@PostMapping
public ResponseEntity<ApiResult> write(@RequestBody BoardDto boardDto) {
if (boardDto.getTitle() == null || boardDto.getTitle().isBlank()) {
return ResponseEntity
.badRequest()
.body(new ApiResult(false, "제목을 입력해주세요."));
}
return ResponseEntity.ok(new ApiResult(true, "게시글 등록 성공"));
}
요청 값이 잘못되었을 때 무조건 200 OK로 응답하기보다, 상황에 맞는 상태 코드를 내려주는 것이 좋습니다.
{
"success": false,
"message": "제목을 입력해주세요."
}
이 응답의 상태 코드는 400 Bad Request입니다.
8. 404 Not Found 응답
요청한 데이터를 찾지 못했을 때는 404 Not Found를 사용할 수 있습니다. 예를 들어 존재하지 않는 게시글 번호로 상세 조회를 요청한 경우입니다.
@GetMapping("/{boardNo}")
public ResponseEntity<?> detail(@PathVariable Long boardNo) {
BoardDto board = findBoard(boardNo);
if (board == null) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ApiResult(false, "게시글을 찾을 수 없습니다."));
}
return ResponseEntity.ok(board);
}
위 코드에서는 게시글이 있으면 200 OK와 함께 게시글 정보를 반환합니다. 게시글이 없으면 404 Not Found와 함께 오류 메시지를 반환합니다.
게시글 있음
→ 200 OK + BoardDto
게시글 없음
→ 404 Not Found + 오류 메시지
다만 실제 프로젝트에서는 Controller에서 직접 null 체크를 반복하기보다, 예외 처리 구조를 따로 두는 경우가 많습니다. 이 내용은 이후 예외 처리 파트에서 정리하면 좋습니다.
9. Header 설정하기
ResponseEntity는 Header도 설정할 수 있습니다. 예를 들어 특정 응답에 커스텀 Header를 추가해보겠습니다.
@GetMapping("/header-test")
public ResponseEntity<String> headerTest() {
return ResponseEntity
.ok()
.header("X-App-Version", "1.0")
.body("ok");
}
이 응답에는 X-App-Version이라는 Header가 포함됩니다.
HTTP/1.1 200 OK
X-App-Version: 1.0
ok
Header는 인증, 캐시, 파일 다운로드, 생성된 리소스 위치 전달 등 다양한 상황에서 사용할 수 있습니다.
10. ApiResult 응답 DTO 만들기
API 응답 구조를 일정하게 만들고 싶다면 응답 DTO를 만들 수 있습니다. 예를 들어 성공 여부와 메시지를 담는 ApiResult를 만들 수 있습니다.
public class ApiResult {
private boolean success;
private String message;
public ApiResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
}
Controller에서는 다음처럼 사용할 수 있습니다.
@PostMapping
public ResponseEntity<ApiResult> write(@RequestBody BoardDto boardDto) {
return ResponseEntity.ok(new ApiResult(true, "게시글 등록 성공"));
}
응답은 다음과 같은 JSON 형태가 될 수 있습니다.
{
"success": true,
"message": "게시글 등록 성공"
}
Map으로 응답을 만들 수도 있지만, 응답 구조가 반복된다면 DTO를 사용하는 것이 더 명확합니다.
11. ResponseEntity와 @ResponseBody 차이
@ResponseBody와 ResponseEntity는 둘 다 응답 Body와 관련이 있습니다. 하지만 제어 범위가 다릅니다.
| 구분 | @ResponseBody | ResponseEntity |
| 역할 | 반환값을 응답 Body로 보냅니다. | 응답 전체를 직접 구성합니다. |
| 제어 가능 범위 | 주로 Body | Status, Header, Body |
| 사용 예시 | return boardDto; |
return ResponseEntity.ok(boardDto); |
| 추천 상황 | 단순 응답 | 상태 코드와 Header까지 명확히 제어할 때 |
즉, 단순히 JSON만 반환해도 된다면 객체를 바로 반환할 수 있습니다. 하지만 상태 코드나 Header를 명확히 다루고 싶다면 ResponseEntity를 사용하면 좋습니다.
12. ResponseEntity와 @RestController
@RestController와 ResponseEntity는 함께 자주 사용됩니다. @RestController는 반환값을 응답 Body로 처리하고, ResponseEntity는 상태 코드와 Header, Body를 담은 응답 객체입니다.
@RestController
@RequestMapping("/api/boards")
public class BoardApiController {
@GetMapping("/{boardNo}")
public ResponseEntity<BoardDto> detail(@PathVariable Long boardNo) {
BoardDto board = new BoardDto(boardNo, "Spring", "ResponseEntity");
return ResponseEntity.ok(board);
}
}
REST API Controller에서는 이런 구조가 자주 등장합니다.
@RestController
→ API Controller
ResponseEntity
→ 상태 코드 + Header + Body 응답
13. 게시판 API 전체 예시
게시판 API를 기준으로 ResponseEntity를 적용하면 다음처럼 작성할 수 있습니다.
@RestController
@RequestMapping("/api/boards")
public class BoardApiController {
@GetMapping
public ResponseEntity<List<BoardDto>> list() {
List<BoardDto> boardList = List.of(
new BoardDto(1L, "Spring MVC", "목록 조회"),
new BoardDto(2L, "Spring Boot", "API 응답")
);
return ResponseEntity.ok(boardList);
}
@GetMapping("/{boardNo}")
public ResponseEntity<?> detail(@PathVariable Long boardNo) {
BoardDto board = findBoard(boardNo);
if (board == null) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ApiResult(false, "게시글을 찾을 수 없습니다."));
}
return ResponseEntity.ok(board);
}
@PostMapping
public ResponseEntity<BoardDto> write(@RequestBody BoardDto boardDto) {
BoardDto savedBoard = new BoardDto(10L, boardDto.getTitle(), boardDto.getContent());
URI location = URI.create("/api/boards/" + savedBoard.getBoardNo());
return ResponseEntity
.created(location)
.body(savedBoard);
}
@DeleteMapping("/{boardNo}")
public ResponseEntity<Void> delete(@PathVariable Long boardNo) {
return ResponseEntity.noContent().build();
}
private BoardDto findBoard(Long boardNo) {
if (boardNo == 1L) {
return new BoardDto(1L, "Spring", "상세 조회");
}
return null;
}
}
이 예시에서는 요청 상황에 따라 상태 코드가 달라집니다.
| 요청 | 상황 | 응답 상태 |
GET /api/boards |
목록 조회 성공 | 200 OK |
GET /api/boards/1 |
상세 조회 성공 | 200 OK |
GET /api/boards/999 |
게시글 없음 | 404 Not Found |
POST /api/boards |
게시글 생성 성공 | 201 Created |
DELETE /api/boards/1 |
삭제 성공 | 204 No Content |
14. 자주 하는 실수
1) 모든 응답을 200 OK로만 보내는 경우
요청 값이 잘못되었거나 데이터가 없는데도 항상 200 OK를 보내면 클라이언트가 상황을 정확히 판단하기 어렵습니다.
// 좋지 않은 예시
return ResponseEntity.ok(new ApiResult(false, "게시글을 찾을 수 없습니다."));
게시글을 찾지 못한 상황이라면 404 Not Found를 사용하는 것이 더 명확할 수 있습니다.
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ApiResult(false, "게시글을 찾을 수 없습니다."));
2) Body 타입을 너무 자주 바꾸는 경우
같은 API에서 어떤 때는 BoardDto를 반환하고, 어떤 때는 Map을 반환하고, 어떤 때는 문자열을 반환하면 클라이언트가 처리하기 어려워질 수 있습니다.
성공 응답
→ BoardDto
실패 응답
→ ApiResult
응답 구조가 너무 들쭉날쭉하면 프론트에서 처리하기 어려움
실제 프로젝트에서는 공통 응답 형식을 정해두면 관리하기 좋습니다.
3) @Controller에서 ResponseEntity를 쓰면서 화면 반환을 기대하는 경우
ResponseEntity는 주로 API 응답에 사용합니다. JSP나 Thymeleaf 화면을 반환할 때는 View 이름을 반환하는 Controller 구조가 더 자연스럽습니다.
@Controller
public class BoardController {
@GetMapping("/board/list")
public String list() {
return "board/list";
}
}
화면 Controller와 API Controller의 역할을 나누면 구조가 명확해집니다.
4) 201 Created를 쓰면서 Location Header를 고려하지 않는 경우
리소스를 새로 생성했다면 생성된 리소스의 위치를 Location Header로 알려줄 수 있습니다. 필수는 아니지만 REST API 설계에서는 자주 고려하는 방식입니다.
URI location = URI.create("/api/boards/" + savedBoard.getBoardNo());
return ResponseEntity
.created(location)
.body(savedBoard);
5) 예외 처리를 Controller마다 반복하는 경우
처음에는 Controller에서 직접 분기해도 됩니다. 하지만 프로젝트가 커지면 오류 응답을 Controller마다 반복해서 작성하게 됩니다.
게시글 없음
회원 없음
상품 없음
권한 없음
입력값 오류
→ 매번 Controller에서 처리하면 중복 증가
이런 문제는 이후 @ExceptionHandler, @ControllerAdvice 같은 공통 예외 처리로 정리할 수 있습니다.
15. 전체 흐름 정리
ResponseEntity를 사용하는 API 응답 흐름은 다음과 같습니다.
1. 클라이언트가 API 요청을 보낸다.
2. Controller가 요청을 받는다.
3. Service를 호출해 기능을 처리한다.
4. 처리 결과에 따라 상태 코드를 정한다.
5. 필요한 Header를 설정한다.
6. Body에 응답 데이터를 담는다.
7. ResponseEntity를 반환한다.
8. 클라이언트는 상태 코드와 응답 데이터를 함께 받는다.
즉, ResponseEntity는 API 응답을 명확하게 표현하기 위한 도구입니다.
정리
ResponseEntity는 HTTP 응답 전체를 표현하는 객체입니다. 상태 코드, Header, Body를 함께 제어할 수 있습니다.
| ResponseEntity.ok() | 200 OK 응답을 반환합니다. |
| ResponseEntity.status() | 원하는 HTTP 상태 코드를 지정할 수 있습니다. |
| ResponseEntity.created() | 201 Created 응답과 Location Header를 설정할 수 있습니다. |
| ResponseEntity.noContent() | 204 No Content 응답을 반환할 수 있습니다. |
| ResponseEntity.badRequest() | 400 Bad Request 응답을 반환할 수 있습니다. |
처음에는 다음 기준으로 기억하면 좋습니다.
단순 JSON 응답
→ 객체를 바로 반환해도 가능
상태 코드와 Header까지 제어
→ ResponseEntity 사용
생성 성공
→ 201 Created
삭제 성공 후 Body 없음
→ 204 No Content
잘못된 요청
→ 400 Bad Request
데이터 없음
→ 404 Not Found
연습해보기
아래 요구사항을 보고 ResponseEntity로 응답을 작성해보세요.
GET /api/members/1
→ 회원 정보가 있으면 200 OK + MemberDto
→ 회원 정보가 없으면 404 Not Found + 오류 메시지
POST /api/members
→ 회원가입 성공 시 201 Created + 생성된 회원 정보
DELETE /api/members/1
→ 삭제 성공 시 204 No Content
예시는 다음과 같습니다.
@RestController
@RequestMapping("/api/members")
public class MemberApiController {
@GetMapping("/{memberNo}")
public ResponseEntity<?> detail(@PathVariable Long memberNo) {
MemberDto member = findMember(memberNo);
if (member == null) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ApiResult(false, "회원을 찾을 수 없습니다."));
}
return ResponseEntity.ok(member);
}
@PostMapping
public ResponseEntity<MemberDto> join(@RequestBody MemberJoinDto memberJoinDto) {
MemberDto savedMember = new MemberDto(1L, memberJoinDto.getMemberId());
URI location = URI.create("/api/members/" + savedMember.getMemberNo());
return ResponseEntity
.created(location)
.body(savedMember);
}
@DeleteMapping("/{memberNo}")
public ResponseEntity<Void> delete(@PathVariable Long memberNo) {
return ResponseEntity.noContent().build();
}
private MemberDto findMember(Long memberNo) {
if (memberNo == 1L) {
return new MemberDto(1L, "springUser");
}
return null;
}
}
다음 글에서는 요청 값 검증의 시작인 @Valid와 BindingResult에 대해 정리하겠습니다. 입력값이 비어 있거나 형식이 맞지 않을 때 Controller에서 어떻게 처리하는지 살펴보겠습니다.
'Tech Stack > Spring Boot' 카테고리의 다른 글
| [Spring] 26. @ExceptionHandler와 @ControllerAdvice (0) | 2026.07.18 |
|---|---|
| [Spring] 24. @Valid와 BindingResult (0) | 2026.07.17 |
| [Spring] 22. @ResponseBody와 @RestController (1) | 2026.07.15 |
| [Spring] 21. @RequestBody란? (0) | 2026.07.14 |
| [Spring] 20. @ModelAttribute와 DTO 바인딩 (0) | 2026.07.13 |