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 경로의 변수( |
@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 지정옵션 | 의미 |
|---|---|
| 필수 여부(기본 |
| 값이 없을 때 기본값. 지정하면 자동으로 선택 파라미터가 됨 |
| 요청 파라미터 이름(변수명과 다를 때 명시) |
어노테이션을 생략하면
String·int 같은 단순 타입은 @RequestParam을, 객체 타입은 @ModelAttribute를 생략해도 스프링이 같은 방식으로 바인딩한다. 다만 학습·협업 단계에서는 의도를 드러내기 위해 명시하는 편이 좋다.
6. HTTP 응답
스프링 MVC가 응답을 만드는 방식은 세 가지다.
정적 리소스 —
src/main/resources/static의 파일을 그대로 제공뷰 템플릿 — 컨트롤러가 논리 뷰 이름(
String)을 반환하면 ViewResolver가 Thymeleaf 템플릿을 렌더링메시지 바디 —
@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 등) |
|
스프링 반환값 |
|
|
사용 예 | 화면 렌더링 | 폼 처리 후 페이지 이동(중복 제출 방지) |
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에 안 보임)
}메서드 | 전달 방식 | 특징 |
|---|---|---|
| URL 경로 변수( | URL에 노출됨. 새로고침해도 유지 |
| 세션에 잠깐 저장 후 다음 요청에서 1회 제공 | URL에 안 보임. 한 번 읽으면 사라짐 |
정리
화면을 그릴 때는 forward(뷰 이름 반환) — 같은 요청 안에서 데이터를 그대로 들고 렌더링.
상태를 바꾼 뒤(POST/저장/삭제)는 redirect — 중복 제출을 막는 PRG.
redirect로 한 줄짜리 결과 메시지를 넘길 땐 URL을 더럽히지 않는
addFlashAttribute가 깔끔하다.