2025년 9월 20일 토요일

다시보는 C# 콘솔프로그램

콘솔프로그램이 C#언어를 배우는 초기 단계에서는 Console.WriteLine() 등으로
어떤 출력내용을 확인하고 나서 그 다음 단원으로 가고 이런걸로 많이 사용한다.

개발자로써 어느정도 경력이 쌓이다 보면 관심사의 분리 같은걸 알게 되면서
어느순간 콘솔프로그램은 테스트유닛으로 활용되거나 백그라운드 테스크용도로
사용된다.    

asp.net도 엄밀히 따지면 콘솔프로그램이라고 할수 있을정도로 무시할수 없는
레벨로 콘솔프로그램의 위상이 올라선다.

그리고 이를 활용하는 눈에 보이지 않는 어플을 만들기 시작하는데
비단 웹뿐만이 아니라 백그라운드 서비스.. (리눅스에서는 데몬)
으로 활용된다.  게다가 아두이노 같은 싱글보드에서 돌아가는 한개의 프로그램을
만드는데에도 적극적으로 활용된다. 물론 아두이노에서 C#은 안돌아가지만..
그런 비슷한 느낌으로 만드는 모듈이 바로 라즈베리파이가 아니라 중국에서 만드는 값싼
오렌지파이 제로나  오렌지파이 3B같은 부류이다.

물론 CPU가격이 메모리 대란같은것으로 들쭉날쭉하게 되면 어쩔수 없고 또한
비싼 비메모리 예를 들어 인텔이나 브로드컴 같은 CPU들은 아두이노랑 비빌수
없다. 하지만 오렌지파이 CPU같은것들은 RockChip같은 브렌드의 부류로
구성되고 알리익스프레스같은 사이트를 이용해서 중국에서 구매하는경우
실제로 한국에서 아두이노를 돈주고 구매하는것과 맞먹는 가격까지
내려간다.  이를 이용해서 C#으로 콘솔어플을 돌리면 GPIO나 I2C같은걸 이용해서
마치 아두이노처럼 프로그램을 할수 있다. C#으로.. 그리고 그렇게 돌아가기 위해
오렌지 파이에는 암비안이나 우분투서버 같은것을 OS로 포팅하고 dotnet 런타임을
설치하고 나서 시스템이 시작할때 데몬으로 등록처리하면 실제로 동작한다.

세상 참 많이 발전했다.. 그리고 이러한 콘솔프로그램을 돌릴수 있는
그 중심에는 이 앱이 하나의 정식적인 앱으로 활용할수 있게되는 

MS의 핵심 확장모듈이 있다. 그 모듈은

dotnet add package Microsoft.Extensions.Hosting;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package Microsoft.Extensions.DependencyInjection;


이라 할수 있다..

이번 포스팅에서는 이 Hosting을 이용하여 지난번 로거와 HelloWorld를
출력하는 서비스 그리고 1초마다 한번씩 현재시간을 로거에 출력하는 서비스와
랜덤한 숫자를 출력하는 프로그램을 만든다.. 물론 이를 활용하여 서비스앱을
만드는데 기초재료로 사용될것을 기대한다.


[주요순서]

1. 콘솔프로그램을 만들고 위의 3개 package를 설치한다.
2. 메인 진입점 작성하고 4개(로거, 단순출력, 랜덤숫자출력, 시간출력)의 서비스를 포함한다.
3. 각서비스를 구현한다.
4. 빌드후 실행


[진입점]

/// <summary>
/// 메인 프로그램
/// </summary>
public class MainApp
{
    /// <summary>
    /// 진입점
    /// </summary>
    public async static Task Main()
    {
        //초기로드
        Console.WriteLine("Application StartUp!!");
        LoadSetup();

        //서비스 로딩
        IHost host = Host.CreateDefaultBuilder()
            .ConfigureServices(services =>
            {
                //로거 바인딩
                services.AddSingleton<ILoggerProvider>(sp =>
                {
                    return new FileLoggerProvider();
                });

                //서비스 호스트 바인딩
                services.AddHostedService<NormalPrint>();
                services.AddHostedService<TimePrinter>();
                services.AddHostedService<RandomPrint>();
            })
            .Build();

        // 호스트 실행
        await host.RunAsync();
    }

    /// <summary>
    /// 앱로딩 설정
    /// </summary>
    public static void LoadSetup()
    {
        EUCKR.Init();
    }

}


