안녕하세요😊 오늘은 C#의 System.Threading.Channels를 활용하여 생산자-소비자 패턴을 구현하는 방법을 알아보겠습니다!

🎈 System.Threading.Channels란? System.Threading.Channels는 .NET에서 제공하는 고성능 채널(Channel) 기반의 데이터 전송 라이브러리입니다. 이를 활용하면 비동기적으로 데이터를 안전하게 주고받을 수 있습니다.

📌 주요 특징:

  • 비동기 데이터 전송 ✨: Task 기반의 비동기 프로그래밍 지원
  • 생산자-소비자 패턴 구현 용이 🏭: 여러 개의 생산자와 소비자가 데이터를 주고받을 수 있음
  • 채널의 크기 조절 가능 📏: 유연한 용량 설정과 대기 모드 지원

💡 단일 생산자가 5개의 Task를 생성하고 소비자가 이를 처리하는 패턴 구현 아래 예제에서는 단일 생산자가 5개의 Task를 생성하여 데이터를 추가하고, 소비자가 이를 읽어 처리하는 방식으로 프로그램을 설계하겠습니다!

 

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // 채널 생성 (용량 20으로 설정)
        var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(20)
        {
            FullMode = BoundedChannelFullMode.Wait // 채널이 가득 차면 대기
        });
        
        // 취소 토큰 (10초 후 작업 취소용)
        using var cts = new CancellationTokenSource();
        cts.CancelAfter(TimeSpan.FromSeconds(10));
        
        // 생산자 실행 (5개의 Task 등록)
        var producerTask = RunProducer(channel.Writer, cts.Token);
        
        // 소비자 실행
        var consumerTask = RunConsumer(channel.Reader, cts.Token);
        
        try
        {
            await Task.WhenAll(producerTask, consumerTask);
            Console.WriteLine("모든 작업이 정상적으로 완료되었습니다.");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("작업이 취소되었습니다.");
        }
    }
    
    static async Task RunProducer(ChannelWriter<int> writer, CancellationToken token)
    {
        var tasks = new List<Task>();
        var random = new Random();
        
        // 5개의 Task를 실행하여 데이터 생성
        for (int i = 0; i < 5; i++)
        {
            int producerId = i + 1;
            tasks.Add(Task.Run(async () =>
            {
                for (int j = 0; j < 20; j++)
                {
                    token.ThrowIfCancellationRequested();
                    await Task.Delay(random.Next(100, 500), token);
                    int item = j * 100 + producerId;
                    await writer.WriteAsync(item, token);
                    Console.WriteLine($"생산자 {producerId}: 항목 {item} 생산");
                }
            }, token));
        }
        
        await Task.WhenAll(tasks);
        writer.Complete();
    }
    
    static async Task RunConsumer(ChannelReader<int> reader, CancellationToken token)
    {
        var tasks = new List<Task>();
        var random = new Random();
        
        // 5개의 소비자 Task 실행
        for (int i = 0; i < 5; i++)
        {
            int consumerId = i + 1;
            tasks.Add(Task.Run(async () =>
            {
                while (await reader.WaitToReadAsync(token))
                {
                    while (reader.TryRead(out int item))
                    {
                        await Task.Delay(random.Next(200, 800), token);
                        Console.WriteLine($"소비자 {consumerId}: 항목 {item} 처리 완료");
                    }
                }
            }, token));
        }
        
        await Task.WhenAll(tasks);
    }
}

 

 

🔹 코드 설명

  1. 채널 생성 🏗️
    • Channel.CreateBounded<int>(new BoundedChannelOptions(20))을 사용해 용량 20인 채널을 생성합니다.
    • FullMode = BoundedChannelFullMode.Wait를 설정하여 채널이 가득 찼을 때 대기하도록 합니다.
  2. 생산자(Producer) 구현 🚀
    • RunProducer에서 5개의 Task를 생성하여 데이터를 채널에 추가합니다.
    • 각 생산자는 20개의 항목을 랜덤한 시간 간격을 두고 생성합니다.
  3. 소비자(Consumer) 구현 📦
    • RunConsumer에서 5개의 Task를 생성하여 데이터를 병렬로 읽어 처리합니다.
    • 데이터가 존재하면 TryRead를 통해 데이터를 읽어 처리합니다.
  4. 취소 및 예외 처리 🚦
    • CancellationTokenSource를 사용해 10초 후 자동 취소하도록 설정합니다.
    • OperationCanceledException을 처리하여 작업이 정상적으로 종료되도록 합니다.

결과 예시

생산자 1: 항목 101 생산
생산자 2: 항목 201 생산
소비자 3: 항목 101 처리 완료
소비자 1: 항목 201 처리 완료
...
모든 작업이 정상적으로 완료되었습니다.

이제 하나의 생산자가 5개의 Task를 등록하여 데이터를 생성하고 소비자가 5개의 Task를 동시에 처리하는 방식으로 개선되었습니다! 🎉

안녕하세요😊 오늘은 .NET Core에서 컨트롤러의 동작을 조정하고 재사용 가능한 로직을 구현할 때 유용한 ActionFilterAttribute에 대해 알아보겠습니다.

🎈 ActionFilterAttribute란?

ActionFilterAttribute.NET Core에서 제공하는 액션 필터(Action Filter) 기능을 구현할 수 있는 속성(attribute)입니다. 컨트롤러나 액션 메서드에 적용하여 액션 실행 전후에 원하는 동작을 수행할 수 있습니다.

📌 주요 사용 예시:

  • 요청(Request) 전/후 로깅 📝
  • 실행 시간 측정 ⏱
  • 인증 및 권한 검사 🔐
  • 입력값 검증 🧐

