효율적인 .NET 개발을 위한 4가지 필수 라이브러리 소개

2014년에 마이크로소프트는 기존 .NET Framework의 오픈 소스 후속작인 .NET Core를 발표했습니다. 이 발표는 큰 변화였으며, 곧 .NET 소스 코드가 GitHub에 공개되었습니다. 마이크로소프트는 앞으로 모든 .NET 릴리스의 기초로 .NET Core를 활용하겠다고 밝혔고, 오픈 소스 기여는 .NET Foundation의 가이드 하에 이루어지게 되었습니다.

.NET Core는 큰 성공을 거두었고, 2020년에는 .NET Framework와 .NET Core가 하나의 오픈 소스 크로스 플랫폼 기술로 통합된 .NET 5가 출시되었습니다.

오픈 소스로 전환됨에 따라 .NET 플랫폼에는 활기 넘치는 커뮤니티가 형성되었습니다. 많은 뛰어난 개발자들이 고품질의 도구와 라이브러리를 게시해 개발자들의 일상 업무를 한결 쉽게 만들고 있습니다.

이번 글에서는 새 프로젝트를 시작할 때 꼭 설치하는 필수 라이브러리 4가지를 소개하고자 합니다.

1. Refit

.NET에서 HTTP 요청을 다루는 작업은 상당히 많은 수작업과 반복되는 코드를 요구합니다. 예를 들어 HttpClient 클래스를 직접 구현해 요청을 처리하는 코드는 다음과 같습니다

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
public class Program
{
private static readonly HttpClient client = new HttpClient();

public static async Task Main(string[] args)
{
var user = await GetUserAsync(123);
Console.WriteLine($"User Name: {user.Name}");
}

private static async Task<User> GetUserAsync(int userId)
{
// 필요한 경우 기본 주소와 기본 헤더를 설정합니다.
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

// HTTP 요청하기
var response = await client.GetAsync($"users/{userId}");
response.EnsureSuccessStatusCode();

// 응답 역직렬화
var responseBody = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<User>(responseBody);
}
}

이와 같은 구현 방식은 복잡하고, 반복적인 코드가 많아지기 쉽습니다.

Refit은 이런 작업을 간편하게 해주는 라이브러리입니다. 단순히 인터페이스를 정의하여 REST API를 사용하게 도와줍니다.

1
2
3
4
5
public interface IMyApi
{
[Get("/users/{userId}")]
Task<User> GetUserAsync(int userId);
}

그 후, 원하는 곳에 주입하여 다음과 같이 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UsersController : ControllerBase
{
private readonly IMyApi _myApi;

public UsersController(IMyApi myApi)
{
_myApi = myApi;
}

[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var user = await _myApi.GetUserAsync(id);
return Ok(user);
}
}

이렇게 하면 HTTP 요청 관련 코드가 간소화되어 유지보수가 용이하며, API 호출이 컴파일 시간에 타입 검사까지 지원됩니다.

2. Coravel

Coravel은 작업 스케줄링, 큐잉, 캐싱, 백그라운드 작업, 이벤트 브로드캐스팅과 같은 반복적이고 어려운 작업을 매우 쉽게 처리할 수 있도록 도와주는 훌륭한 라이브러리입니다. Coravel은 다양한 영역을 포괄하는 대규모 라이브러리이며, 특히 스케줄링 기능이 뛰어납니다.

일반적으로 시스템을 구축할 때 어떤 형태로든 반복 작업을 처리해야 할 경우가 많습니다. 예를 들어, 매 시간마다 제3자 시스템에 데이터를 전달하거나, 매일 자정에 데이터베이스 백업을 수행해야 하는 경우가 있을 수 있습니다. Coravel은 이러한 작업을 매우 간단하게 처리할 수 있는 설정 방식을 제공하며, 주요 클라우드 제공 업체의 솔루션보다 유지보수가 더 쉬운 편입니다.

