ASP.NET LAB
ASP.NET Core 2.2 · 2부. MVC / Minimal API

MVC 패턴과 컨트롤러·라우팅

주소를 코드에 잇습니다.

예상 학습 시간 18분 난이도 기초
개념 설명

주소가 늘면 나누어 둘 자리가 필요합니다

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 가 관례이지만 규칙은 아닙니다. 어디에 두어도 찾아냅니다.

Controllers/TodosController.cs
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);
    }
}
받은 응답
GET /todos 200 [{"id":1,"title":"우유 사기","done":false},…] GET /Todos 200 ← 대소문자를 가리지 않습니다 GET /todos/1 200 {"id":1,"title":"우유 사기","done":false} GET /todos/99 404 GET /todos/abc 404 POST /todos 201 Location: http://localhost:5421/Todos/3

이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 터미널에서 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() 를 사용할 수 있습니다.

ControllerControllerBase 를 물려받은 것이므로, 화면이 필요 없는데 Controller 를 물려받아도 동작은 합니다. 다만 사용하지 않을 것을 함께 들이게 됩니다.

상세 사용법

주소를 잇는 방법이 둘입니다

위에서 사용한 것은 어트리뷰트 라우팅입니다. 주소를 그 메서드 바로 위에 적습니다. 어느 주소가 어디로 가는지 그 자리에서 보입니다.

다른 하나는 관례 라우팅입니다. 주소의 모양을 Program.cs 에 한 번 정해 두고, 컨트롤러에는 적지 않습니다.

Program.cs · HomeController.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() ?? "(없음)");
}
받은 응답
/ 200 Home/Index 입니다. /home 200 Home/Index 입니다. /home/about 200 Home/About 입니다. /home/detail/7 200 Home/Detail id=7 /home/detail 200 Home/Detail id=(없음) /home/nothere 404

{controller=Home}=Home 이 기본값이라, 아무것도 적지 않은 /Home/Index 로 갑니다. {id?} 의 물음표는 없어도 된다는 표시입니다.

어트리뷰트관례
주소를 적는 곳메서드 위Program.cs 한 곳
맞는 자리API화면을 그리는 웹앱
ApiController사용합니다사용할 수 없습니다

둘을 한 프로젝트에 함께 둘 수 있습니다. 화면은 관례로, API 는 어트리뷰트로 두는 것이 흔한 모양입니다.

버전 배지

언제부터 있었나

ASP.NET Core 2.1 부터

[ApiController]ActionResult<T> 가 이때 들어왔습니다. 그 전에는 값이 옳은지 ModelState.IsValid 로 메서드 안에서 직접 보고, 400을 손으로 돌려주어야 했습니다.

MVC 자체는 그보다 훨씬 오래된 것이라 인터넷 예제가 많습니다. 그런데 ASP.NET Core 가 아닌 옛 ASP.NET MVC 의 것이 섞여 있습니다. System.Web.Mvc 를 사용하는 것은 옛것입니다. 지금은 Microsoft.AspNetCore.Mvc 입니다.

실습 문제

직접 해보기

1. 지우는 주소 옮기기 난이도 하

2.1의 MapDelete 를 컨트롤러 메서드로 옮겨보세요. 없는 번호면 404, 지웠으면 204입니다.

어트리뷰트는 [HttpDelete("{id:int}")] 이고, 204는 NoContent() 입니다.
[HttpDelete("{id:int}")] public IActionResult Remove(int id) { var found = _todos.FirstOrDefault(t => t.Id == id); if (found is null) return NotFound(); _todos.Remove(found); return NoContent(); }
2. 주소 앞에 api 붙이기 난이도 중

/todos 대신 /api/todos 로 받도록 고쳐보세요. 클래스 이름은 그대로 두고 한 줄만 고칩니다.

대괄호 자리표 앞에 글자를 그대로 붙일 수 있습니다.
[Route("api/[controller]")] // 클래스 이름을 바꾸면 주소가 함께 바뀝니다. 이름을 고정하고 싶으면 // 자리표를 사용하지 않고 그대로 적습니다. [Route("api/todos")]
3. 조용히 지나간 요청 찾기 난이도 상

[ApiController] 를 뗀 채로 잘못된 JSON 을 보내면 200이 나오고 값은 기본값으로 채워집니다. 어트리뷰트를 붙이는 것 말고 이런 요청을 걸러 낼 방법을 하나 적어보세요.

값이 옳은지에 대한 판정은 이미 만들어져 있습니다. 다만 그 결과를 아무도 보지 않았을 뿐입니다.
public ActionResult<Todo> Add(Todo todo) { if (!ModelState.IsValid) return BadRequest(ModelState); _todos.Add(todo); return Ok(todo); } // [ApiController] 가 대신해 주던 것이 이것입니다. 메서드마다 적어야 // 하고, 한 곳이라도 빠지면 그 자리만 조용히 지나갑니다.
요약
  • 컨트롤러는 같은 대상을 다루는 주소를 한 파일에 모읍니다.
  • AddControllers()Build() 앞에, MapControllers() 는 뒤에 둡니다.
  • [ApiController] 가 없으면 잘못된 요청이 오류 없이 기본값으로 채워집니다.
  • 데이터만 돌려줄 때는 ControllerBase, 화면을 그릴 때는 Controller 입니다.
  • 주소는 메서드 위(어트리뷰트) 또는 Program.cs 한 곳(관례)에 적습니다. 함께 사용할 수 있습니다.