📌 .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%로 안정적으로 시나리오 동작 하는 것을 확인 하였습니다.

 

성능 테스트 완료

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

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


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


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

 

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와 같은 고급 동기화 메커니즘을 사용하는 것이 좋습니다.

C# 에서 65000 숫자 중 32500이라는 숫자를 찾을 때 빠른 알고리즘을 찾기 위해 테스트를 해보았습니다.

이진 탐색이라는 점과 같은 시간 복잡도를 가지고 있는 Binary Search와 Balanced Binary Search Tree를 비교 해보겠습니다.

시간복잡도 : O(logN)

알고리즘 : 이진 탐색 알고리즘 (탐색할 값과 중간 값을 비교하고, 탐색 범위를 반으로 줄이는 방식)

조건 : 이진 탐색은 정렬된 배열에서 찾고, 균형 이진 트리는 SortedSet 자료구조를 사용했습니다.

 

1. 소스

 

using System;
using System.Collections.Generic;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        int size = 65000;
        int target = 32500;

        // 1. 균형 이진 트리
        SortedSet<int> balancedBinaryTree = new SortedSet<int>();
        for (int i = 1; i <= size; i++)
        {
            balancedBinaryTree.Add(i);
        }

        Stopwatch stopwatch = new Stopwatch();

        stopwatch.Start();
        bool foundInTree = balancedBinaryTree.Contains(target);
        stopwatch.Stop();
        Console.WriteLine($"균형 이진 트리에서 찾는 시간: {stopwatch.ElapsedTicks} ticks");

        // 2. 배열에 담아놓고 바이너리서치
        int[] array = new int[size];
        for (int i = 1; i <= size; i++)
        {
            array[i - 1] = i;
        }

        stopwatch.Restart();
        int index = Array.BinarySearch(array, target);
        stopwatch.Stop();
        Console.WriteLine($"배열에 담아놓고 바이너리서치에서 찾는 시간: {stopwatch.ElapsedTicks} ticks");

        // 찾은 결과 출력
        Console.WriteLine($"균형 이진 트리에서 찾음: {foundInTree}");
        Console.WriteLine($"배열에 담아놓고 바이너리서치에서 찾음: {index >= 0}");
    }
}

 

2. 결과

이진 탐색 (Binary search)가 빠르게 측정이 됐습니다.

균형 이진 트리에서 찾는 시간: 2967 ticks
배열에 담아놓고 바이너리 서치에서 찾는 시간: 2227 ticks
균형 이진 트리에서 찾음: True
배열에 담아놓고 바이너리 서치에서 찾음: True


3. 차이점

배열은 연속된 위치에 저장되어 있어 캐시 히트율이 높습니다. 이를 통해 더 빠른 접근이 가능했습니다. 트리는 노드가 메모리의 임의 위치에 저장될 수 있어, 포인터 참조로 인해 메모리 접근이 더 느릴 수 있습니다.
삽입과 삭제는 균형 이진 트리가 O(log n)의 시간 복잡도로 O(n)의 시간 복잡도에 비해 효율적입니다.

상황에 따라 선택 해서 사용하면 될 것 같습니다.

*캐시 히트율 : 배열이 캐시 메모리의 효율을 잘 활용

C#을 사용하여 일정 기간 이전에 만들어진 로그 파일을 삭제하는 서비스를 만들어보았습니다.

  • 삭제할 기준의 날짜를 설정한다.
  • 해당 기준 이전의 파일이 txt파일 경우에 삭제한다.

 

 

1. C# 소스

try
{
    // 제거할 날짜 범위 (15일)
    byte Days = CAppConfig.cServiceInfo.MAX_STORAGE_DATE;
    string deletePath = @"C:\\Logs\\";

    DirectoryInfo di = new DirectoryInfo(deletePath);
    if (di.Exists)
    {
        DirectoryInfo[] dirInfo = di.GetDirectories();
        // 삭제 날짜 기준 -days(15일)
        string IDate = DateTime.Today.AddDays(-Days).ToString("yyyyMMdd");
        foreach (DirectoryInfo dir in dirInfo)
        {
            foreach (FileInfo file in dir.GetFiles())
            {
                // 확장자
                if(file.Extension != ".txt")
                {
                    continue;
                }

                // 해당 날짜 보다 작으면 삭제
                if (IDate.CompareTo(file.LastWriteTime.ToString("yyyyMMdd")) > 0)
                {
                    file.Delete();
                    logger.LogInformation($"[제거된 파일] {file.Name}");
                }
            }


            logger.LogInformation($"{dir.Name} 디렉토리 완료");
        }

    }
}

 

 

 

github 소스를 참고하던중 해당 오류가 발생했습니다.

# gci -Recurse {해당 파일 path} | Unblock-File
gci -Recurse "D:\WinformExample" | Unblock-File

 

+ Recent posts