웹 개발›Spring MVC

요청과 응답

Spring MVC — 스프링 기반과 DI에 이은 두 번째 편이다. 요청 매핑부터 파라미터 바인딩, 응답 생성, Forward/Redirect까지 다룬다.


4. 요청 매핑 (@RequestMapping)

어노테이션

설명

@RequestMapping

URL과 메소드를 매핑. 클래스 레벨(기본 경로) + 메소드 레벨로 사용

@GetMapping

GET 요청 매핑

@PostMapping

POST 요청 매핑

@Controller
@RequestMapping("/members")          // 클래스 레벨: 기본 경로
public class MemberController {

    @GetMapping                      // GET /members
    public String list() { ... }

    @GetMapping("/{id}")             // GET /members/1
    public String detail(@PathVariable Long id) { ... }

    @PostMapping                     // POST /members
    public String save(...) { ... }
}

5. HTTP 요청 파라미터 받기

어노테이션

용도

@RequestParam

쿼리 파라미터·폼 데이터를 개별 변수로 받음

@PathVariable

URL 경로의 변수(/members/{id})를 받음

@ModelAttribute

요청 파라미터를 객체에 바인딩

@RequestBody

HTTP 메시지 바디(JSON 등)를 객체로 변환

// @RequestParam : /hello?username=kim&age=20
@GetMapping("/hello")
public String hello(@RequestParam String username,
                    @RequestParam int age) { ... }

// @ModelAttribute : 폼 데이터를 객체로 자동 바인딩
@PostMapping("/members")
public String save(@ModelAttribute Member member) { ... }

// @RequestBody : JSON 바디를 객체로 변환 (HTTP API)
@PostMapping("/api/members")
public String apiSave(@RequestBody Member member) { ... }

@RequestParam 옵션

@RequestParam은 기본적으로 파라미터가 없으면 오류(400) 다. 선택 파라미터·기본값은 옵션으로 제어한다.

@GetMapping("/members")
public String list(
        @RequestParam(required = false) String keyword,        // 없어도 됨 → null
        @RequestParam(defaultValue = "1") int page,            // 없으면 1 (defaultValue가 있으면 required 무의미)
        @RequestParam(name = "sort") String sortField) { ... } // 파라미터명과 변수명이 다를 때 name 지정

옵션

의미

required

필수 여부(기본 true). false면 없을 때 null

defaultValue

값이 없을 때 기본값. 지정하면 자동으로 선택 파라미터가 됨

name / value

요청 파라미터 이름(변수명과 다를 때 명시)

어노테이션을 생략하면

String·int 같은 단순 타입은 @RequestParam을, 객체 타입은 @ModelAttribute를 생략해도 스프링이 같은 방식으로 바인딩한다. 다만 학습·협업 단계에서는 의도를 드러내기 위해 명시하는 편이 좋다.


6. HTTP 응답

스프링 MVC가 응답을 만드는 방식은 세 가지다.

  1. 정적 리소스 — src/main/resources/static의 파일을 그대로 제공

  2. 뷰 템플릿 — 컨트롤러가 논리 뷰 이름(String)을 반환하면 ViewResolver가 Thymeleaf 템플릿을 렌더링

  3. 메시지 바디 — @ResponseBody를 붙이면 뷰를 거치지 않고 문자열/JSON을 바디에 직접 전달

// 뷰 템플릿 응답
@GetMapping("/members")
public String list(Model model) {
    model.addAttribute("members", memberService.findAll());
    return "members/list";          // templates/members/list.html
}

// 메시지 바디 응답 (JSON)
@GetMapping("/api/members")
@ResponseBody
public List<Member> apiList() {
    return memberService.findAll();
}

@RestController — 클래스 전체가 JSON 응답일 때

메서드마다 @ResponseBody를 붙이는 대신, 클래스에 @RestController 를 붙이면 모든 메서드가 자동으로 메시지 바디(JSON) 응답이 된다. 즉 @RestController = @Controller + @ResponseBody 다. 화면(HTML)을 그리는 컨트롤러는 @Controller, JSON을 내려주는 API 컨트롤러는 @RestController로 나눠 쓴다.

@RestController                  // 모든 메서드에 @ResponseBody 적용
@RequestMapping("/api/members")
public class MemberApiController {
    @GetMapping
    public List<Member> list() { return memberService.findAll(); }  // → JSON
}

(필터·인터셉터·예외 처리 편의 @RestControllerAdvice도 같은 원리로 @ControllerAdvice + @ResponseBody다.)

엔티티를 그대로 반환하는 건 학습용일 때만

위처럼 Member 엔티티를 JSON으로 직접 내보내면 비밀번호 노출·양방향 순환참조·LAZY 직렬화 오류 같은 문제가 생긴다. 실무에서는 DTO로 변환해 응답한다. Spring Data JPA — 엔티티와 DTO 분리에서 자세히 다룬다.