[로거 부분 생략 (지난번 포스팅 참조)]
...


[NormalPrint.cs]

/// <summary>
/// 일반 메시지 출력용
/// </summary>
public class NormalPrint : IHostedService
{
    /// <summary>
    /// 생성자와 DI
    /// </summary>
    private readonly ILogger<NormalPrint> _logger;
    public NormalPrint(ILogger<NormalPrint> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 서비스 시작
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("NormalPrint 서비스 시작!!");

        // 한번만 로그 출력하고 끝냄
        _logger.LogWarning("아이엠 Just 프린트 노멀 유노암쎙??");
        
        //
        return Task.CompletedTask;
    }

    /// <summary>
    /// 서비스 종료
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("NormalPrint 서비스 종료!!");
        return Task.CompletedTask;
    }

}


[RandomPrint.cs]

/// <summary>
/// 랜덤한 숫자 출력
/// </summary>
public class RandomPrint: IHostedService
{
    /// <summary>
    /// 생성자와 내부변수
    /// </summary>
    private readonly ILogger<RandomPrint> _logger;
    private Task _time_count_Task;
    private readonly CancellationTokenSource _stoppingToken = new CancellationTokenSource();
    private Random rnd;
    public RandomPrint(ILogger<RandomPrint> logger)
    {
        _logger = logger;
        rnd = new Random();
    }

    /// <summary>
    /// 서비스 시작
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("RandomPrint 서비스 시작!!");

        // 백그라운드 서비스 시작
        _time_count_Task = Task.Run(async () =>
        {
            await DoWorkAsync(_stoppingToken.Token);
        });
        //
        return Task.CompletedTask;
    }

    /// <summary>
    /// 서비스 종료
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("RandomPrint 서비스 중지!!");
        //
        try
        {
            //작업중지 신호
            _stoppingToken.Cancel();

            //백그라운드 Task가 완료되기를 기다림
            return Task.WhenAny(_time_count_Task, Task.Delay(Timeout.Infinite, cancellationToken));

        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            return Task.CompletedTask;
        }
    }

    /// <summary>
    /// 작업로직
    /// </summary>
    private async Task DoWorkAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                //
                int rnd_count = rnd.Next(100);                
                _logger.LogWarning($"0 ~ 99 사이의 랜덤숫자 -> { rnd_count.ToString() }");

                //0.5초쉼..
                await Task.Delay(500);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}


[TimePrinter.cs]


/// <summary>
/// 시간출력용 클래스
/// </summary>
public class TimePrinter : IHostedService
{
    /// <summary>
    /// 생성자와 내부변수
    /// </summary>
    private readonly ILogger<TimePrinter> _logger;
    private Task _time_count_Task;
    private readonly CancellationTokenSource _stoppingToken = new CancellationTokenSource();
    public TimePrinter(ILogger<TimePrinter> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 서비스 시작
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("TimerPrinter 서비스 시작!!");

        // 백그라운드 서비스 시작
        _time_count_Task = Task.Run(async () =>
        {
            await DoWorkAsync(_stoppingToken.Token);
        });
        //
        return Task.CompletedTask;
    }

    /// <summary>
    /// 서비스 종료
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogWarning("TimerPrinter 서비스 중지!!");
        //
        try
        {
            //작업중지 신호
            _stoppingToken.Cancel();

            //백그라운드 Task가 완료되기를 기다림
            return Task.WhenAny(_time_count_Task, Task.Delay(Timeout.Infinite, cancellationToken));

        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            return Task.CompletedTask;
        }
    }

    /// <summary>
    /// 작업로직
    /// </summary>
    private async Task DoWorkAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                //
                DateTime nowtime = DateTime.Now;
                _logger.LogWarning("TimePrinter의 현재시간 기록: " + nowtime.ToString("yyyy-MM-dd HH:mm:ss"));

                //1초쉼..
                await Task.Delay(1000);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }            
        }
    }
    
}


이렇게 완성하고 나서  dotnet run으로 돌려보면 로그를 잘찍는다..
배포를 하려면 

[윈도우 배포 - 64비트]
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true

[리눅스 배포 - 64비트]
dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true


요로코롬 하면된다..  물론 윈도우나  리눅스에서 시작프로그램에 등록해야 하지만..

ㅇㅇ


댓글 없음:

댓글 쓰기