ASP.NET LAB
ASP.NET Core 1.3 · 1부. 웹 개발 기초

요청과 응답 파이프라인

요청이 들어와 응답이 나가기까지의 길입니다.

예상 학습 시간 16분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 입문
개념 설명

요청은 줄지어 놓인 것들을 지나갑니다

1.2에서 builder.Build() 뒤에 놓는 것들을 미들웨어라고 불렀습니다. 요청이 들어오면 등록한 순서대로 그것들을 하나씩 지나가고, 마지막에 우리가 적은 처리 코드에 닿습니다.

중요한 것은 돌아오는 길도 있다는 점입니다. 미들웨어는 다음으로 넘기고 나서 그 아래가 모두 끝난 뒤에 다시 실행할 코드를 둘 수 있습니다. 그래서 미들웨어 하나는 요청을 두 번 만납니다. 들어갈 때 한 번, 나올 때 한 번입니다.

다음으로 넘기지 않을 수도 있습니다. 그러면 그 아래는 아예 실행되지 않습니다. 인증이 실패했을 때 본문을 만들지 않고 곧바로 401을 돌려주는 것이 이 방식입니다.

최소 예제

순서를 직접 보기

ASP.NET Core 의 미들웨어는 다음 처리기를 받아 새 처리기를 돌려주는 함수입니다. 그것을 겹쳐 감싸면 파이프라인이 됩니다. 아래는 그 부분만 떼어 낸 것이라 서버 없이도 돌아갑니다.

Program.cs
var pipeline = new List<Func<Handler, Handler>>();

void Use(Func<Handler, Handler> middleware) => pipeline.Add(middleware);

Use(next => async path => {
    Console.WriteLine("기록 : 들어갑니다");
    await next(path);
    Console.WriteLine("기록 : 나옵니다");
});

Use(next => async path => {
    if (path == "/admin") { Console.WriteLine("인증 : 막았습니다"); return; }
    Console.WriteLine("인증 : 통과");
    await next(path);
});

Handler last = path => { Console.WriteLine("본문 : " + path); return Task.CompletedTask; };

// 뒤에서부터 감쌉니다. 그래야 먼저 등록한 것이 바깥이 됩니다.
Handler app = last;
for (var i = pipeline.Count - 1; i >= 0; i--) app = pipeline[i](app);

await app("/");
Console.WriteLine();
await app("/admin");

delegate Task Handler(string path);
출력
기록 : 들어갑니다 인증 : 통과 본문 : / 기록 : 나옵니다 기록 : 들어갑니다 인증 : 막았습니다 기록 : 나옵니다

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

두 가지가 보입니다. 기록은 들어갈 때와 나올 때 두 번 찍혔고, /admin 에서는 본문이 아예 실행되지 않았습니다. 그런데도 기록의 "나옵니다" 는 찍혔습니다. 인증이 넘기지 않아도 그보다 바깥에 있는 것은 제 몫을 마칩니다.

상세 사용법

실제 코드에서는 이렇습니다

위와 같은 것을 ASP.NET Core 로 적으면 아래가 됩니다. path 자리에 HttpContext 가 오고, 다음 처리기는 next 로 넘어옵니다. 찍히는 순서는 위와 같습니다.

Program.cs
var app = builder.Build();

app.Use(async (context, next) => {
    Console.WriteLine("기록 : 들어갑니다");
    await next();
    Console.WriteLine("기록 : 나옵니다  상태 " + context.Response.StatusCode);
});

app.Use(async (context, next) => {
    if (context.Request.Path == "/admin") {
        Console.WriteLine("인증 : 막았습니다");
        context.Response.StatusCode = 401;
        return;
    }
    Console.WriteLine("인증 : 통과");
    await next();
});

app.MapGet("/", () => "안녕하세요.");

app.Run();
서버 콘솔
기록 : 들어갑니다 인증 : 통과 본문 : / 기록 : 나옵니다 상태 200 기록 : 들어갑니다 인증 : 막았습니다 기록 : 나옵니다 상태 401

이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 위의 예제가 같은 일을 합니다.

Use · Run · Map

파이프라인에 무언가를 놓는 방법이 셋입니다.

메서드하는 일
Use다음으로 넘길 수 있습니다. 넘기지 않으면 거기서 끝납니다.
Run다음이 없습니다. 파이프라인의 끝에 둡니다.
Map주소를 보고 나뉜 파이프라인으로 보냅니다. MapGet 도 여기에 듭니다.

순서가 곧 동작입니다

미들웨어는 적은 순서대로 실행되므로, 순서를 바꾸면 동작이 바뀝니다. 자리를 잘못 두어도 컴파일 오류가 나지 않기 때문에 돌려 봐야 압니다. 아래가 일반적인 차례입니다.

app.UseHttpsRedirection();   // http 로 오면 https 로 보냅니다
app.UseStaticFiles();        // 파일이면 여기서 끝냅니다(아래로 안 갑니다)
app.UseRouting();            // 어느 코드가 맡을지 정합니다
app.UseAuthentication();     // 누구인지 봅니다
app.UseAuthorization();      // 할 수 있는지 봅니다
app.MapControllers();        // 맡은 코드를 부릅니다

