MVC 패턴과 컨트롤러·라우팅
주소를 코드에 잇습니다.
주소가 늘면 나누어 둘 자리가 필요합니다
2.1의 방식은 주소와 함수를 곧바로 이었습니다. 주소가 몇 개일 때는 그것이 가장 읽기 쉽습니다. 그런데 수십 개가 되면 Program.cs 하나가 길어지고, 같은 대상을 다루는 것들이 흩어집니다.
MVC 는 그것을 세 자리로 나눕니다.
- Model — 다루는 데이터와 규칙입니다. 2.3에서 봅니다.
- View — 화면(HTML)을 그립니다. 2.4에서 봅니다.
- Controller — 요청을 받아 무엇을 할지 정하고 결과를 돌려줍니다. 이 단원이 다룹니다.
데이터만 돌려주는 API 라면 View 는 사용하지 않습니다. 그래도 컨트롤러로 나누는 값은 남습니다. 같은 대상을 다루는 주소가 한 파일에 모이기 때문입니다.
지금 보고 계신 이 학습 화면도 컨트롤러 하나가 맡고 있습니다 (LearnController). 목차·단원·버전 이력이 그 안에 함께 있습니다.
컨트롤러 하나
2.1의 할일 API 를 컨트롤러로 옮긴 것입니다. 먼저
Program.cs 에서 두 줄을 적습니다. 1.2에서 본 경계가
그대로입니다. 등록은 Build() 앞, 배치는 뒤입니다.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 앞 — 무엇을 갖출지 var app = builder.Build(); app.MapControllers(); // 뒤 — 요청이 지날 길 app.Run();
컨트롤러는 따로 파일을 둡니다. 폴더 이름은 Controllers 가 관례이지만 규칙은 아닙니다. 어디에 두어도 찾아냅니다.
using Microsoft.AspNetCore.Mvc; [ApiController] [Route("[controller]")] public class TodosController : ControllerBase { private static readonly List<Todo> _todos = [ new(1, "우유 사기", false), new(2, "책 반납", true) ]; [HttpGet] public IEnumerable<Todo> List() => _todos; [HttpGet("{id:int}")] public ActionResult<Todo> Get(int id) => _todos.FirstOrDefault(t => t.Id == id) is { } found ? found : NotFound(); [HttpPost] public ActionResult<Todo> Add(Todo todo) { _todos.Add(todo); return CreatedAtAction(nameof(Get), new { id = todo.Id }, todo); } }
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 터미널에서
dotnet run 으로 띄운 뒤 불러 보십시오.
[Route("[controller]")] 의 대괄호가
클래스 이름에서 Controller 를 뗀 것으로
바뀝니다. TodosController 이므로
Todos 입니다. 위의 Location
머리글에 대문자 Todos 가 보이는 것이 그 까닭입니다.
주소를 맞출 때는 대소문자를 가리지 않지만, 만들어 낼 때는 이 표기가 그대로
나갑니다.
ApiController 를 붙이는 것과 붙이지 않는 것
[ApiController] 는 API 를 만들 때 되풀이하던 일을
대신해 줍니다. 그중 가장 큰 것이 들어온 값이 옳은지 먼저 보는
것입니다. 붙이지 않으면 그 검사가 없습니다.
둘에 같은 것을 보내 보았습니다. id 자리에 숫자가 아닌 글자를 담은 JSON 입니다.
// 400 { "title": "One or more validation errors occurred.", "status": 400, "errors": { "$.id": ["The JSON value could not be converted to Todo…"] } }
// 200 { "id": 0, "title": null, "done": false }
오른쪽이 위험합니다. 잘못된 요청이 오류 없이 지나가 기본값으로
채워진 채 저장됩니다. 부르는 쪽은 성공한 줄 알고, 나중에 값이 비어 있는 것을 보고
원인을 찾게 됩니다. API 를 만든다면 [ApiController] 를
붙이십시오.
그 밖에 해 주는 것들입니다.
- 복합 형식 매개 변수를 본문에서 읽는 것으로 봅니다.
[FromBody]를 적지 않아도 됩니다. - 오류 응답을 정해진 모양(ProblemDetails)으로 맞춥니다.
- 주소를 어트리뷰트로 적도록 요구합니다. 관례 라우팅은 사용할 수 없습니다.
ControllerBase 와 Controller
물려받을 것이 둘입니다. 이름이 비슷하지만 하는 일이 다릅니다.
| 기반 형식 | 언제 |
|---|---|
| ControllerBase | 데이터만 돌려줄 때입니다. 화면을 그리는 기능이 없어 가볍습니다. |
| Controller | 화면(HTML)을 함께 그릴 때입니다. View() 를 사용할 수 있습니다. |
Controller 가
ControllerBase 를 물려받은 것이므로, 화면이 필요 없는데
Controller 를 물려받아도 동작은 합니다. 다만 사용하지
않을 것을 함께 들이게 됩니다.
주소를 잇는 방법이 둘입니다
위에서 사용한 것은 어트리뷰트 라우팅입니다. 주소를 그 메서드 바로 위에 적습니다. 어느 주소가 어디로 가는지 그 자리에서 보입니다.
다른 하나는 관례 라우팅입니다. 주소의 모양을 Program.cs 에 한 번 정해 두고, 컨트롤러에는 적지 않습니다.
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// ─────────────────────────────
public class HomeController : ControllerBase {
public string Index() => "Home/Index 입니다.";
public string About() => "Home/About 입니다.";
public string Detail(int? id) => "Home/Detail id=" + (id?.ToString() ?? "(없음)");
}
{controller=Home} 의 =Home 이
기본값이라, 아무것도 적지 않은 / 도
Home/Index 로 갑니다.
{id?} 의 물음표는 없어도 된다는
표시입니다.
| 어트리뷰트 | 관례 | |
|---|---|---|
| 주소를 적는 곳 | 메서드 위 | Program.cs 한 곳 |
| 맞는 자리 | API | 화면을 그리는 웹앱 |
| ApiController | 사용합니다 | 사용할 수 없습니다 |
둘을 한 프로젝트에 함께 둘 수 있습니다. 화면은 관례로, API 는 어트리뷰트로 두는 것이 흔한 모양입니다.
언제부터 있었나
[ApiController] 와
ActionResult<T> 가 이때 들어왔습니다. 그
전에는 값이 옳은지 ModelState.IsValid 로 메서드
안에서 직접 보고, 400을 손으로 돌려주어야 했습니다.
MVC 자체는 그보다 훨씬 오래된 것이라 인터넷 예제가 많습니다. 그런데
ASP.NET Core 가 아닌 옛 ASP.NET MVC 의 것이 섞여 있습니다.
System.Web.Mvc 를 사용하는 것은 옛것입니다.
지금은 Microsoft.AspNetCore.Mvc 입니다.
직접 해보기
2.1의 MapDelete 를 컨트롤러 메서드로 옮겨보세요.
없는 번호면 404, 지웠으면 204입니다.
[HttpDelete("{id:int}")] 이고, 204는 NoContent() 입니다./todos 대신 /api/todos 로 받도록 고쳐보세요. 클래스 이름은 그대로 두고 한 줄만 고칩니다.
[ApiController] 를 뗀 채로 잘못된 JSON 을 보내면
200이 나오고 값은 기본값으로 채워집니다. 어트리뷰트를 붙이는 것
말고 이런 요청을 걸러 낼 방법을 하나 적어보세요.
- 컨트롤러는 같은 대상을 다루는 주소를 한 파일에 모읍니다.
AddControllers()는Build()앞에,MapControllers()는 뒤에 둡니다.[ApiController]가 없으면 잘못된 요청이 오류 없이 기본값으로 채워집니다.- 데이터만 돌려줄 때는
ControllerBase, 화면을 그릴 때는Controller입니다. - 주소는 메서드 위(어트리뷰트) 또는 Program.cs 한 곳(관례)에 적습니다. 함께 사용할 수 있습니다.