ActionFilterAttribute를 활용하면 중복되는 로직을 필터로 빼서 코드의 재사용성과 유지보수성을 높일 수 있습니다!


⚙️ ActionFilterAttribute의 작동 방식

ActionFilterAttribute는 컨트롤러와 액션의 실행 흐름을 가로채서 특정 로직을 수행할 수 있도록 해줍니다. 이를 위해 다음 두 가지 주요 메서드를 오버라이딩하여 동작합니다.

🛠️ 주요 메서드

1️⃣ OnActionExecuting(ActionExecutingContext context)

  • 액션 메서드 실행 전에 호출됩니다.
  • 요청 정보를 검증하거나, 필요한 사전 작업을 수행할 수 있습니다.
  • 예를 들어, 로그인 세션이 없으면 요청을 차단하고 로그인 페이지로 리디렉트할 수 있습니다.

2️⃣ OnActionExecuted(ActionExecutedContext context)

  • 액션 메서드 실행 후에 호출됩니다.
  • 실행된 결과를 변경하거나, 후처리 작업을 수행할 수 있습니다.
  • 예를 들어, API 요청의 실행 시간을 로깅하거나, 특정 조건에서 추가적인 응답 데이터를 설정할 수 있습니다.

📌 일상적인 비유

이 개념을 일상생활에 비유하면 레스토랑에서의 주문 과정과 비슷합니다. 🍽️

  • OnActionExecuting: 주문을 받기 전에 직원이 고객의 예약 여부를 확인하는 단계
  • 컨트롤러 액션 실행: 주문한 음식을 요리하는 과정
  • OnActionExecuted: 음식이 나온 후 고객의 만족도를 확인하고 추가 요청을 받는 과정

이처럼 액션 실행 전후에 필요한 검증이나 후처리를 수행할 수 있는 것이 ActionFilterAttribute의 핵심 기능입니다.


🔑 로그인 세션 확인 필터 구현하기

예제로 로그인 세션을 확인하는 커스텀 액션 필터를 만들어 보겠습니다.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Http;

public class LoginSessionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var session = context.HttpContext.Session;
        if (session.GetString("UserId") == null)
        {
            // 로그인 세션이 없으면 로그인 페이지로 리디렉션
            context.Result = new RedirectToActionResult("Login", "Account", null);
        }
    }
}

📌 필터 적용하기

위에서 만든 LoginSessionFilter를 컨트롤러나 액션 메서드에 적용하면, 로그인 세션이 없는 경우 자동으로 로그인 페이지로 이동하도록 할 수 있습니다.

[LoginSessionFilter]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

이제 HomeControllerIndex 액션을 호출할 때 로그인 세션이 없으면 로그인 페이지로 리디렉트됩니다!


🎯 간단한 로깅 필터 예제

다음은 액션 실행 전후로 로깅하는 필터 예제입니다.

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

public class LoggingFilter : ActionFilterAttribute
{
    private readonly ILogger<LoggingFilter> _logger;

    public LoggingFilter(ILogger<LoggingFilter> logger)
    {
        _logger = logger;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation("액션 실행 전: {ActionName}", context.ActionDescriptor.DisplayName);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation("액션 실행 후: {ActionName}", context.ActionDescriptor.DisplayName);
    }
}

컨트롤러에 적용하면 로그에 액션 실행 전후의 정보가 남게 됩니다. 👀

[LoggingFilter]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

🎯 ActionFilterAttribute vs Middleware 차이점

Action FilterMiddleware

컨트롤러와 액션에 초점 전체 HTTP 요청 파이프라인에 초점
특정 컨트롤러/액션에 적용 모든 요청에 대해 실행 가능
MVC 전용 기능 MVC뿐만 아니라 API, Static Files 등 모든 요청 처리 가능

필터는 특정 컨트롤러나 액션에만 적용할 수 있어 세밀한 제어가 가능하고, 미들웨어는 전역적으로 동작하여 모든 요청을 처리할 수 있습니다. 상황에 맞게 적절한 방법을 선택하면 됩니다! 😊


📌 정리 및 마무리

오늘은 .NET Core에서 ActionFilterAttribute를 활용하여 액션 실행 전후에 특정 로직을 추가하는 방법을 알아보았습니다.

💡 핵심 정리: ✅ OnActionExecuting, OnActionExecuted를 활용하여 사전/사후 처리 가능 ✅ 로그인 세션 검증, 로깅 등 다양한 활용 가능 ✅ 미들웨어와 비교하여 컨트롤러/액션 단위로 적용 가능 ✅ 일상 속 레스토랑 주문 과정과 비슷한 흐름을 가짐

ActionFilterAttribute를 활용하면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 🚀

앞으로도 유용한 .NET Core 관련 글을 계속 공유하겠습니다. 감사합니다! 😊

 

참고

ActionFilterAttribute 클래스 (System.Web.Mvc) | Microsoft Learn

📌 .NET에서 Dependency Injection(DI) 기초 가이드

안녕하세요😊
오늘은 .NET에서 제공하는 의존성 주입(Dependency Injection, DI)에 대해 깊이 있게 다뤄보겠습니다.

DI는 현대적인 .NET 애플리케이션에서 유지보수성과 확장성을 높이는 핵심 개념입니다.
이 글을 끝까지 읽으시면 DI의 개념부터 고급 활용법까지 완벽히 이해하실 수 있을 거예요!


1. 의존성 주입(DI)이란?

🔹 1.1 의존성 문제와 DI의 필요성

개발을 하다 보면 클래스 간의 의존성(Dependency)이 강해지는 경우가 많습니다.
예를 들어, OrderService에서 PaymentService를 직접 생성하면 다음과 같은 문제가 발생합니다.

public class OrderService
{
    private readonly PaymentService _paymentService;

