티스토리 뷰

이상한 미들웨어 등록 API

상식적으로 미들웨어라면 HttpContext랑 다음 할 일인 next() 함수를 패러미터로 받는 함수라고 생각합니다.

근데 사실 그 API는 확장 메서드로 짜놓은 래퍼구요, 실제 API는 두 번째 함수입니다. RequestDelegate만 두 개 받습니다. 뭐라구요?

S/O 자료

https://stackoverflow.com/a/49320350/4394750

두 번째 함수 내용을 봅시다. (위 링크의 답변에 있는 코드 카피 → 원본 소스)

public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
{
    return app.Use(next =>
    {
        return context =>
        {
            Func<Task> simpleNext = () => next(context);
            return middleware(context, simpleNext);
        };
    });
}

먼 소린지 머르겟다! → 정상입니다. 저도 머르겟어요.

해석을 시도하자

정리를 해봅시다. 너무 암호문스럽습니다. 여기 있는 타입들을 들여다봅시다.

타입명 타입 시그니처
보통 생각하는
미들웨어
Func<HttpContext, Func<Task>, Task>

가상의 함수로 다시 쓰자면
async Task Middleware(HttpContext context, Func<Task> next)
app.Use (2번)의 패러미터인
RequestDelegate
재해석하자면
async Task RequestDelegate(HttpContext context)
app.Use (2번)와
app.Use가 생각하는
미들웨어
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)

미들웨어를 가상의 함수로 다시 쓰자면
RequestDelegate /* current 반환 */ Middleware(RequestDelegate next);

어... 이제 좀 뭐가 보이기 시작하네요.

  1. RequestDelegate는 요청만 딱 처리합니다. 그래서 HttpContext context만 받죠.
  2. ASP.NET Core에서 생각하는 미들웨어는
    1. 다음(next) 요청 처리기(RequestDelegate)를 받아서
    2. 현재(current) 요청 처리기(RequestDelegate)를 반환하는 함수입니다.
  3. 상식적으로 생각하는 미들웨어는
    1. HttpContext, 즉 요청을 처리하는 함수와
    2. Task Next(void), 즉 다음 단계로 넘어가는 함수가 되겠습니다.

2로 3을 구현할 수 있습니다. 위 코드를 재해석해봅시다.

delegate Task RequestDelegate(HttpContext context);
//                                ↑ modifies HttpContext.
//                                  a simple request handler, only does that

// ASP.NET Core middleware, or can be called as "CreateMiddleware" function
delegate RequestDelegate New_Middleware(RequestDelegate next);
//             ↑ current req handler                     ↑ next req handler

delegate Task Old_Middleware(HttpContext context, Func<Task> next);
//               ↑ traditional middleware w/ ↑ context and    ↑ void next(void) function

public static IApplicationBuilder Use(this IApplicationBuilder app, Old_Middleware old_middleware_with_nextvoid)
{
    New_Middleware new_middleware = (RequestDelegate next_reqHandler) => {

        RequestDelegate cur_reqHandler = (/*HttpContexy*/ context) {
            
            Func<Task> simplified_next_reqHandler = () => next_reqHandler(context);
            old_middleware_with_nextvoid(/*HttpContext*/ context, simplified_next_reqHandler);
        };
        return cur_reqHandler;
    }
    
    return app.Use(new_middleware);
}

제맘대로 해석해봤습니다.

분석과 방법론

일반적으로 생각하는 미들웨어를 만들 때, 다음 리퀘스트 처리기는 HttpContext를 어떻게 패러미터로 받을까요? 즉, 왜 Next(void), 즉 HttpContext 없이 next를 호출할 수 있는걸까요?

프레임워크가 넣어주면 되지 않을까요? 저 같은 초보라면 그렇게 생각할 겁니다.
그렇지만 아닙니다. 관점이 달라서 헷갈리는 겁니다.

ASP.NET Core의 코드는 "미들웨어" 라는 관점이 아닌 "요청 처리기" 라는 관점에서 보고 있습니다. 미들웨어 다 떼놓고, 순차적으로 요청을 처리하는 처리기가 두 개 있다고 생각해봅시다.

그러면 저는 계속할 지 여부를 반환하게 짜고 밖에서 루프를 돌게 짜는 걸 먼저 생각해낼 겁니다.

// (문법에는 안 맞는 의사코드입니다)
delegate bool /* continue process? */ RequestProcessor(HttpContext context);

List<RequestProcessor> _handlers = new { Handler1, Handler2 };

HttpContext ctx;
foreach (var handler in _handlers) {
    bool cont = handler(ctx);
    if (!cont) { break; }
}

(현재 제가 만든 취미 프로젝트에서 위와 비슷한 접근방법을 취하고 있습니다. 바꿀거에요.)

하지만 단순하게 다음 요청 처리기람다 캡처해서 호출해도 됩니다. 이게 ASP.NET Core의 처리방법입니다.

// 역시 의사코드입니다. 찰떡같이 알아들으시기 바랍니다.
var NextHandler = (HttpContext ctx) { 복잡한 처리 }
var CurHandler = (HttpContext ctx) { 복잡한 처리; NextHandler(ctx); }

문제는 CurHandler를 만드려면 람다에 넣으려면 NextHandler가 필요하다는 점입니다. 그래서 ASP.NET Core의 Use() 패러미터가 함수입니다. NextHandler를 입력으로, CurHandler를 출력으로 하면 그 안에서 람다를 만들던 지지고 볶던 해도 상관이 없거든요.

IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)

아래는 이 메서드의 산출과정입니다. (접어놓음)

더보기
delegate Task 상식적인_미들웨어(HttpContext context, Func<Task> next);

IApplicationBuilder Use(this IApplicationBuilder app, 상식적인_미들웨어 상식웨어) {
    return app.Use(
        ASPNETCORE_미들웨어
    );
}
// ↓
IApplicationBuilder Use(this IApplicationBuilder app, 상식적인_미들웨어 상식웨어) {
    return app.Use(
        // ASPNETCORE_미들웨어는 RequestDelegate -> RequestDelegate 인 함수입니다
        (nextReqDel) => {
            return 현재_요청_처리기;
        }
    );
}
// ↓
IApplicationBuilder Use(this IApplicationBuilder app, 상식적인_미들웨어 상식웨어) {
    return app.Use(
        (nextReqDel) => {
            // RequestDelegate는 뭐다? (HttpContext) -> Task
            return async (curHttpContext) => { 무언가 };
        }
    );
}
// ↓
IApplicationBuilder Use(this IApplicationBuilder app, 상식적인_미들웨어 상식웨어) {
    return app.Use(
        (nextReqDel) => {
            // "상식적인" 미들웨어를 호출하고, next를 호출한 뒤 await exit해야죠
            return async (curHttpContext) => {
                상식웨어를_호출해야_한다;
                await nextReqDel_을_호출해야_한다;
                return;
            };
        }
    );
}
// ↓
IApplicationBuilder Use(this IApplicationBuilder app, 상식적인_미들웨어 상식웨어) {
    return app.Use(
        (nextReqDel) => {
            // nextReqDel에 httpContext를 넣는 걸 캡처해서 패러미터가 없는 함수(Func<Task>)로 변환
            Func<Task> nextReqDel호출기 = (nextHttpContext) => nextReqDel(nextHttpContext);
            
            return async (curHttpContext) => {
                상식웨어를_호출해야_한다;
                await nextReqDel_을_호출해야_한다;
                return;
            };
        }
    );
}
// ↓
IApplicationBuilder Use(this IApplicationBuilder app, Func<  HttpContext context,  Func<Task> next  > 상식웨어) {
    return app.Use(
        (nextReqDel) => {
            Func<Task> nextReqDel호출기 = (nextHttpContext) => nextReqDel(nextHttpContext);
            
            return async (curHttpContext) => {
                // nextReqDel호출기(= Task Next(void)) 는 저희가 직접 호출해서는 안 됩니다.
                // "상식적인" 미들웨어가 불러야 합니다. 패러미터로 전달해줍시다.
                await 상식웨어(curHttpContext, nextReqDel호출기);
                return;
            };
        }
    );
}
// ↓
IApplicationBuilder Use(this IApplicationBuilder app, Func<  HttpContext context,  Func<Task> next  > 상식웨어) {
    return app.Use(
        (nextReqDel) => {
            Func<Task> nextReqDel호출기 = (nextHttpContext) => nextReqDel(nextHttpContext);
            
            return (curHttpContext) => {
                // 어차피 Task를 반환하는데 굳이 async 함수라고 생각할 필요가 없습니다
                상식웨어(curHttpContext, nextReqDel호출기);
                return;
            };
        }
    );
}
// ↓
위 코드는 사실 asp.net core의 소스코드와 이름 빼고 동일하죠

next RequestDelegate는 언제 주어지는가

그럼 이런 의문이 들겁니다. 분명 NextHandler를 람다로 잡아서 호출하는데, 얘는 언제 주어지는거야?

정답은 빌드할 때.

  • [소스] Use 함수는 컴포넌트 목록(List<RequestDelegate>)에 RequestDelegate를 넣기만 합니다.
  • [소스] Build 함수에서 역순으로 돌면서 Next를 패러미터로 주면 해결되죠!

결과적으로 콜 스택이 미들웨어 호출 순서대로 쌓이게 되고, "상식적인 미들웨어" 에서 next()를 호출하지 않으면 콜 스택이 더 쌓이지 않고 순차적으로 리턴하게 됩니다.

왜 이런 짓을?

이제 이해는 되는데, 왜 이런 삽질(?)을 했을까요?

제 생각은 next() 를 호출할 시점을 자유롭게 조절하기 위함이 아닌가 싶습니다. 이 방법이 아니면 중간 지점에 다음 미들웨어를 호출한 뒤, 다음께 완료되면 돌아올 시점에 코드를 실행하기 어렵거든요.

저는 예전에 Next와 비슷하게 생긴 API를 만드려고 TaskCompletionSource를 활용한 적이 있는데 지금 생각해보면 이것도 다음 함수 완료 후에 코드를 실행할 수 없습니다. (있다고 해도 이미 머리가 아파서... 안 깔끔하잖아요.)

콜 스택은 아래 그림에서 빨강 -> 초록 -> 노랑 같은 순으로 쌓였다가 다시 풀리게 됩니다. (출처 MS문서)

https://twingyeo.kr/@sftblw/103368642512156374 (이 때 당시에는 살짝 오해해서 이해했었습니다)

다음으로 궁금한 것

그렇다면 클래스로 미들웨어를 만드는 건 어떻게 하는 걸까요?

제 생각엔 이런 미들웨어 구조가 있다면 이건 직접 만드는 게 크게 어려울 것 같진 않아요. 살짝 머리를 굴리긴 해야겠지만요.

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함