Coravel에서 작업을 정의하려면 IInvocable 인터페이스를 상속하는 클래스를 만들어야 합니다. 이 클래스가 바로 Coravel이 다양한 애플리케이션 파트에서 사용할 수 있는 특정 작업을 나타내며, 주기적으로 실행될 비즈니스 로직을 넣는 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
using Coravel.Invocable;
using System;
using System.Threading.Tasks;

public class MyScheduledTask : IInvocable
{
public Task Invoke()
{
// 여기에 실행할 로직을 작성합니다
Console.WriteLine($"스케줄된 작업 실행 시간: {DateTime.Now}");
return Task.CompletedTask;
}
}

이제 Program.cs 파일에서 Coravel 스케줄러 서비스를 등록하고, 작업을 실행할 주기와 시점을 지정합니다.

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
32
33
34
35
36
37
38
39
40
using Coravel;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

// 서비스 등록
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Coravel 서비스 추가
builder.Services.AddScheduler();

// IInvocable 작업(MyScheduledTask) 등록
builder.Services.AddTransient<MyScheduledTask>();

var app = builder.Build();

// HTTP 요청 파이프라인 구성
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

// IInvocable 작업 스케줄 설정
var provider = app.Services;

provider.UseScheduler(scheduler =>
{
scheduler.Schedule<MyScheduledTask>().EveryMinute();
});

app.Run();

이렇게 설정이 완료됩니다. Coravel은 내부적으로 작업을 스케줄에 맞춰 실행할 코드를 자동으로 관리합니다. 또한, 원하는 주기로 정확하게 작업이 실행되도록 설정할 수 있으며, CRON 표현식도 사용할 수 있어 더욱 세부적인 스케줄 관리가 가능합니다.

3. FluentValidation

FluentValidation은 .NET 애플리케이션에서 데이터 유효성 검사를 간편하게 정의하고 적용할 수 있도록 해주는 인기 있는 라이브러리입니다. 전통적으로 .NET에서의 데이터 유효성 검사는 커스텀 로직을 클래스 내부에 추가하거나 유연성이 부족한 데이터 주석을 사용하는 방식으로 이루어졌습니다.

대신 FluentValidation을 사용하면, 검증 로직을 작성하는 부담을 줄이고 커스터마이징 가능한 유효성 검사를 위한 인터페이스를 제공합니다. 이는 번잡하고 반복적인 유효성 검사 코드를 작성하지 않아도 되며, 자연스러운 언어처럼 유효성 검사를 정의할 수 있어 코드 가독성이 크게 향상됩니다. 또한, 다른 개발자들이 향후 코드를 쉽게 이해하고 유지보수할 수 있게 만듭니다.

먼저 AbstractValidator 클래스를 상속하는 클래스를 정의하고, RuleFor 메서드를 통해 유효성 검사 규칙을 간단히 설정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Customer
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}

public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Name)
.NotEmpty().WithMessage("이름은 필수 항목입니다.");
RuleFor(customer => customer.Age)
.InclusiveBetween(18, 60).WithMessage("나이는 18세에서 60세 사이여야 합니다.");
RuleFor(customer => customer.Email)
.EmailAddress().WithMessage("유효하지 않은 이메일 주소입니다.");
}
}

FluentValidation에는 .EmailAddress(), .NotEmpty(), .GreaterThan(), .CreditCard()와 같은 편리한 기본 제공 메서드가 다수 포함되어 있으며, 필요에 따라 사용자 정의 유효성 검사 로직도 구현할 수 있습니다.

이제 정의된 CustomerValidator를 사용하여 필요한 곳에서 Customer 객체를 검증할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
var customer = new Customer { Name = "", Age = 25, Email = "invalid-email" };
var validator = new CustomerValidator();
var result = validator.Validate(customer);

if (!result.IsValid)
{
foreach (var failure in result.Errors)
{
Console.WriteLine($"속성 {failure.PropertyName}이(가) 유효성 검사를 통과하지 못했습니다. 오류: {failure.ErrorMessage}");
}
}