UseAuthorizationUseAuthentication 보다 앞서면 누구인지 모르는 채로 권한을 봅니다. 로그인한 사람도 거절당하는데, 오류는 나지 않아 원인을 찾기 어렵습니다.

상세 사용법

나오는 길에서는 늦은 것이 있습니다

돌아오는 길에 코드를 둘 수 있다고 해서 무엇이든 할 수 있는 것은 아닙니다. 응답은 머리글이 먼저 나가고 본문이 뒤따릅니다. 본문이 나가기 시작하면 머리글은 이미 보낸 뒤라 고칠 수 없습니다.

Program.cs
app.Use(async (context, next) => {
    await next();
    context.Response.Headers["X-Late"] = "늦었습니다";
});
서버 콘솔
fail: Microsoft.AspNetCore.Server.Kestrel[13] An unhandled exception was thrown by the application. System.InvalidOperationException: Headers are read-only, response has already started.

그런데 브라우저는 200을 받습니다. 응답이 이미 나간 뒤에 발생한 예외라 돌려줄 곳이 없기 때문입니다. 화면은 멀쩡한데 서버 기록에만 예외가 쌓이는 자리이므로, 나오는 길에 응답을 손대는 코드는 조심해서 두어야 합니다.

머리글을 꼭 붙여야 한다면 본문이 나가기 전에 붙이거나, OnStarting 으로 나가기 직전에 부를 코드를 등록합니다.

app.Use(async (context, next) => {
    context.Response.OnStarting(() => {
        context.Response.Headers["X-Early"] = "제때";
        return Task.CompletedTask;
    });

    await next();
});
버전 배지

UseRouting 을 적지 않아도 되는 까닭

.NET 6 부터

UseRoutingUseEndpoints 를 반드시 적어야 했습니다. 지금은 Map… 을 부르면 필요한 자리에 알아서 놓입니다. 그래서 1.2에서 본 네 줄짜리 Program.cs 에도 라우팅이 동작합니다.

직접 적어야 하는 때는 순서를 정해야 할 때입니다. 인증을 라우팅 뒤에 두어야 하는 경우가 그렇습니다. 적어 두면 그 자리에 놓이고, 적지 않으면 기본 자리에 놓입니다.

실습 문제

직접 해보기

1. 순서를 바꿔 보기 난이도 하

위 예제에서 기록과 인증의 등록 순서를 바꾸면 출력이 어떻게 달라질지 먼저 생각해보고, 코드를 고쳐 실행해 확인해보세요.

인증이 바깥이 되면, 인증이 넘기지 않을 때 기록은 아예 실행되지 않습니다.
// /admin 일 때 인증 : 막았습니다 // "기록 : 들어갑니다" 도 "기록 : 나옵니다" 도 찍히지 않습니다. // 인증이 바깥이라 그 아래가 통째로 실행되지 않기 때문입니다.
2. 걸린 시간 재기 난이도 중

가장 바깥에 미들웨어를 하나 더 놓아, 아래가 모두 끝나기까지 몇 번 지났는지 세어 출력해보세요. 들어갈 때와 나올 때 두 번 실행된다는 점을 사용합니다.

가장 바깥이 되려면 Use가장 먼저 불러야 합니다. await next(path) 앞뒤에 코드를 둡니다.
var count = 0; Use(next => async path => { count = 0; await next(path); Console.WriteLine("지난 곳 : " + count); }); // 나머지 미들웨어 안에서 count++ 를 합니다.
3. 순서를 잘못 둔 결과 짐작하기 난이도 상

app.UseStaticFiles()app.UseAuthentication() 뒤에 두면 무엇이 달라질지 적어보세요.

정적 파일 미들웨어는 파일을 찾으면 다음으로 넘기지 않고 거기서 끝냅니다.
// 앞에 두면(일반적인 자리) // 이미지·css 요청이 인증을 지나지 않고 곧바로 나갑니다. 빠릅니다. // 뒤에 두면 // 파일 하나를 받을 때마다 인증을 거칩니다. 느려지고, 인증을 // 요구하도록 설정해 두었다면 로그인 전에는 화면이 깨져 보입니다.
요약
  • 미들웨어는 등록한 순서대로 지나가고, 돌아오는 길에 한 번 더 지납니다.
  • 다음으로 넘기지 않으면 그 아래는 실행되지 않습니다. 인증 실패가 이 방식입니다.
  • Use 는 넘길 수 있고, Run 은 끝이며, Map 은 주소로 갈라 보냅니다.
  • 순서를 잘못 두어도 컴파일 오류가 나지 않습니다. 인증과 인가의 앞뒤가 대표적입니다.
  • 응답이 나가기 시작한 뒤에는 머리글을 고칠 수 없고, 그때 발생한 예외는 브라우저에 전달되지 않습니다.