    public OrderService()
    {
        _paymentService = new PaymentService(); // 강한 결합 (Tightly Coupled)
    }
}

문제점

  • OrderService가 PaymentService의 구체적인 구현을 직접 알고 있어야 함
  • PaymentService의 변경이 OrderService에도 영향을 줌
  • 테스트가 어렵다! → PaymentService를 Mocking하기 어려움

이러한 문제를 해결하기 위해 의존성 주입(DI)이 등장했습니다.

🔹 1.2 DI 적용 후

DI를 적용하면 다음과 같이 코드를 개선할 수 있습니다.

public class OrderService
{
    private readonly IPaymentService _paymentService;

    public OrderService(IPaymentService paymentService) // 생성자 주입
    {
        _paymentService = paymentService;
    }
}

DI의 장점

객체 간 결합도 감소 → 변경이 유연해짐
Mocking을 쉽게 적용 가능단위 테스트(Unit Test) 가능
유지보수성과 확장성이 높아짐


2. .NET의 DI 컨테이너 사용법

🔹 2.1 기본 DI 컨테이너 설정

.NET에서는 내장된 DI 컨테이너(IServiceCollection)를 제공합니다.
서비스 등록은 Program.cs에서 아래처럼 하면 됩니다.

var builder = WebApplication.CreateBuilder(args);

// 서비스 등록
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IPaymentService, PaymentService>();

var app = builder.Build();
app.Run();

🔹 2.2 서비스 등록 방법

.NET DI 컨테이너에서는 세 가지 수명 주기(Lifetime)를 지원합니다.

수명 주기메서드특징

Transient AddTransient<TInterface, TImplementation>() 요청 시마다 새 인스턴스 생성
Scoped AddScoped<TInterface, TImplementation>() HTTP 요청당 하나의 인스턴스 유지
Singleton AddSingleton<TInterface, TImplementation>() 애플리케이션 종료 시까지 동일한 인스턴스 사용

수명 주기 예제

builder.Services.AddTransient<IProductService, ProductService>();  // 요청마다 새 객체
builder.Services.AddScoped<IUserService, UserService>();          // 요청 내에서 유지
builder.Services.AddSingleton<ILoggerService, LoggerService>();   // 앱 실행 동안 동일 객체

3. DI 컨테이너의 동작 원리

🔹 3.1 DI 컨테이너 내부 원리

DI 컨테이너는 다음 3단계 과정으로 동작합니다.

1️⃣ 서비스 등록 → IServiceCollection을 사용해 인터페이스-구현체를 등록
2️⃣ 객체 생성 → IServiceProvider가 의존성을 분석하여 객체를 생성
3️⃣ 주입(Injection) 실행 → 필요한 곳에 인스턴스를 전달

🔹 3.2 IServiceProvider 활용

IServiceProvider를 직접 활용해 수동으로 인스턴스를 생성할 수도 있습니다.

var serviceProvider = new ServiceCollection()
    .AddSingleton<ILoggerService, LoggerService>()
    .BuildServiceProvider();

var logger = serviceProvider.GetRequiredService<ILoggerService>();
logger.Log("DI 컨테이너 직접 사용");

 


4. DI를 활용한 패턴과 실전 예제

🔹 4.1 Factory 패턴과 DI 활용

Factory 패턴을 활용하면 동적으로 객체를 생성할 수 있습니다.

public interface ICar { void Drive(); }
public class BMW : ICar { public void Drive() => Console.WriteLine("BMW Driving..."); }

public class CarFactory
{
    private readonly IServiceProvider _serviceProvider;
    public CarFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;

    public ICar CreateCar() => _serviceProvider.GetRequiredService<ICar>();
}

🔹 4.2 Middleware에서 DI 사용

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerService _logger;

    public LoggingMiddleware(RequestDelegate next, ILoggerService logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        _logger.Log("Request Started");
        await _next(context);
        _logger.Log("Request Ended");
    }
}

public void Configure(IApplicationBuilder app)
{
    app.UseMiddleware<LoggingMiddleware>();
}

🔹 4.3 HostedService에서 DI 사용

public class MyBackgroundService : BackgroundService
{
    private readonly ILogger<MyBackgroundService> _logger;
    public MyBackgroundService(ILogger<MyBackgroundService> logger) => _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Background task running...");
            await Task.Delay(1000, stoppingToken);
        }
    }
}

builder.Services.AddHostedService<MyBackgroundService>();

5. Keyed DI (NET 8 이상 기능)

.NET 8에서는 Keyed DI 기능이 추가되었습니다.

builder.Services.AddKeyedSingleton<IMessageSender, EmailSender>("email");
builder.Services.AddKeyedSingleton<IMessageSender, SmsSender>("sms");

var provider = builder.Build().Services;
var emailSender = provider.GetKeyedService<IMessageSender>("email");
emailSender.SendMessage("Hello Email");

6. 정리 및 공식 문서 링크

오늘은 .NET의 DI(Dependency Injection) 개념부터 고급 패턴까지 살펴보았습니다.
DI를 적극 활용하면 코드 유지보수성, 테스트 용이성, 확장성이 크게 향상됩니다! 🚀

📌 관련 공식 문서

🔹 .NET Dependency Injection 개요
🔹 서비스 수명 주기 (Lifetime) 공식 문서
🔹 ASP.NET Core에서 DI 적용하기

"클로저가 뭐지?" 하시는 분들을 위해 생활 속 예시와 코드를 통해 쉽게 설명해 드립니다.


📌 클로저(Closure)란?

클로저는 "외부 변수나 상태를 캡처하고 기억하는 함수" 입니다. 즉, 함수가 선언된 환경의 변수를 함수 내부에서 계속 유지할 수 있도록 해주는 기능입니다.