4. Polly

Polly는 .NET 애플리케이션의 강인성과 장애 처리 기능을 크게 향상시켜주는 인기 있는 라이브러리입니다. Polly를 사용하면 개발자가 재시도(retry) 로직, 회로 차단(circuit breaking), 타임아웃(timeout), 벌크헤드 격리(bulkhead isolation), 폴백(fallback) 등의 다양한 정책을 정의하여 소프트웨어의 안정성을 높일 수 있습니다.

소프트웨어를 작성하다 보면 예외와 일시적인 오류가 불가피하게 발생합니다. 예를 들어, 프로그램이 네트워크 타임아웃을 겪는 경우가 있을 수 있으며, 이러한 상황을 우아하게 처리하지 못하면 비즈니스에 중요한 애플리케이션 부분에 악영향을 미칠 수 있습니다. 이러한 상황에서 Polly가 매우 유용합니다. Polly의 정책은 모듈식으로 구성되어 있으며, 복잡한 오류 처리 시나리오도 대응할 수 있도록 조합할 수 있습니다.

예를 들어, 재시도 정책과 회로 차단 정책을 결합하여 강력한 오류 처리 전략을 만들 수 있습니다.

외부 API에서 데이터를 가져오는 서비스를 예로 들어, 일시적인 오류에도 잘 견딜 수 있도록 설정해 보겠습니다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.CircuitBreaker;
using Polly.Fallback;
using Polly.Retry;

class Program
{
static async Task Main(string[] args)
{
// 재시도 정책 정의
var retryPolicy = Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

// 회로 차단 정책 정의
var circuitBreakerPolicy = Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30));

// 폴백 정책 정의
var fallbackPolicy = Policy<HttpResponseMessage>
.Handle<BrokenCircuitException>()
.OrResult(r => !r.IsSuccessStatusCode)
.FallbackAsync(
new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("{\"message\": \"Fallback response\"}")
},
onFallbackAsync: async b =>
{
Console.WriteLine("폴백 로직 실행 중...");
});

// 정책 조합
var combinedPolicy = fallbackPolicy.WrapAsync(circuitBreakerPolicy).WrapAsync(retryPolicy);

using (var httpClient = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://external-api.com/data");

try
{
// 조합된 정책을 사용하여 요청 실행
HttpResponseMessage response = await combinedPolicy.ExecuteAsync(() => httpClient.SendAsync(request));

if (response.IsSuccessStatusCode)
{
Console.WriteLine("요청 성공!");
// 응답 처리
string data = await response.Content.ReadAsStringAsync();
Console.WriteLine(data);
}
else
{
Console.WriteLine("요청 실패. 상태 코드: " + response.StatusCode);
}
}
catch (Exception ex)
{
Console.WriteLine("예외 발생: " + ex.Message);
}
}
}
}

우선, 세 가지 정책을 정의했습니다: 재시도 정책, 회로 차단 정책, 그리고 폴백 정책입니다.

  • 재시도 정책은 HTTP 요청이 실패하여 성공하지 않은 상태 코드가 반환되면 최대 3회까지 재시도하도록 합니다. 재시도 간격은 지수 백오프(exponential backoff) 전략을 사용해 실패할 때마다 재시도 간격이 증가합니다.

  • 회로 차단 정책은 연속해서 세 번 실패하면 30초 동안 추가 시도를 중단하게 됩니다. 이로 인해 시스템에 회복할 시간을 주고 이후에 다시 시도하게 됩니다.

  • 마지막으로, 폴백 정책은 모든 재시도가 실패하고 회로가 여전히 열린 경우 기본 응답을 제공하여 애플리케이션이 계속 정상적으로 동작할 수 있도록 합니다.

이렇게 조합된 정책을 통해 HTTP 요청 실행 중 발생할 수 있는 오류나 예외를 강력하게 처리할 수 있는 시스템을 구축할 수 있습니다.

Share