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

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

 

성능 테스트 완료

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

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


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

 

.net core 프로젝트JenkinsDocker를 사용하여 
linux 배포서버에 자동배포를 할 수 있도록
CI/CD 구성을 해보려고 합니다.
docker 참고 : https://nitpick92.tistory.com/4
jenkins 참고 : https://nitpick92.tistory.com/14
docker 명령어 참고 : https://nitpick92.tistory.com/5

 

1. 필요한 환경 세팅

  • 로컬 pc (Windows)
    github, docker, visual studio 2022 설치

https://www.docker.com/products/docker-desktop/
Docker Desktop 설치


https://momobob.tistory.com/m/71
WSL2 프로그램 설치

 


  • Jenkins Server (linux CentOS7)
    Jenkins, docker 설치

  • Deploy Server (linux CentOS7)
    docker설치

 


2. 기본동작 확인

자동화 배포 흐름도

  • jenkins 서버에서 github 최신 소스를 pull 받아와 build test를 진행.
  • build test 통과 후 image 제작
  • image -> docker 저장소인 docker hub에 push
  • 프로젝트에 포함돼있는 scripts/deploy.sh를 배포서버에 ssh 전송
  • deploy.sh 실행

3. 프로젝트 준비

docker desktop 동작을 확인해주세요.


 

net core 6.0으로 프로젝트를 생성해 주세요.
Docker 사용을 클릭해 주세요.


 

docker로 프로젝트 실행이 되는지 확인해주세요.

#해당 위치의 dockerfile를 사용하여 image1 생성
docker build -t image1 ./

docker image1 생성 확인

docker image 명령어를 사용하여 image를 생성을 해보세요.

# 생성된 image1 이미지를 사용하여 container1를 생성 후 동작시키는 명령어
docker run -d -p 8080:80 --name container1 image1

docker container 명령어를 사용하여 container를 생성해 보세요.

container 동작을 확인해봅니다.
정상 동작이 완료되면 준비는 끝났습니다.
자신의 Github에 레포지토리 업로드 해주세요.

4. github 접근 토큰 생성

Profile > Settings / Developer settings > Personal access tokens

만들어진 토큰을 안전한 곳에 저장해주세요.


5. github webhook 설정

Repository > Settings > Webhooks

젠킨스 서버 주소/github-webhook/을 연결해주세요.
master branch에 push시 jenkins에 알릴 수 있도록 해주는 역할입니다.

6. docker hub 접근 토큰 생성

Account Settings > Security > New Access Tokens

만들어진 토큰을 안전한 곳에 저장해주세요.


7. 젠킨스 플러그인 설치

https://nitpick92.tistory.com/14

참고하여 설치 후 젠킨스 접속 후 로그인 해주세요.

Jenkins 관리 > Plugin Manager 이동하여 하단 플러그인을 설치해주세요.

  • github integration
  • post build task
  • publish over ssh

8. 젠킨스에 배포 서버 SSH 연결

Dashboard > Jenkins 관리 > Configure System

추후에 deploy.sh파일을 배포서버에 전달해주기 위한 SSH 설정입니다.
ssh or password로 test configuration success 확인 해주시면 됩니다.

 


9. 젠킨스 Credentials Add

Jenkins 관리 > Credentials > System > Global credentials

github에서 만든 접근 토큰을 세팅해줍니다.

docker hub에서 만든 접근 토큰을 세팅해줍니다.

10. 젠킨스 Project 생성

Dashboard > 새로운 item

자신의 github 주소를 연결해주세요.

처음에 세팅한 프로젝트가 업로드된 레포지토리 주소를 적어주세요.
*/master -> master branch에 push를 할때 동작을 하려고 합니다.

Docker Hub Token을 변수에 담아서 사용하기 위한 세팅입니다.

echo $PASSWORD | docker login -u $USERNAME --password-stdin
docker build -t {도커 허브 아이디}/{이미지 네임} ./
docker push {도커 허브 아이디}/{이미지 네임}
docker rmi {도커 허브 아이디}/{이미지 네임}
최신화 된 소스를 대상으로 빌드 테스트 후 Docker Hub에 배포해줍니다.

echo $PASSWORD | docker login -u $USERNAME --password-stdin
sh deploy.sh
Jenkins -> deploy_server1로 프로젝트 scripts폴더에 있는
deploy.sh를 옮기고 실행시켜줍니다.

11. deploy.sh 추가

상단에서 세팅한 프로젝트 scripts폴더 생성 후 deploy.sh를 추가해줍니다.

# 가동중인 testcontainer1 컨테이너 중단 및 삭제
sudo docker ps -a -q --filter "name=testcontainer1" | grep -q . && docker stop testcontainer1 && docker rm testcontainer1 | true

# 기존 이미지 삭제
sudo docker rmi {본인 docker_hub_id}/testimage1

# 도커허브 이미지 pull
sudo docker pull {본인 docker_hub_id}/testimage1

# 도커 run
docker run -d -p 8080:80 --name testcontainer1 {본인 docker_hub_id}/testimage1

# 사용하지 않는 불필요한 이미지 삭제 -> 현재 컨테이너가 물고 있는 이미지는 삭제되지 않습니다.
docker rmi -f $(docker images -f "dangling=true" -q) || true

12. 배포 성공 확인

https://nitpick92.tistory.com/manage/posts/ 

컨테이너 port로 방화벽 오픈 후 배포된 웹 사이트를 확인 해보시면 됩니다.

jenkins 프로젝트 동작 성공
배포된 mvc 프로젝트

 

 

.net core + jenkins + docker를 이용하여 linux 서버에 웹 서비스를 배포하는 방법을 알아보았습니다.

 

궁금한 사항은 댓글 남겨주세요 : )

 

13. 다음에는

현재는 docker container 중지 후 시작하는 사이에는 배포된 웹 페이지에 접속이 불가능한 문제가 있어

무중단 배포에 대해 정리해보려고 합니다.

'Server > 배포 자동화' 카테고리의 다른 글

CentOS7 Jenkins 설치하기  (0) 2023.02.10

+ Recent posts