🚗 생활 속 예시: 택시 기사와 손님

택시를 탔다고 가정해 보겠습니다. 🚖

  • 손님이 "서울역으로 가주세요!" 라고 요청하면,
  • 택시 기사는 "목적지: 서울역" 을 기억합니다.
  • 이후 운전하는 동안에도 "서울역까지 가야 한다" 는 정보를 유지하며 길을 찾아갑니다.
  • 결국, 서울역에 도착하면 손님을 내려줍니다.

즉, 클로저는 "특정 상태(서울역이라는 목적지)를 기억하는 함수" 라고 볼 수 있습니다.


🛠 C#에서의 클로저 예제

using System;

class Program
{
    static void Main()
    {
        Func<int, int> taxi = TaxiDriver(5000); // 기본 요금 5000원 설정
        Console.WriteLine(taxi(3)); // 3km 이동 -> 결과: 8000원
        Console.WriteLine(taxi(2)); // 추가 2km 이동 -> 결과: 10000원
    }

    static Func<int, int> TaxiDriver(int baseFare)
    {
        int totalFare = baseFare; // 기본 요금
        int perKmFare = 1000; // km당 요금

        return (int km) =>
        {
            totalFare += km * perKmFare; // 이동 거리만큼 요금 증가
            return totalFare; // 현재까지의 총 요금 반환
        };
    }
}

💡 코드 설명

  1. TaxiDriver(5000)을 호출하면, 기본 요금 5000원을 설정합니다.
  2. taxi(3)을 호출하면, 3km당 1000원씩 추가되어 8000원이 됩니다.
  3. taxi(2)을 호출하면, 이전 값을 유지하고 2km 더 가서 10000원이 됩니다.
  4. totalFare 변수가 TaxiDriver 함수 실행이 끝나도 계속 유지되면서 업데이트됩니다.
    • 이것이 바로 클로저의 핵심 기능입니다! 🎯

🔹 클로저가 유용한 경우

상태 유지가 필요한 경우

  • 위의 택시 요금 계산처럼 외부 변수를 기억해야 하는 상황에서 활용됩니다.

이벤트 핸들러

  • 특정 값을 저장하고 이벤트 발생 시 사용할 때 유용합니다.

지연 실행 (Lazy Evaluation)

  • 나중에 실행해야 하는 코드에서 외부 상태를 유지할 때 사용됩니다.

📝 마무리 정리

C#의 클로저는 "외부 변수 값을 기억하고, 함수가 실행된 후에도 유지하는 기능" 입니다. 🚀
택시 기사가 목적지를 기억하고 운전하는 것처럼, 함수가 특정 상태를 기억하고 계속 활용할 수 있도록 해줍니다!

Starting DotNetty server...
Server started on port 8080
Received: Hello 0
Received: Hello 1
Received: Hello 2
Received: Hello 3
Received: Hello 4

게임 개발에서 Unity와 서버 간의 원활한 통신은 매우 중요한 요소입니다. 특히 고성능의 비동기 통신 라이브러리인 DotNetty는 .NET 환경에서 이를 쉽게 구현할 수 있게 도와줍니다. 이 글에서는 DotNetty를 사용하여 Unity와 게임 서버 간의 패킷 교환 예제를 보여드리겠습니다.


DotNetty란?

DotNetty는 .NET 환경에서 사용할 수 있는 고성능 네트워크 라이브러리로, Java의 Netty를 기반으로 만들어졌습니다. 높은 확장성과 낮은 레이턴시를 제공하며, TCP 및 UDP 프로토콜을 지원합니다.

주요 특징

  • 비동기 I/O: 높은 성능과 확장성 제공.
  • 이벤트 기반 아키텍처: 네트워크 이벤트를 효율적으로 처리 가능.
  • 모듈화: 유연한 데이터 처리 파이프라인 구성.

프로젝트 설정

DotNetty를 활용하려면 NuGet 패키지를 설치해야 합니다. 다음 명령어를 통해 패키지를 추가합니다.

Install-Package DotNetty.Buffers
Install-Package DotNetty.Codecs
Install-Package DotNetty.Transport

DotNetty 서버 예제

using System;
using System.Text;
using System.Threading.Tasks;
using DotNetty.Buffers;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;

namespace DotNettyServer
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting DotNetty server...");
            var server = new GameServer();
            await server.RunAsync(8080);
        }
    }

    public class GameServer
    {
        public async Task RunAsync(int port)
        {
            var bossGroup = new MultithreadEventLoopGroup(1);
            var workerGroup = new MultithreadEventLoopGroup();

            try
            {
                var bootstrap = new ServerBootstrap();
                bootstrap.Group(bossGroup, workerGroup)
                         .Channel<TcpServerSocketChannel>()
                         .ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
                         {
                             var pipeline = channel.Pipeline;
                             pipeline.AddLast(new GameServerHandler());
                         }));

                var channel = await bootstrap.BindAsync(port);
                Console.WriteLine($"Server started on port {port}");
                await channel.CloseCompletion;
            }
            finally
            {
                await bossGroup.ShutdownGracefullyAsync();
                await workerGroup.ShutdownGracefullyAsync();
            }
        }
    }

    public class GameServerHandler : SimpleChannelInboundHandler<IByteBuffer>
    {
        protected override void ChannelRead0(IChannelHandlerContext ctx, IByteBuffer msg)
        {
            string receivedMessage = msg.ToString(Encoding.UTF8);
            Console.WriteLine($"Received: {receivedMessage}");

            // Echo the message back
            byte[] responseBytes = Encoding.UTF8.GetBytes($"Server response: {receivedMessage}");
            ctx.WriteAndFlushAsync(Unpooled.WrappedBuffer(responseBytes));
        }

        public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
        {
            Console.WriteLine($"Error: {exception.Message}");
            context.CloseAsync();
        }
    }
}
 

