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

https://velog.io/@tkdwns414/Spring-Boot-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5%EC%9A%A9-ApiResponse-%EB%A7%8C%EB%93%A4%EA%B8%B0

https://deveun.tistory.com/entry/SpringBoot-%EA%B3%B5%ED%86%B5-Response-%ED%8F%AC%EB%A7%B7-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 

 

 

 

 

 

 

 

 

 

 

728x90