출처 : Strangler Fig Architectural Pattern in C# - Code Maze (code-maze.com)
Application 개발의 공통 문제
앱 디자인 시, 현재 use cases에 집중하지만 향후 확장성을 예측해서 여분의 공간을 남기는 노력을 한다. 기존 코드와 기능에 커다란 영향 없이 새로운 use cases를 지원하기 위해 노력한다. 하지만, 이러한 영향을 항상 피할 수 있는 것은 아니다.
첫번째 문제를 고려해보자. 커다란 수정사항이 있는 API 앱이 있다. APP의 여러 버전을 지원해야 한다.
두번쨰 문재는 .NET6에서 운영중인 동일한 API가 있는데, .NET7 기능을 사용하고 싶을 수 있다. 종종 전체 앱을 업그레이드할만한 시간과 Risk 감수력이 없을 수 있다.
이러한 2가지 문제에 대해 strangler fig patten이 도움을 줄 수 있다.
Strangler Fig Architecture가 어떻게 도움이 될 수 있는가?
.NET 예를 계속해보자. 앱은 아래와 같이 동작한다.
이제, 존재하는 앱을 수정하는 대신에 모든 기능을 커버하는 새로운 앱을 만든다. 기존 코드를 .NET 7앱에 복사할 수 있다.
고객이 다른 2 APIs에 대해 말한다. 이때 stringler fig architecture가 도움이 된다. 우리는 고객이 알지 못한 채, 기존 기능과 새 기능을 제공할 수 있다. APIs 앞에 facade를 소개함으로 할 수 있다.
고객은 하나의 앱에 대해 계속 이야기한다. Stringler Facade는 URL에 따라 적절한 API를 가리킨다.
기존 기능에 risk를 주지 않고 새로운 기술을 사용할 수 있게 한다. 또한 시간에 따라 점진적으로 기존 기능을 새 시스템으로 마이그레이션 할 수 있다.
"strangler fig"라는 용어는 식물의 종에서 유래했다. 기존의 나무에 뿌리를 내리고 영양분을 흡수해 결국 죽게 만드는 종이다. 무서운 소리일 수 있지만 application architecture에서는 좋은 의미이다. 우리는 점진적으로 오래된 시스템을 대체하길 원한다.
이어지는 section에서 어떻게 이 pattern을 구현할 수 있는지 알아보자.
Projects 생성
"LegacyApplication", "ModernApplication" webapi project를 생성하자.
dotnet new webapi -o LegacyApplication
dotnet new webapi -o ModernApplication
"Tests"라는 xunit project도 생성하자.
dotnet new xunit -o Tests
"StranglerFacade" minimal API project 생성하자.
dotnet new web -o StranglerFacade
StranglerFacade project에 Proxy package를 설치한다.
dotnet add package Yarp.ReverseProxy
우리가 하려는 것은 LegacyApplication과 새로운 기능이 추가된 ModernApplication을 user가 눈치채지 못하게 기존과 동일한 port로 서비스하다가 모든 기능을 ModernApplication으로 옮긴다음 LegacyApplication을 더 이상 사용하지 않는 것이다. 그리고 ModernApplication만 사용하도록 유지보수하면 된다.
1. LegacyApplication을 실행하자.
http://localhost:5131/weatherforecast
기본 템플릿에서 제공하는 url으로 아래와 같이 화면 확인이 가능하다.
2. ModernApplication에 새로운 Controller를 추가하자.
RainForecastController.cs
using Microsoft.AspNetCore.Mvc;
namespace ModernApplication.Controllers;
public record RainForecast(DateOnly Date, double Rainfall);
[ApiController]
[Route("[controller]")]
public class RainForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public RainForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<RainForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new RainForecast(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.NextDouble()
)).ToArray();
}
}
실행해보자.
http://localhost:5140/rainforecast
3. Tests xunit project에 LegacyApplication을 test하는 파일을 추가하자.
WeatherForecastLiveTest.cs
using System.Net.Http.Json;
namespace Tests;
public class WeatherForecastLiveTest
{
[Fact]
public async Task WhenSendingAGetRequestToTheWeatherForecastEndpoint_ThenWeatherDataIsReturned()
{
//Arrange.
var httpClient = new HttpClient
{
BaseAddress = new Uri("http://localhost:5131")
};
//Act.
var weatherForecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("weatherforecast");
//Assert.
Assert.NotNull(weatherForecasts);
Assert.NotEmpty(weatherForecasts);
foreach(var weatherForecast in weatherForecasts)
{
Assert.True(weatherForecast.Date > DateTime.MinValue);
Assert.True(weatherForecast.TemperatureC != 0);
Assert.True(weatherForecast.TemperatureF != 0);
}
}
}
public record WeatherForecast(DateTime Date, int TemperatureC, int TemperatureF, string? Summary);
dotnet test를 실행하면 테스트 통과됨 확인할 수 있다.
Strangler Fig Facade 설정
user(여기서는 xunit project)가 기존 legacy project host( http://localhost:5131/weatherforecast )에서 /rainforecast endpoint에 접속하려면 2 API's 앞에 facade를 설정해야 한다. 여러기술이 있지만 여기서는 단순하게 YARP package를 사용하자.
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
appsettings.json 파일에 ReverseProxy section을 구성하자.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"weather": {
"ClusterId": "legacyapp",
"Match": {
"Path": "weatherforecast"
}
},
"rain": {
"ClusterId": "modernapp",
"Match": {
"Path": "rainforecast"
}
},
"catchall": {
"ClusterId": "modernapp",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"legacyapp": {
"Destinations": {
"destinations1": {
"Address": "http://localhost:5131/"
}
}
},
"modernapp": {
"Destinations": {
"destinations1": {
"Address": "http://localhost:5140/"
}
}
}
}
}
}
2개의 다른 clusters를 가리키는 route를 설정했다. catchall은 새로운 기능이 자동으로 새로운 application으로 향하도록 한다.
StranglerFacade project를 실행해보자.
localhost
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
Proxying to http://localhost:5140/rainforecast HTTP/2 RequestVersionOrLower no-streaming
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
Received HTTP/1.1 response 200.
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
Proxying to http://localhost:5131/weatherforecast HTTP/2 RequestVersionOrLower no-streaming
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
Received HTTP/1.1 response 200.
Strangler가 traffic을 적절한 downstream application으로 연결한다. 하지만 xunit은 기존의 5131 host를 사용하고 있다. post를 바꾼다.
Port 변경처리
각 프로젝트의 launchSettings.json파일을 아래와 같이 port 수정한다.
Strangler의 port를 5106 -> 5131
Legacy의 port를 5131 -> 5139
Legacy의 appsettings.json에서 legacy application port도 5131에서 5139로 수정한다.
이제 Strangler는 5131 port에서 동작한다.
이제 user는 마치 하나의 application인 것처럼, legacy modern projects에 둘다 접속할 수 있다.
Strangling the Legacy Application (기존 application 목졸라 죽이기!?)
우리는 modern applicaion을 사용하기로 결정했다. 우리는 legacy를 더 이상 원하지 않는다. 다음 스텝은?
우선, 기존 코드를 새 project로 migrate한다. 여기서는 기본 template으로 project를 만들었기 떄문에 legacy application의 기능이 modern application에도 존재한다. 다음 단계인 LegacyApplication을 지워보자.
StranglerFacade project의 appsettings.json파일을 수정하자.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"catchall": {
"ClusterId": "modernapp",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"modernapp": {
"Destinations": {
"destinations1": {
"Address": "http://localhost:5140/"
}
}
}
}
}
}
많이 깔끔해졌다. 우리는 이제 2 clusters가 필요하지 않는다. 특정 routes도 필요하지 않는다. weatherforecast, rainforecast 모두 modern app으로 서비스하기 때문이다.
facade project 실행해보자. 5131port에서 동작하고 xunit도 정상 통과된다.
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
Proxying to http://localhost:5140/rainforecast HTTP/2 RequestVersionOrLower no-streaming
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
Received HTTP/1.1 response 200.
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
Proxying to http://localhost:5140/weatherforecast HTTP/2 RequestVersionOrLower no-streaming
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
Received HTTP/1.1 response 200.
우리는 성공적으로 legacy application을 strangled 했다. 더 이상 유지보수할 필요도 없고 새로운 기능은 modern application에서 작업하면 된다. strangler facade도 one application으로만 proxy하므로 facade도 이젠 필요없다.
삭제하고, port를 modern application port로 업데이트 하면 된다.
Strangler Fig 사용 시, 고려할 점
이 architecture를 강력한 반면, 한계점과 사용하지 말아야 할 때를 이해하는 것이 중요하다.
첫번째로, facade가 down되면 모든 기능이 down된다.
두번째로, new application은 공통형식을 사용해다 한다.
이 예에서, legacy 는 one route만 있으므로 strangler fig architecture 없이 바로 modern으로 교체하는 것이 낫다.
Strangler Fig는 완벽히 점검하는 것이 불가능한 상당한 크기의 legacy application을 다룰 때 가장 효과적이다.
strangler pattern의 또 다른 좋은 점은, 하나에 국한되지 않는다는 것이다. new application이 원하는 것이 아니라 판단되면 또 다른 strangler를 소개할 수 있다.
결론
strangler fig architecture가 기존 기능에 대한 risk없이 별도 영역에 새로운 기능을 생성하고 사용하는데 어떻게 쓰이는지 데모해보았다. 이 design pattern과 YARP reverse proxy와 함께 두 hosts로부터 하나의 host로 response를 제공하는지도 살펴보았다. 우리는 기존 application의 변경에 따른 버전처리를 구현할 필요가 없다. 기존 소스 수정없이, 새로운 기술을 사용해보고 싶을때 이것은 강력한 선택사항이 된다.
'Software design pattern' 카테고리의 다른 글
C#] Observer Design Pattern(관찰자 디자인 패턴) (1) | 2024.01.10 |
---|---|
C#] Pipes and Filter Architectural Pattern (0) | 2024.01.09 |
ASP.NET Core - MVC] Repository Pattern (0) | 2023.04.25 |
댓글