클라이언트 코드 (GameClient)

using System;
using System.Text;
using System.Threading.Tasks;
using DotNetty.Buffers;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;

namespace DotNettyClient
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting DotNetty client...");
            var client = new GameClient();
            await client.RunAsync("127.0.0.1", 8080);
        }
    }

    public class GameClient
    {
        public async Task RunAsync(string host, int port)
        {
            var group = new MultithreadEventLoopGroup();

            try
            {
                var bootstrap = new Bootstrap();
                bootstrap.Group(group)
                         .Channel<TcpSocketChannel>()
                         .Handler(new ActionChannelInitializer<IChannel>(channel =>
                         {
                             var pipeline = channel.Pipeline;
                             pipeline.AddLast(new GameClientHandler());
                         }));

                var channel = await bootstrap.ConnectAsync(host, port);
                Console.WriteLine("Client connected to server.");

                for (int i = 0; i < 5; i++)
                {
                    string message = $"Hello {i}";
                    byte[] messageBytes = Encoding.UTF8.GetBytes(message);
                    Console.WriteLine($"Sending: {message}");
                    await channel.WriteAndFlushAsync(Unpooled.WrappedBuffer(messageBytes));
                    await Task.Delay(1000);
                }

                await channel.CloseAsync();
            }
            finally
            {
                await group.ShutdownGracefullyAsync();
            }
        }
    }

    public class GameClientHandler : SimpleChannelInboundHandler<IByteBuffer>
    {
        protected override void ChannelRead0(IChannelHandlerContext ctx, IByteBuffer msg)
        {
            string receivedMessage = msg.ToString(Encoding.UTF8);
            Console.WriteLine($"Server response: {receivedMessage}");
        }

        public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
        {
            Console.WriteLine($"Error: {exception.Message}");
            context.CloseAsync();
        }
    }
}

실행 방법

  1. 서버 실행: GameServer 클래스를 실행하여 서버를 시작합니다.
  2. 클라이언트 실행: GameClient 클래스를 실행하여 서버와 통신을 시작합니다.
  3. 클라이언트에서 보낸 메시지가 서버로 전달되고, 서버는 응답을 반환합니다.

실행 결과

1. 서버

Starting DotNetty server...
Server started on port 8080
Received: Hello 0
Received: Hello 1
Received: Hello 2
Received: Hello 3
Received: Hello 4

 

1. 클라이언트

Starting DotNetty client...
Client connected to server.
Sending: Hello 0
Server response: Server response: Hello 0
Sending: Hello 1
Server response: Server response: Hello 1
Sending: Hello 2
Server response: Server response: Hello 2
Sending: Hello 3
Server response: Server response: Hello 3
Sending: Hello 4
Server response: Server response: Hello 4

마무리

DotNetty는 TCP 소켓 통신에서 뛰어난 성능과 확장성을 제공하며, Unity와 같은 게임 엔진과의 통합에 적합한 라이브러리입니다. 이번 글에서는 간단한 예제를 통해 DotNetty를 사용한 서버-클라이언트 통신을 구현하는 방법을 알아보았습니다.

Protobuf란 무엇인가?

Protocol Buffers(이하 Protobuf)는 Google에서 설계한 데이터 직렬화 포맷으로, 빠르고, 가볍고, 효율적이라는 특징을 가지고 있습니다. Protobuf는 데이터를 바이너리 포맷으로 직렬화하여 전송하고, 수신 측에서 다시 역직렬화하여 사용할 수 있게 합니다. 이는 JSON, XML 같은 텍스트 기반 포맷보다 훨씬 더 적은 크기로 데이터를 처리할 수 있어 네트워크 및 저장소 비용을 절감할 수 있습니다.


Protobuf의 특징

  1. 빠른 속도: 바이너리 포맷을 사용해 텍스트 포맷보다 빠르게 데이터를 처리.
  2. 작은 크기: 데이터 크기가 작아 네트워크 대역폭 절약.
  3. 유연성: 다양한 언어에서 사용할 수 있는 코드 생성 도구 제공.
  4. 호환성: 스키마를 기반으로 버전 관리를 지원.

예제 코드로 배우는 Protobuf

1. 클래스 정의

Protobuf를 사용하려면 [ProtoContract]와 [ProtoMember]를 통해 직렬화할 클래스를 정의해야 합니다. 아래는 User 클래스의 정의입니다.

 
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp15
{
    [ProtoContract]
    public class User
    {
        [ProtoMember(1)]
        public string name { get; set; }
        [ProtoMember(2)]
        public int age { get; set; }
        public int id { get; set; }
    }
}

2. 직렬화 (Serialization)

Protobuf 직렬화는 데이터를 바이너리 형식으로 변환하여 네트워크 전송이나 저장소에 저장할 때 사용됩니다. 다음은 User 객체를 직렬화하는 코드입니다.

// User class
User user = new User
{
    name = "Jhon",
    age = 20,
    id = 1
};

//  serialize
MemoryStream serialize = new MemoryStream();
ProtoBuf.Serializer.Serialize<User>(serialize, user);
byte[] byteData = serialize.ToArray();
Console.WriteLine($"Serialize : {BitConverter.ToString(byteData)}");

3. 역직렬화 (Deserialization)

바이너리 데이터를 다시 객체로 변환하는 과정입니다.

//  deserialize
MemoryStream deserialize = new MemoryStream(byteData);
User result = ProtoBuf.Serializer.Deserialize<User>(deserialize);
Console.WriteLine($"DeSerialize : {result.name}, {result.age}, {result.id = 1}");

