마이크로서비스 아키텍처와 .NET 8의 통합
.NET 8은 마이크로소프트에서 제공하는 최신 크로스플랫폼 프레임워크로, 클라우드 네이티브 애플리케이션 구축에 최적화되어 있습니다. 이 버전은 성능 향상, 간소화된 API 설계, 그리고 Azure를 포함한 주요 클라우드 플랫폼과의 긴밀한 통합을 특징으로 하며, 특히 마이크로서비스 기반 시스템 개발에 유리한 환경을 제공합니다.
ASP.NET Core Web API와 MVC의 역할 구분
ASP.NET Core는 웹 애플리케이션과 API를 모두 지원하지만, 사용 목적에 따라 구성 방식이 다릅니다.
- Web API: JSON 기반 데이터를 반환하며, 프론트엔드 또는 다른 서비스와의 통신에 적합합니다.
- MVC: 뷰를 렌더링하여 HTML을 직접 반환하며, 전통적인 서버 사이드 렌더링 웹 앱에 사용됩니다.
Web API 예제: 제품 정보 제공
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
private static readonly List<ProductDto> _products = new()
{
new ProductDto { Id = 1, Name = "노트북", Price = 1500 },
new ProductDto { Id = 2, Name = "스마트폰", Price = 900 }
};
[HttpGet]
public IActionResult GetAll() => Ok(_products);
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var item = _products.FirstOrDefault(p => p.Id == id);
return item is not null ? Ok(item) : NotFound();
}
[HttpPost]
public IActionResult Create([FromBody] ProductDto product)
{
product.Id = _products.Max(p => p.Id) + 1;
_products.Add(product);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
public record ProductDto(int Id, string Name, decimal Price);
마이크로서비스 개발 절차
단일 애플리케이션을 여러 독립 실행형 서비스로 분해하려면 다음 단계를 따릅니다.
1. 서비스 경계 정의
비즈니스 도메인에 따라 기능을 분리합니다:
- 사용자 인증 서비스
- 학생 정보 관리 서비스
- 알림 전송 서비스
- 결제 처리 서비스
2. 데이터 분리 전략
각 마이크로서비스는 전용 데이터 저장소를 가져야 합니다. 예를 들어:
- 학생 서비스 → 학생 전용 데이터베이스
- 알림 서비스 → 알림 로그 DB
- 사용자 서비스 → 인증 정보 저장소
이를 통해 서비스 간 결합도를 줄이고 독립적인 배포가 가능해집니다.
3. API 게이트웨이 도입
외부 요청은 API Gateway를 통해 라우팅됩니다. 이 계층은 인증, 요청 분산, 부하 분산 등의 역할을 수행하며, 내부 서비스 구조를 외부로부터 은닉합니다.
4. 서비스 간 통신 방식
서로 다른 서비스는 다음과 같은 방법으로 소통합니다.
동기식: HTTP/gRPC
즉시 응답이 필요한 경우 사용됩니다. 예를 들어, 사용자 인증 후 학생 정보를 조회할 때:
public class StudentClient
{
private readonly HttpClient _client;
public StudentClient(HttpClient client) => _client = client;
public async Task<StudentDto?> FetchAsync(int studentId, string token)
{
_client.DefaultRequestHeaders.Authorization = new("Bearer", token);
var response = await _client.GetAsync($"/api/students/{studentId}");
return response.IsSuccessStatusCode
? await response.Content.ReadFromJsonAsync<StudentDto>()
: null;
}
}
비동기식: 메시지 브로커 (RabbitMQ/Kafka)
작업 완료 후 이벤트 기반 알림이 필요할 때 유용합니다. 예: 사용자 등록 완료 시 알림 발송
// 발행자 (User Service)
var message = JsonSerializer.Serialize(new { UserId = 123, Action = "Registered" });
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish("", "user.events", null, body);
// 구독자 (Notification Service)
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (_, ea) =>
{
var json = Encoding.UTF8.GetString(ea.Body.ToArray());
var evt = JsonSerializer.Deserialize<UserEvent>(json);
NotifyUser(evt!.UserId);
};
channel.BasicConsume("user.events", true, consumer);
5. 장애 복구 및 재시도 전략
네트워크 불안정 상황에 대비해 Polly 라이브러리를 활용한 재시도 정책을 적용할 수 있습니다.
services.AddHttpClient<StudentClient>()
.AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3,
attempt => TimeSpan.FromMilliseconds(200 * Math.Pow(2, attempt))));
6. 컨테이너화 및 오케스트레이션
Docker를 사용해 각 서비스를 컨테이너로 패키징하고, Kubernetes로 배포 및 확장 관리를 수행합니다.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
COPY ./bin/Release/net8.0/publish .
ENTRYPOINT ["dotnet", "StudentService.dll"]
고성능 데이터 접근: Dapper 활용
Entity Framework Core보다 낮은 오버헤드가 요구되는 경우, Dapper는 직접 SQL을 사용하면서도 객체 매핑을 지원하는 가벼운 ORM입니다.
public class PathRepository
{
private readonly string _connectionString;
public PathRepository(IConfiguration config)
{
_connectionString = config.GetConnectionString("Default");
}
public async Task<PathInfo> GetOptimalPathAsync(int routeId)
{
await using var conn = new SqlConnection(_connectionString);
const string sql = "SELECT * FROM RoutePaths WHERE RouteId = @RouteId";
return await conn.QuerySingleOrDefaultAsync<PathInfo>(sql, new { RouteId = routeId });
}
}
public record PathInfo(int RouteId, string Start, string End, decimal Distance);
로그 관리: NLog 설정 및 사용
애플리케이션 동작 상태를 추적하기 위해 NLog을 사용하면 다양한 출력 위치(file, database, console 등)로 로그를 기록할 수 있습니다.
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public async Task ProcessRoute(RouteData data)
{
Log.Info("경로 계획 시작: {Start} → {End}", data.Start, data.End);
try
{
var result = await CalculateOptimalPath(data);
Log.Info("경로 계획 완료. 거리: {Distance}km", result.Distance);
}
catch (Exception ex)
{
Log.Error(ex, "경로 계산 중 오류 발생");
throw;
}
}
설정 파일(nlog.config)을 통해 출력 형식, 필터링, 타겟 등을 정의할 수 있습니다.
의존성 주입(DI)을 통한 유연한 설계
.NET의 기본 DI 컨테이너를 사용하면 클래스 간 결합도를 낮추고 테스트 용이성을 높일 수 있습니다.
public interface ILoggingService
{
void Write(string message);
}
public class FileLoggingService : ILoggingService
{
public void Write(string message) => File.AppendAllText("log.txt", message + "\n");
}
// Program.cs
builder.Services.AddScoped<ILoggingService, FileLoggingService>();
builder.Services.AddTransient<RoutePlanner>();
이렇게 등록된 서비스는 생성자 주입을 통해 자동으로 제공됩니다.
Entity Framework Core로 간편한 데이터 조작
복잡한 쿼리보다 모델 중심 개발이 적합한 경우 EF Core를 사용합니다.
public class SchoolContext : DbContext
{
public DbSet<Student> Students { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("Server=.;Initial Catalog=SchoolDb;Integrated Security=true");
}
// 사용 예
using var context = new SchoolContext();
context.Students.Add(new Student { Name = "김민수", Age = 17 });
context.SaveChanges();
코드 기반 마이그레이션을 통해 데이터베이스 스키마 변경도 버전 제어와 함께 관리할 수 있습니다.