7. Forward 와 Redirect

요청을 받은 뒤 다른 자원으로 넘기는 두 가지 방식이다. "서버가 속으로 넘기느냐(Forward)" vs "브라우저에게 다시 요청하라고 시키느냐(Redirect)"가 본질적인 차이다.

Forward — 서버 내부 전달

서버 안에서 요청을 다른 자원(다른 컨트롤러·뷰 템플릿 등)으로 떠넘긴다. 브라우저는 이 사실을 전혀 모른다.

sequenceDiagram
    participant B as 브라우저
    participant S as 서버
    B->>S: 요청 1번
    Note over S: 서버 내부에서 다른 자원으로 전달<br/>(같은 요청 그대로 유지)
    S-->>B: 응답 (URL 변화 없음)
  • 요청은 1번. 브라우저 주소창 URL은 그대로다.

  • 같은 요청이므로 HttpServletRequest·요청 속성(request attribute)이 그대로 유지된다.

  • 스프링 MVC에서 컨트롤러가 반환하는 뷰 이름은 기본적으로 forward다. (return "members/list" → 해당 템플릿으로 포워딩해 렌더링)

Redirect — 클라이언트 재요청 유도

서버가 브라우저에게 "이 URL로 다시 요청해"라고 응답(302 상태 + Location 헤더)한다. 그러면 브라우저가 그 주소로 새 요청을 보낸다.

sequenceDiagram
    participant B as 브라우저
    participant S as 서버
    B->>S: 요청 1번
    S-->>B: 302 + Location 헤더
    Note over B: 브라우저가 Location의<br/>새 URL로 다시 요청
    B->>S: 요청 2번 (새 URL)
    S-->>B: 응답 (URL 바뀜)
  • 요청은 2번. 브라우저 주소창 URL이 바뀐다.

  • 요청이 새로 시작되므로 이전 요청의 request 속성은 사라진다. → 데이터를 넘기려면 별도 방법이 필요하다(아래 RedirectAttributes).

한눈에 비교

구분

Forward

Redirect

동작

서버 내부에서 다른 자원으로 전달

클라이언트에게 새 URL로 재요청하도록 응답

요청 횟수

1번

2번

URL 변화

없음

바뀜

request 속성 유지

유지됨

사라짐(새 요청)

HTTP 상태

그대로 (200 등)

302(+Location 헤더)

스프링 반환값

"뷰이름" 또는 "forward:/경로"

"redirect:/경로"

사용 예

화면 렌더링

폼 처리 후 페이지 이동(중복 제출 방지)

PRG 패턴 — 왜 폼 처리 후엔 Redirect인가

POST로 데이터를 저장한 뒤 그 응답으로 곧장 화면을 forward하면, 사용자가 새로고침(F5)할 때 브라우저가 직전 POST 요청을 그대로 재전송한다. → 중복 등록·중복 결제가 발생한다.

이를 막는 것이 PRG(Post-Redirect-Get) 패턴이다. POST 처리 후 결과 페이지로 redirect하면, 주소창이 GET URL로 바뀌어 새로고침해도 안전한 GET만 반복된다.

// PRG : POST 처리 후 GET 페이지로 redirect
@PostMapping("/members")
public String save(@ModelAttribute Member member) {
    memberService.save(member);
    return "redirect:/members";   // 저장 후 목록 페이지로 재요청 유도
}

Redirect로 데이터 전달 — RedirectAttributes

redirect는 새 요청이라 Model에 담은 데이터가 사라진다. 데이터를 넘기려면 RedirectAttributes를 쓴다.

@PostMapping("/members")
public String save(@ModelAttribute Member member, RedirectAttributes redirectAttributes) {
    Member saved = memberService.save(member);

    redirectAttributes.addAttribute("id", saved.getId());     // ① URL 쿼리/경로 변수로
    redirectAttributes.addFlashAttribute("status", "저장 완료"); // ② 1회성 flash 데이터
    return "redirect:/members/{id}";   // → /members/100?... (status는 URL에 안 보임)
}

메서드

전달 방식

특징

addAttribute

URL 경로 변수({id})·쿼리스트링

URL에 노출됨. 새로고침해도 유지

addFlashAttribute

세션에 잠깐 저장 후 다음 요청에서 1회 제공

URL에 안 보임. 한 번 읽으면 사라짐

정리

  • 화면을 그릴 때는 forward(뷰 이름 반환) — 같은 요청 안에서 데이터를 그대로 들고 렌더링.

  • 상태를 바꾼 뒤(POST/저장/삭제)는 redirect — 중복 제출을 막는 PRG.

  • redirect로 한 줄짜리 결과 메시지를 넘길 땐 URL을 더럽히지 않는 addFlashAttribute 가 깔끔하다.

마지막 수정 2026년 10월 22일