Spring
[Spring] @Controller와 @RestController, 공통 응답 포맷
s_ih_yun
2025. 10. 20. 23:50
728x90

[Next.js] Thymeleaf 기반 구조 Next.js로 리팩토링 : Next.js 준비 및 설정 / Docker 컨테이너화 / Nginx 프록시 설정
이전 Thymeleaf로 간단한 페이지를 제공하던 프로젝트에서 Next.js 프론트를 추가하면서
백엔드 컨트롤러를 @Controller에서 @RestController로 변경했습니당
그러면서 JSON 응답의 일관성을 위해 공통 응답 포맷 적용ㄱ
1. @Controller
- 전통적은 Spring MVC의 컨트롤러 어노테이션
- 주로 View(화면)을 반환하기 위해 사용
- @ResponseBody 어노테이션을 활용하여 JSON 형태 데이터 반환 가능
@Controller
@RequestMapping("/ingredients")
public class IngredientController {
...
@PostMapping("/update")
public String update(@Valid @ModelAttribute("form") IngredientForm form,
BindingResult br,
RedirectAttributes ra,
Model model) {
if (br.hasErrors()) {
model.addAttribute("ingredients", svc.listAllOrdered());
return "ingredientList";
}
svc.update(form);
ra.addFlashAttribute("msg", "저장되었습니다.");
return "redirect:/ingredients";
}
...
2. @RestController
- Restful Web Service에서 사용되는 컨트롤러 어노테이션
- @Controller + @ResponseBody가 합쳐진 형태로, JSON 형태의 객체 데이터 반환
- 객체를 ResponseEntity로 감싸서 반환
@RestController
@RequestMapping("/ingredients")
public class IngredientController {
...
@PostMapping(
path = "/update",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<?> update(@Valid @RequestBody IngredientForm form,
BindingResult br) {
if (br.hasErrors()) {
return ResponseEntity.badRequest().body(br.getAllErrors());
}
svc.update(form);
return ResponseEntity.ok().build();
}
...
3. 공통 응답 포맷 사용하기
3.0. 사용 이유
- 다음과 같은 일관된 백엔드 응답을 제공하고자 할 때, Controller의 리턴 타입을 반복적으로 작성해야 한다..!
- 코드 중복을 피해 공통 응답 코드를 작성해두고 반환하자
{
"status": "success",
"data": {
}
"meta": null
}
3.1. [BE] API Response DTO
- 다음과 같이 공통 Response Class 작성
- 공통 ExceptionHandler도 작성하여 Exception 발생 시에도 유형에 따라 처리하자
package com.syun.posleep.dto.response;
import java.util.Map;
public record ApiResponse<T>(
String status,
T data,
Map<String, Object> meta
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>("success", data, null);
}
public static <T> ApiResponse<T> success(T data, Map<String,Object> meta) {
return new ApiResponse<>("success", data, meta);
}
}
- 다음과 같이 컨트롤러의 응답에 사용
@RestController
@RequestMapping("/ingredients")
public class IngredientController {
...
@GetMapping
public ApiResponse<List<IngredientSheetRow>> getPage() {
List<IngredientSheetRow> list = svc.listAllOrdered();
return ApiResponse.success(list);
}
...
3.2. [FE] 공통 API 응답 타입
- 공통 API 응답 타입 선언
// 공통 API 응답 타입 (frontend/src/types/api.ts)
export type ApiResponse<T> = {
status: string; // "success" | "error" 등
data: T; // 실제 데이터
meta?: Record<string, unknown>; // 페이징, 총 개수 등 부가 정보
};
- 다음과 같이 응답타입 사용
// 목록 조회 (frontend/src/app/ingredients/page.tsx)
useEffect(() => {
(async () => {
try {
setLoading(true);
const res = await fetch(`${BASE}/ingredients`, { cache: 'no-store' });
if (!res.ok) throw new Error();
const json: ApiResponse<Ingredient[]> = await res.json();
...
📌 References
https://mangkyu.tistory.com/49
https://backendcode.tistory.com/213#google_vignette
728x90