개요
ASP.NET Core의 app파이프라인 내에 속하는 MVC프레임웍 입장에서 response.body는 직접 건드릴 수 없는 대상입니다.
결국 파이프라인 상 각 액션과 필터 단계의 결론은 IActionResult고 이를 MVC프레임웍에서 최종적으로 response.body 스트림에 써주기 때문입니다.
이를 처리하려면 MVC프레임웍 밖에 있는 미들웨어 레벨에서 수정할 수 밖에 없습니다. 차근차근 이를 처리해보죠.
MVC프레임웍을 감쌀 미들웨어
미들웨어가 MVC프레임웍을 감싸기 위해서는 두 개의 방법이 있죠. app.UseMvc 앞 뒤로 각각 미들웨어를 배치하거나 앞에 하나의 미들웨어를 배치하되 await next()를 통해 MVC작동 이후까지 처리하는 방식입니다.
귀찮으니 next를 전후로 해서 처리하는 하나의 미들웨어를 작성하도록 하죠.
이 미들웨어는 반드시 UseMvc 앞에 와야 합니다.
app .Use(async (c, next) => { using(var buf = new MemoryStream()) { //1 var origin = c.Response.Body; c.Response.Body = buf; //2 await next(); //3 var s = new MemoryStream(); var sw = new StreamWriter(s); //4 await sw.WriteAsync("before"); //5 buf.Seek(0, SeekOrigin.Begin); await sw.WriteLineAsync(to<Stream, string>(buf)); //6 await sw.WriteAsync("after"); await sw.FlushAsync(); //7 s.Seek(0, SeekOrigin.Begin); s.WriteTo(origin); c.Response.Body = origin; } }) .UseMvc(configureRoutes); //8
위 미들웨어코드는 총 7단계에 걸쳐서 이뤄지므로 각 주석 항목에 맞춰서 설명합니다.
- 애당초 HttpContext.Response.Body 속성에 할당되어있는 스트림 객체는 특수한 녀석으로 전진 전용에 쓰기 밖에 안되는 스트림입니다. 따라서 나중에 조작하려면 이 기본 스트림을 MemoryStream으로 대체해둘 필요가 있습니다. 따라서 이 섹션에서는 원래 Body에 있는 스트림을 origin으로 잡아두고 Body에는 MemoryStream을 넣어주고 있습니다.
- 이 상태에서 await next() 를 통해 다음 미들웨어를 실행하게 됩니다. 따라서 반드시 다음 미들웨어가 MVC일 필요가 있다는 것입니다. 이제 3번 이후 상황은 이미 MVC의 필터와 액션을 거쳐 만들어진 IActionResult가 스트림데이터로 Body에 반영된 상태일 것입니다.
- Body에 할당되어있던 MemoryStream을 포함하여 앞뒤로 내용을 더 붙일 예정이므로 이를 위한 새로운 최종 결과용 MemoryStream을 새로 만들고 이에 대한 StreamWriter를 준비합니다.
- 우선 이 새로운 스트림에 원래 body 앞에 before라는 문자열을 추가합니다.
- 그리고 나서 Seek이 자유로운 MemoryStream의 특성을 이용해 MVC가 작성한 Response.Body의 내용을 복사해줍니다.
- 이제 마지막으로 그 내용 뒤에 after라는 문자열을 추가해주고 flush하여 정리합니다.
- 이렇게 만들어진 결과용 스트림을 0포지션으로 되돌리고 원래 잡아두었던 전진쓰기전용 origin 스트림에 복사하여 최종적인 Body에 다시 할당해줍니다.
- 앞 서 설명한 대로 이 미들웨어 다음에 반드시 MVC미들웨어가 와야합니다.
이러한 과정을 통해 만들어진 Body에 사후적인 컨텐츠 붙이기를 할 수 있습니다.
결론
공통기능을 구현하거나 프레임웍 수준에서 응답처리를 하고 싶거나, 혹은 액션이 일부 책임을 지고 프레임웍이 나머지 내용을 채워주는 등의 추상화를 진행하면 Body를 조작하고 싶은 니즈는 자연스럽게 생깁니다.
이를 MVC프레임웍 내의 필터나 액션 수준에서 구현하는 게 기존의 ASP.NET에서는 Response.Filter를 이용해 가능했지만 ASP.NET Core에서는 완전히 차단되어 미들웨어 수준으로 내려가야만 가능하게 되었습니다.
미들웨어에서 구현했다고는 하나 미들웨어는 결국 HttpContext를 알고 있습니다. 즉 context.Item 속성을 이용하면 얼마든 action이나 filter들이 body전후로 반영하고 싶은 내용을 미리 저장해두고 미들웨어가 이를 처리하는 식으로 작성할 수 있습니다.
예를 들어 위의 before와 after가 개입하는 과정을 아래와 같이 바꾸면 action과 filter도 참가할 수 있게 되겠죠.
app .Use(async (c, next) => { using(var buf = new MemoryStream()) { var origin = c.Response.Body; c.Response.Body = buf; await next(); var s = new MemoryStream(); var sw = new StreamWriter(s); //Item 내용을 반영해줌 await sw.WriteAsync(c.Items["before"] + ""); buf.Seek(0, SeekOrigin.Begin); await sw.WriteLineAsync(to<Stream, string>(buf)); //Item 내용을 반영해줌 await sw.WriteAsync(c.Items["after"] + ""); await sw.FlushAsync(); s.Seek(0, SeekOrigin.Begin); s.WriteTo(origin); c.Response.Body = origin; } }) .UseMvc(configureRoutes);
recent comment