4. 출력결과

Serialize : 0A-04-4A-68-6F-6E-10-14
DeSerialize : Jhon, 20, 1

Protobuf의 장단점

장점

  • 성능: 데이터 크기와 처리 속도에서 JSON, XML 대비 뛰어남.
  • 언어 독립성: 다양한 언어에서 Protobuf 메시지를 생성하고 사용할 수 있음.
  • 버전 관리: 메시지 구조를 확장하거나 수정해도 기존 데이터와의 호환성 유지.

단점

  • 가독성 부족: 바이너리 포맷이라 사람이 읽기 어려움.
  • 학습 곡선: JSON처럼 직관적이지 않아 초기 설정이 복잡할 수 있음.

언제 Protobuf를 사용해야 할까?

  • 대규모 데이터 전송: 네트워크 비용을 절감하고 전송 속도를 높이고 싶을 때.
  • 저장소 최적화: 디스크 공간을 최소화하면서 데이터를 저장해야 할 때.
  • 멀티플랫폼 환경: 다양한 언어와 플랫폼에서 동일한 데이터 구조를 사용할 때.

마무리

Protobuf는 성능과 효율성이 중요한 환경에서 강력한 도구로 사용할 수 있습니다. JSON이나 XML이 너무 무겁게 느껴질 때 Protobuf를 고려해 보는것이 좋을 것 같습니다. 직렬화와 역직렬화 과정도 단순하며, 특히 네트워크 트래픽을 최적화해야 하는 상황에서 유용합니다.

서버 성능 테스트를 해야하는 업무가 발생하여 어떤식으로 진행했는지 공유 해보려고 합니다.
하루 기준 약 300만의 유저가 1~2일 동안 저희 서비스에 유입이될 수 있는 상황이였습니다.

이에 따라 3000000 / 24 / 60 / 60 1초에 약 34명이 유입될 수 있다고 단순하게 계산을 한 후

서버 성능 테스트를 진행해보기로 했습니다.

상황에 따라 어느 언어나 툴을 사용하던 시나리오를 잘 구성하여 체크하면 된다고하여

가장 효율적인 방법을 고민하게 되었고,

가장 자신 있는 C# Conole 프로젝트로 진행 해보려고 합니다.

시나리오

필수 요소인 회원가입 & 로그인 작업 이후 유저가 실제로 행동할 Task를 분석하여

시나리오를 구성해보았습니다.

 

1초에 30명 ~ 50명 유저가 유입된다.
모든 유저는 각 Task 당 약 1초의 delay로 진행된다.
A (로그인) -> B Task -> C Task -> D Task -> B Task -> C Task -> D Task -> Exit

 

성능 테스트용 봇 개발

.NET Console 프로젝트를 활용하여 개발 하였습니다.

using log4net;
using log4net.Config;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

class PerformanceTestBot
{
    private static readonly ILog log = LogManager.GetLogger(typeof(PerformanceTestBot));
    private static readonly HttpClient client = new HttpClient();
    private const int userCountPerSecond = 30; // 초당 생성할 유저 수
    private static bool keepRunning = true;
    private static int userCounter = 1; // 생성 시작할 유저 id

    static async Task Main(string[] args)
    {
        // Log4net 초기화
        XmlConfigurator.Configure(new FileInfo("log4net.config"));
        log.Info("Starting performance test...");

        Console.CancelKeyPress += (sender, e) =>
        {
            e.Cancel = true;
            keepRunning = false;
            log.Info("Stopping user creation...");
        };

        await StartUserCreationLoop(userCountPerSecond);
        log.Info("Performance test completed.");
    }

    // 유저 생성 루프
    private static async Task StartUserCreationLoop(int userCountPerSecond)
    {
        while (keepRunning)
        {
            var tasks = new List<Task>();

            for (int j = 0; j < userCountPerSecond; j++)
            {
                tasks.Add(CreateAndRunScenario());
            }

            _ = Task.WhenAll(tasks);
            await Task.Delay(10000); //1초
        }
    }

    // 유저 생성 후 시나리오 실행
    private static async Task CreateAndRunScenario()
    {
        int userId = Interlocked.Increment(ref userCounter);

        // SSL 인증서 무시 설정을 추가한 HttpClient 생성
        var handler = new HttpClientHandler
        {
            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
        };

        using (var localClient = new HttpClient(handler))
        {
            var loginToken = await ATask();

            if (!string.IsNullOrEmpty(loginToken))
            {
                log.Info($"[A Task] User {userId} signed up successfully with login token: {loginToken}");

                for (int i = 0; i < 3; i++)
                {
                    await BTask();
                    log.Info($"[B Task] User {userId} with token {loginToken} called API B.");

                    await Task.Delay(2000);

                    await CTask();
                    log.Info($"[C Task] User {userId} with token {loginToken} called API C");

                    await Task.Delay(2000);

                    await DTask();
                    log.Info($"[D Task] User {userId} with token {loginToken} called API D");

                    await Task.Delay(2000);
                }

                log.Info($"User {userId} has completed all missions and is exiting.");
            }
            else
            {
                log.Warn($"[A Task] User {userId} failed to sign up.");
            }
        }
    }


    #region API methods

    // A Task
    private static async Task<string> ATask()
    {
        // A API 호출
    }

    // B Task
    public static async Task BTask()
    {
        // B API 호출
    }

    // C Task
    public static async Task CTask()
    {
        // C API 호출
    }

    // D Task
    public static async Task DTask()
    {
        // D API 호출
    }

    #endregion
}

 

성능 테스트 과정

