컨트롤러는 웹 요청의 입구다
웹 애플리케이션에서 사용자는 주소를 입력하거나 버튼을 누른다. 그러면 브라우저는 서버에 요청을 보낸다. 스프링에서 이 요청을 가장 먼저 받는 자바 클래스가 컨트롤러다.
컨트롤러는 음식점의 주문 접수대와 비슷하다. 손님이 "김치찌개 하나 주세요"라고 말하면 접수대는 주문을 확인하고 주방에 전달한다. 웹에서도 마찬가지다. 사용자가 /posts로 요청하면 컨트롤러는 "게시글 목록을 보여 달라는 요청이구나"라고 판단하고 필요한 코드를 실행한다.
가장 작은 컨트롤러부터 보자.
package com.example.board.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloRestController {
@GetMapping("/hello")
public String hello() {
return "안녕하세요. Spring MVC입니다.";
}
}서버를 실행하고 브라우저에서 다음 주소로 들어가 보자.
http://localhost:8080/hello화면에 문자열이 그대로 보이면 성공이다. 여기서 중요한 부분은 두 가지다.
@RestController: 이 클래스가 웹 요청을 처리하는 클래스라고 스프링에 알려 준다.@GetMapping("/hello"):GET /hello요청이 오면 이 메서드를 실행하라고 연결한다.
즉, 컨트롤러는 URL과 자바 메서드를 연결하는 장소다.
참고로 이번 장에는 개념을 나누어 설명하기 위해 비슷한 URL을 사용하는 예제가 여러 번 나온다. 한 프로젝트에 예제 코드를 모두 그대로 붙여 넣으면 같은 요청을 처리하는 메서드가 둘 이상이라고 스프링이 판단할 수 있다. 따라 할 때는 예제 하나씩 실행하거나, 이전 예제 클래스의 URL을 바꾸거나, 잠시 주석 처리하면서 진행하자.
AI 생성 이미지
@Controller와 @RestController의 차이
스프링 MVC에서 자주 만나는 컨트롤러 애너테이션은 두 가지다.
| 애너테이션 | 주로 쓰는 상황 | 반환값의 의미 |
|---|---|---|
@Controller | HTML 화면을 보여 줄 때 | 템플릿 파일 이름 |
@RestController | JSON, 문자열 같은 데이터를 응답할 때 | 응답 본문 데이터 |
@RestController는 API 서버를 만들 때 많이 사용한다. 메서드가 반환한 값이 그대로 HTTP 응답 본문에 들어간다.
@RestController
public class PostApiController {
@GetMapping("/api/posts/count")
public int countPosts() {
return 3;
}
}GET /api/posts/count로 요청하면 응답 본문에 3이 들어간다. 화면 파일을 찾지 않는다.
반면 @Controller는 HTML 화면을 만들 때 사용한다. 이때 메서드가 반환하는 문자열은 응답 본문이 아니라 뷰 이름이다. 뷰는 사용자가 보는 화면 파일이라고 생각하면 된다.
package com.example.board.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class PostViewController {
@GetMapping("/posts-preview")
public String list(Model model) {
model.addAttribute("posts", List.of("첫 번째 글", "스프링 MVC 연습"));
return "posts/list";
}
}이 코드는 /posts-preview 요청이 오면 src/main/resources/templates/posts/list.html 파일을 찾아 화면으로 렌더링한다. model.addAttribute("posts", ...)는 화면에서 사용할 데이터를 담는 작업이다. 아직 list.html 파일을 만들지 않았다면 바로 실행했을 때 템플릿을 찾을 수 없다는 오류가 날 수 있다. 뒤에서 Thymeleaf 예제를 만들 때 이 흐름을 다시 확인할 것이다.
정리하면 이렇다.
화면을 반환하려면
@Controller데이터를 반환하려면
@RestController
게시판 API를 만들 때는 대부분 @RestController를 쓴다. 하지만 관리자 화면, 게시글 작성 폼 같은 HTML 페이지가 필요하면 @Controller도 함께 사용할 수 있다.
URL과 메서드를 연결하는 @GetMapping, @PostMapping
HTTP 요청에는 메서드가 있다. 여기서 메서드는 자바 메서드가 아니라 HTTP의 요청 방식이다.
GET: 데이터를 조회할 때 사용한다.POST: 새 데이터를 등록하거나 폼을 제출할 때 사용한다.
스프링은 @GetMapping, @PostMapping으로 HTTP 메서드와 URL을 자바 메서드에 연결한다.
package com.example.board.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/posts")
public class PostPracticeController {
@GetMapping
public String list() {
return "게시글 목록 조회";
}
@GetMapping("/new")
public String newForm() {
return "게시글 작성 화면 요청";
}
@PostMapping
public String create() {
return "게시글 등록 요청";
}
}@RequestMapping("/api/posts")는 클래스 전체에 공통 URL을 붙인다. 그래서 실제 주소는 다음처럼 만들어진다.
| 요청 | 실행되는 메서드 |
|---|---|
GET /api/posts | list() |
GET /api/posts/new | newForm() |
POST /api/posts | create() |
여기서는 @RestController를 사용했기 때문에 "게시글 작성 화면 요청" 같은 문자열도 실제 HTML 화면이 아니라 응답 본문으로 그대로 내려간다. 화면을 렌더링하려면 앞에서 본 것처럼 @Controller와 템플릿 파일을 사용해야 한다.
초보자가 자주 헷갈리는 점이 있다. 브라우저 주소창에 주소를 입력하면 기본적으로 GET 요청이 간다. 그래서 POST /api/posts는 주소창만으로 테스트하기 어렵다. 나중에 Postman, curl, HTML form, 프론트엔드 코드로 테스트하게 된다.
요청 파라미터 읽기: @RequestParam
사용자가 검색어를 입력하면 보통 URL 뒤에 값이 붙는다.
GET /api/posts/search?keyword=spring&page=1? 뒤에 붙은 keyword=spring&page=1 같은 값을 쿼리 파라미터라고 한다. 스프링에서는 @RequestParam으로 이 값을 읽는다. @RequestParam은 쿼리 파라미터나 폼 데이터를 컨트롤러 메서드의 파라미터에 연결하며, 기본적으로 값이 없으면 오류가 난다. 숫자 타입으로 선언하면 스프링이 가능한 범위에서 자동 변환도 해 준다.1
@GetMapping("/api/posts/search")
public String search(
@RequestParam String keyword,
@RequestParam(defaultValue = "1") int page
) {
return "검색어: " + keyword + ", 페이지: " + page;
}요청해 보자.
http://localhost:8080/api/posts/search?keyword=spring&page=2응답은 다음과 비슷하다.
검색어: spring, 페이지: 2page에는 defaultValue = "1"을 지정했다. 그래서 사용자가 page 값을 보내지 않아도 기본값 1이 들어간다.
http://localhost:8080/api/posts/search?keyword=spring이 요청도 정상 처리된다.
반대로 keyword를 빼고 요청하면 오류가 날 수 있다.
Required request parameter 'keyword' for method parameter type String is not present이 메시지는 "컨트롤러가 keyword를 필요로 하는데 요청에 없다"는 뜻이다. 꼭 필요한 값이면 그대로 두고, 선택 값이면 다음처럼 바꾼다.
@RequestParam(required = false) String keywordURL 경로에서 값 읽기: @PathVariable
이번에는 이런 주소를 생각해 보자.
GET /api/posts/10여기서 10은 게시글 번호다. 이렇게 URL 경로 자체에 들어 있는 값을 읽을 때는 @PathVariable을 사용한다. @PathVariable은 URL 템플릿의 변수 부분을 메서드 파라미터에 연결한다.2
@GetMapping("/api/posts/{postId}")
public String findById(@PathVariable("postId") Long postId) {
return postId + "번 게시글 조회";
}/api/posts/{postId}에서 {postId}는 "이 자리에 오는 값을 postId라는 이름으로 받겠다"는 뜻이다. 그리고 @PathVariable("postId")는 그 값을 자바 메서드의 postId 파라미터에 넣겠다는 뜻이다.
요청:
http://localhost:8080/api/posts/10응답:
10번 게시글 조회@RequestParam과 @PathVariable은 쓰임이 다르다.
| 방식 | 예시 | 어울리는 상황 |
|---|---|---|
@RequestParam | /posts?keyword=spring | 검색어, 페이지 번호, 정렬 조건 |
@PathVariable | /posts/10 | 특정 자원의 ID |
게시글 하나를 조회할 때는 /posts/10처럼 경로에 ID를 넣는 방식이 자연스럽다. 반면 검색 조건은 /posts?keyword=spring&page=1처럼 파라미터로 붙이는 방식이 자연스럽다.
여러 입력값을 객체로 받기: @ModelAttribute
게시글 작성 폼에는 값이 하나만 있지 않다.
제목
내용
작성자
이 값을 전부 @RequestParam으로 받으면 메서드가 길어진다.
@PostMapping("/api/posts/form")
public String create(
@RequestParam String title,
@RequestParam String content,
@RequestParam String writer
) {
return "등록: " + title;
}값이 세 개라면 괜찮지만, 열 개가 되면 읽기 어려워진다. 이럴 때 입력값을 담는 클래스를 만든다.
package com.example.board.dto;
public class PostCreateRequest {
private String title;
private String content;
private String writer;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
}그리고 컨트롤러에서 @ModelAttribute로 받는다.
@PostMapping("/api/posts/form")
public String create(@ModelAttribute PostCreateRequest request) {
return "등록된 제목: " + request.getTitle()
+ ", 작성자: " + request.getWriter();
}@ModelAttribute는 요청 파라미터를 객체에 묶어 준다. 예를 들어 요청에 title, content, writer라는 이름의 값이 있으면 스프링이 PostCreateRequest 객체를 만들고 같은 이름의 프로퍼티에 값을 채운다.3 이 예제에서는 setTitle(), setContent(), setWriter() 같은 세터 메서드를 통해 값이 들어간다고 이해하면 된다.
HTML 폼으로 보내면 이런 모습이다.
<form action="/api/posts/form" method="post">
<input type="text" name="title">
<textarea name="content"></textarea>
<input type="text" name="writer">
<button type="submit">등록</button>
</form>여기서 name="title"과 PostCreateRequest의 title 프로퍼티가 연결된다. 이름이 다르면 값이 들어가지 않는다.
한 가지를 미리 구분해 두자. @ModelAttribute는 주로 HTML 폼처럼 title=...&content=... 형태로 넘어오는 값이나 쿼리 파라미터를 객체로 묶을 때 사용한다. JSON 요청 본문을 객체로 받을 때는 보통 @RequestBody를 사용하며, 이 내용은 REST API를 만들 때 다시 다룬다.
초보 단계에서는 이렇게 기억하면 된다.
값 하나나 두 개를 간단히 받을 때:
@RequestParamURL 경로의 ID를 받을 때:
@PathVariable폼 입력값 여러 개를 객체로 받을 때:
@ModelAttribute
MVC 패턴은 역할을 나누는 방법이다
MVC는 세 단어의 앞글자다.
Model: 화면에 보여 줄 데이터
View: 사용자가 보는 화면
Controller: 요청을 받고 Model과 View를 연결하는 코드
비유하면 이렇다.
Controller: 주문을 받는 직원
Model: 주문서와 음식 재료
View: 손님에게 나가는 접시
역할을 나누는 이유는 단순하다. 한 클래스가 요청 처리, 데이터 준비, HTML 생성까지 모두 담당하면 금방 복잡해진다. 반대로 역할을 나누면 수정할 위치가 분명해진다.
게시글 목록 화면을 MVC 흐름으로 만들면 다음 순서가 된다.
사용자가
/posts로 요청한다.Controller가 요청을 받는다.
Controller가 게시글 목록 데이터를 Model에 담는다.
Controller가 View 이름을 반환한다.
템플릿 엔진이 Model 데이터를 사용해 HTML을 만든다.
브라우저가 완성된 HTML을 보여 준다.
스프링 MVC와 Thymeleaf를 함께 쓰면 컨트롤러가 Model에 데이터를 담고 뷰 이름을 반환하며, Thymeleaf 템플릿은 ${...} 문법으로 그 데이터를 꺼내 HTML을 만든다.4
먼저 Thymeleaf 의존성이 필요하다. build.gradle에 다음 의존성이 있는지 확인하자.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}컨트롤러는 다음처럼 작성한다.
package com.example.board.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class PostPageController {
@GetMapping("/posts")
public String list(Model model) {
List<String> posts = List.of("첫 번째 게시글", "MVC 패턴 연습");
model.addAttribute("posts", posts);
return "posts/list";
}
}이제 다음 위치에 HTML 파일을 만든다.
src/main/resources/templates/posts/list.html<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시글 목록</title>
</head>
<body>
<h1>게시글 목록</h1>
<ul>
<li th:each="post : ${posts}" th:text="${post}">게시글 제목</li>
</ul>
</body>
</html>model.addAttribute("posts", posts)로 담은 데이터는 템플릿에서 ${posts}로 꺼낸다. th:each는 목록을 하나씩 반복하고, th:text는 화면에 표시할 텍스트를 넣는다.
브라우저에서 다음 주소로 들어가 보자.
http://localhost:8080/posts게시글 두 개가 목록으로 보이면 MVC 흐름이 제대로 연결된 것이다.
흔한 오류와 해결법
1. 404 Not Found
요청한 URL을 처리할 컨트롤러 메서드가 없다는 뜻이다.
확인할 것:
@GetMapping주소가 맞는가?클래스 위의
@RequestMapping까지 합친 전체 URL이 맞는가?컨트롤러 클래스가 메인 애플리케이션 클래스의 하위 패키지에 있는가?
2. 405 Method Not Allowed
URL은 맞지만 HTTP 메서드가 다르다는 뜻이다. 예를 들어 컨트롤러는 @PostMapping인데 브라우저 주소창으로 GET 요청을 보낸 경우다.
3. Required request parameter 오류
@RequestParam으로 받기로 한 값이 요청에 없을 때 발생한다. 꼭 필요한 값인지 확인하고, 선택 값이면 required = false나 defaultValue를 사용한다.
4. 템플릿을 찾을 수 없음
@Controller에서 return "posts/list";를 했는데 templates/posts/list.html 파일이 없으면 오류가 난다. 브라우저에서는 500 에러처럼 보일 수 있다.
확인할 것:
Thymeleaf 의존성이 있는가?
파일 위치가
src/main/resources/templates아래인가?반환한 뷰 이름과 폴더/파일명이 일치하는가?
5. Ambiguous mapping 오류
같은 URL과 같은 HTTP 메서드를 처리하는 컨트롤러 메서드가 둘 이상 있을 때 발생한다. 예를 들어 GET /posts를 처리하는 메서드를 두 클래스에 각각 만들면 스프링은 어느 쪽을 실행해야 할지 결정할 수 없다.
확인할 것:
같은
@GetMapping또는@PostMapping주소가 중복되어 있지 않은가?클래스 위의
@RequestMapping까지 합쳤을 때 주소가 겹치지 않는가?예제 코드를 여러 개 복사했다면 이전 예제의 URL을 바꾸거나 주석 처리했는가?
이번 장 정리
이번 장에서는 Spring MVC로 웹 요청을 처리하는 기본기를 익혔다.
컨트롤러는 URL 요청을 자바 메서드에 연결한다.
@RestController는 문자열이나 JSON 같은 데이터를 응답할 때 사용한다.@Controller는 HTML 화면을 반환할 때 사용한다.@GetMapping,@PostMapping은 HTTP 메서드와 URL을 메서드에 연결한다.@RequestParam은 쿼리 파라미터나 폼 값을 읽는다.@PathVariable은 URL 경로에 들어 있는 값을 읽는다.@ModelAttribute는 여러 폼 입력값을 객체로 묶어 받는다.MVC는 Model, View, Controller의 역할을 나누어 코드를 이해하기 쉽게 만든다.
다음 장에서는 이 흐름을 화면보다 데이터 중심으로 확장한다. 게시글 목록 조회, 게시글 등록 같은 기능을 JSON으로 주고받는 REST API 형태로 만들어 볼 것이다.
참고
1@RequestParam :: Spring Framework
2PathVariable - spring-framework
3@ModelAttribute :: Spring Framework
4Spring MVC and Thymeleaf: how to access data from templates - Thymeleaf
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.