서버 모니터링 시스템을 킨 후 현재 CPU, Memory를 확인한 후 프로그램을 실행하였습니다.
초당 30명 생성은 약 5초 후에 CPU 100% 차면서 서버가 멈췄습니다😂
현재 개발 서버 스펙은 초당 15명 생성이 한계였습니다😂


개발 서버를 2배로 스펙업 후 초당 40명 생성을 확인 하였고

약 3시간 동작 결과 CPU 70%로 안정적으로 시나리오 동작 하는 것을 확인 하였습니다.

 

성능 테스트 완료

상용 서버와 개발 서버 스펙업을 비교 하고 유저 유입 기간에 맞춰서

상용 서버 스펙업을 진행하기로 하였습니다.


그리고 성공적으로 상용서버에서 유저 유입에 성공 하였습니다🎈 🎈 🎈

안녕하세요. 이번 포스트에서는 이미지 생성 API를 호출할 때 사용하는 세팅 값에 대해서 정리해보려고 합니다.

적절한 값을 세팅하면 좋은 결과를 얻을 수 있을 것 같습니다.

저는 black-forest-labs/flux-dev – Run with an API on Replicate 기준으로 정리해보고 있습니다.

* Image Generator(이미지 생성기)는 입력된 텍스트를 기반으로 인공지능이 이미지를 생성하는 도구입니다. 사용자는 간단한 설명이나 키워드를 입력하면, 이에 맞는 고품질 그래픽, 그림 또는 사진을 얻을 수 있습니다. 이 기술은 콘텐츠 제작, 디자인, 예술 등 다양한 분야에서 창의적인 작업을 돕습니다.

세팅값

prompt

생성하고자 하는 이미지의 주제를 설명하는 텍스트입니다. 예를 들어 "바닷가에 있는 빨간 등대"라고 입력하면 해당 주제의 이미지가 생성됩니다.

 

aspect_ratio

이미지를 생성 비율 ex. 1:1, 16:9

 

image

이미지를 생성할 때 참고할 초기 이미지를 제공합니다. 이 초기 이미지를 기반으로 텍스트와 결합하여 새로운 이미지를 만듭니다.

 

prompt_strength

초기 이미지와 텍스트 중 어떤 요소를 더 많이 반영할지를 설정합니다. 값이 높으면 텍스트가 더 많이 반영되고, 낮으면 초기 이미지가 더 강조됩니다.

 

num_outputs

한 번에 생성할 이미지의 개수를 지정합니다. 예를 들어 값을 3으로 설정하면 한 번에 3개의 이미지가 생성됩니다.

 

num_inference_steps

이미지 생성 과정에서 품질과 세부 사항을 결정하는 반복 단계 수입니다. 단계가 많을수록 이미지 품질이 높아지지만, 처리 시간이 더 길어집니다.

 

guidance

텍스트 지침을 얼마나 엄격하게 따를지를 조정하는 값입니다. 높을수록 텍스트 설명에 더 충실한 이미지가 생성됩니다.

 

seed

생성 결과를 반복적으로 동일하게 얻기 위한 랜덤 값 초기화 번호입니다. 같은 prompt와 seed를 사용하면 동일한 이미지를 생성할 수 있습니다.

 

output_format

생성된 이미지의 파일 형식을 설정합니다. 예를 들어 PNG 또는 JPEG 같은 형식을 선택할 수 있습니다.

 

output_quality

생성 이미지의 해상도와 품질을 조정하는 옵션입니다. 값이 높을수록 선명한 이미지가 만들어지지만 파일 크기가 커질 수 있습니다.

 

disable_safety_checker

안전 필터를 비활성화할지 여부를 설정합니다. 비활성화하면 특정 제한 없이 다양한 이미지가 생성될 수 있습니다.

 

go_fast

이미지 품질을 조금 낮추는 대신 생성 속도를 빠르게 하는 옵션입니다. 시간이 제한된 상황에서 유용합니다.

 

megapixels

생성 이미지의 크기(해상도)를 결정하는 값입니다. 예를 들어 1MP는 작은 크기, 5MP는 고해상도 이미지를 의미합니다.

결론

이미지 생성 API의 세팅 값은 각 항목이 이미지의 품질, 속도, 스타일에 영향을 미치므로 목적에 맞게 조정해야 합니다. 처음에는 기본 값을 사용해보고, 필요에 따라 세부 옵션을 조정하며 최적의 결과를 찾아가는 것이 중요합니다. 이러한 설정을 이해하면 원하는 이미지를 효율적으로 생성할 수 있습니다.

안녕하세요. 이번 포스트에서는 Rocky Linux 서버에서 NTP 동기화 문제를 해결하고 JWT 토큰 오류를 해결한 과정을 공유하려고 합니다. 시간 동기화 문제는 JWT 토큰 생성 및 사용 시 중요한 요소이기 때문에, NTP 동기화 설정이 제대로 되어 있지 않으면 다양한 문제가 발생할 수 있습니다.

 

* NTP(Network Time Protocol) 서버는 네트워크 상에서 시간을 정확히 맞춰주는 서버입니다. 시스템 시간의 정확한 동기화는 보안, 로그 기록, 일정한 작업 수행을 위해 중요합니다. 정확한 시간 동기화가 없으면 인증 오류, 로그 불일치 등 문제가 발생할 수 있습니다.

문제 발생

서버 로그에서 JWT 토큰 생성 시 다음과 같은 오류가 발생했습니다:

Error: "invalid_grant", Description: "Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe"

timedatectl status 명령어를 통해 시간 동기화 상태를 확인한 결과, System clock synchronized: no로 표시되었고, NTP 서버와 동기화되지 않는 문제가 발견되었습니다.

문제 해결 과정

1. chrony 설치 및 설정

Rocky Linux에서는 chrony를 사용하여 NTP 동기화를 설정합니다.

chrony 설치

sudo yum install chrony

chrony.conf 파일 설정

/etc/chrony.conf 파일을 열어 NTP 서버 설정을 추가합니다.

sudo vi /etc/chrony.conf

 

다음과 같은 설정을 추가합니다:

# Use public servers from the pool.ntp.org project.
pool pool.ntp.org iburst
server time.google.com iburst
server time.cloudflare.com iburst
server ntp.ubuntu.com iburst

# Allow the system clock to be stepped in the first three updates
# if its offset is larger than 1 second.
makestep 1.0 3

# Enable kernel synchronization of the real-time clock (RTC).
rtcsync

# Record the rate at which the system clock gains/losses time.
driftfile /var/lib/chrony/drift

# Specify directory for log files.
logdir /var/log/chrony

2. chrony 서비스 재시작 및 동기화 강제 업데이트

chrony 서비스를 재시작하고 시간 동기화를 강제 업데이트합니다.

sudo systemctl restart chronyd
sudo chronyc -a makestep

3. timedatectl 상태 확인

시간 동기화 상태를 확인합니다.

 
timedatectl status

 

출력 예시:

Local time: Mon 2024-07-29 10:32:29 KST
Universal time: Mon 2024-07-29 01:32:29 UTC
RTC time: Mon 2024-07-29 01:32:29
Time zone: Asia/Seoul (KST, +0900)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no

4. 방화벽 설정 확인

서버의 방화벽 설정을 확인하여 NTP 트래픽이 차단되지 않도록 설정합니다.

sudo iptables -A INPUT -p udp --dport 123 -j ACCEPT
sudo iptables -A OUTPUT -p udp --dport 123 -j ACCEPT
sudo service iptables save

5. 수동 NTP 동기화

ntpdate를 사용하여 수동으로 NTP 서버와 동기화를 시도합니다.

sudo yum install ntpdate
sudo ntpdate pool.ntp.org

6. 시스템 재부팅

모든 설정을 완료한 후, 시스템을 재부팅하여 설정이 제대로 적용되도록 합니다.

sudo reboot

7. 최종 확인

시스템 재부팅 후, timedatectl status 명령어를 실행하여 시스템 시간 동기화 상태를 다시 확인합니다.

결론

이 단계를 통해 시스템 시간이 NTP 서버와 올바르게 동기화되었으며, JWT 토큰 생성 및 사용 시 발생하는 시간 관련 오류가 해결되었습니다. 시스템 시간이 정확하게 동기화됨으로써 시간에 민감한 작업이 정상적으로 수행될 수 있게 되었습니다.

이와 같은 문제를 해결할 때는 NTP 서버와의 시간 동기화 상태를 항상 확인하고, 필요한 경우 방화벽 설정 및 네트워크 설정을 점검하여 원활한 동기화가 이루어지도록 해야 합니다.


데드락은 두 개 이상의 프로세스가 서로 상대방이 점유하고 있는 자원을 기다리며 무한정 대기하는 상태를 말합니다. 즉, 각 프로세스가 자신이 필요로 하는 자원을 얻기 위해 다른 프로세스가 해제되기를 기다리는데, 다른 프로세스 역시 자원을 해제하지 못하고 있는 상황입니다. 이런 상황에서는 프로세스들이 계속 대기 상태에 빠지게 되어 더 이상 진행할 수 없게 됩니다.

 

1. 소스

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    private static readonly object lockA = new object();
    private static readonly object lockB = new object();

    static async Task Main(string[] args)
    {
        var task1 = Task.Run(() => TaskA());
        var task2 = Task.Run(() => TaskB());

        await Task.WhenAll(task1, task2);

        Console.WriteLine("Tasks completed");
    }

    private static void TaskA()
    {
        lock (lockA)
        {
            Console.WriteLine("TaskA acquired lockA");
            Thread.Sleep(100); // Simulate work

            lock (lockB)
            {
                Console.WriteLine("TaskA acquired lockB");
                // Simulate work
            }
        }
    }

    private static void TaskB()
    {
        lock (lockB)
        {
            Console.WriteLine("TaskB acquired lockB");
            Thread.Sleep(100); // Simulate work

            lock (lockA)
            {
                Console.WriteLine("TaskB acquired lockA");
                // Simulate work
            }
        }
    }
}

 

2. 결과

TaskB acquired lockB
TaskA acquired lockA

 

3. 분석

이 코드는 두 개의 작업(TaskA와 TaskB)이 서로 다른 순서로 두 개의 잠금(lockA와 lockB)을 얻으려고 할 때 데드락(교착 상태)이 발생할 수 있음을 보여줍니다.

TaskA는 먼저 lockA를 얻은 다음 lockB를 얻으려고 합니다. 반면에 TaskB는 먼저 lockB를 얻은 다음 lockA를 얻으려고 합니다.

이렇게 서로 다른 순서로 잠금을 시도하기 때문에, TaskA는 lockA를 잡고 lockB를 기다리는 상황에 놓이고, 동시에 TaskB는 lockB를 잡고 lockA를 기다리는 상황에 놓입니다. 이로 인해 두 작업 모두 더 이상 진행할 수 없는 교착 상태에 빠지게 됩니다.

실제로 이 코드를 실행하면, 프로그램이 데드락에 빠져 "Tasks completed" 메시지가 출력되지 않고, 무한히 대기 상태에 빠져 있는 것을 볼 수 있습니다. 이를 통해 데드락 상황이 발생했음을 확인할 수 있습니다.

데드락을 피하기 위해서는 모든 스레드가 동일한 순서로 잠금을 얻도록 코드를 작성해야 합니다. 또는, Mutex와 같은 고급 동기화 메커니즘을 사용하는 것이 좋습니다.

+ Recent posts