ConcurrentStack

ConcurrentStack<T>는 .NET의 동시성 컬렉션 중 하나로, LIFOLast In, First Out 방식으로 데이터를 관리하는 스택 구조를 제공합니다. 멀티스레드 환경에서도 안전하게 사용할 수 있도록 설계된 ConcurrentStack<T>는 여러 스레드에서 동시에 데이터를 추가하거나 제거할 때도 안전성을 보장합니다. 이는 스레드 간 동시 접근이 빈번한 환경에서 효율적으로 스택을 사용할 수 있도록 돕습니다.

ConcurrentStack의 특성

LIFO 구조

일반적인 스택처럼 마지막에 추가된 요소가 가장 먼저 제거되는 구조를 따릅니다. ConcurrentStack<T>는 멀티스레드 환경에서 스택을 사용하는 경우에 적합하며, 비슷한 기능을 제공하는 다른 동시성 컬렉션(ConcurrentQueue, ConcurrentBag)과의 차별점은 데이터의 처리 순서에 있습니다. ConcurrentStack<T>는 후입선출 방식으로 데이터를 처리하기 때문에 가장 최근에 추가된 데이터를 우선적으로 처리하고자 할 때 유용합니다.

주요 메서드와 사용법

생성자와 초기화

기본 생성자

ConcurrentStack을 생성합니다.

ConcurrentStack<int> stack = new ConcurrentStack<int>();

컬렉션으로 초기화

다른 컬렉션을 사용해 ConcurrentStack을 초기화할 수 있습니다.

int[] numbers = { 1, 2, 3, 4, 5 };
ConcurrentStack<int> stackFromCollection = new ConcurrentStack<int>(numbers);

요소 추가 및 제거

Push

스택의 맨 위에 요소를 추가합니다.

stack.Push(10);
stack.Push(20);
  • 여러 요소를 한 번에 추가할 수도 있습니다.
stack.PushRange(new int[] { 30, 40, 50 });

TryPop

스택의 맨 위 요소를 제거하고 반환합니다.

if (stack.TryPop(out int result))
{
    Console.WriteLine(result);  // 스택에서 꺼낸 요소 출력
}
  • 스택이 비어 있는 경우 TryPopfalse를 반환하고, result는 기본값으로 설정됩니다.

TryPeek

스택의 맨 위 요소를 제거하지 않고 반환합니다.

if (stack.TryPeek(out int topElement))
{
    Console.WriteLine(topElement);  // 스택의 맨 위 요소 출력
}

기타 유용한 메서드

IsEmpty

스택이 비어 있는지 확인합니다.

if (stack.IsEmpty)
{
    Console.WriteLine("스택이 비어 있습니다.");
}

Count

스택에 있는 요소의 개수를 반환합니다.

int count = stack.Count;
Console.WriteLine($"스택에 있는 요소의 개수: {count}");

ToArray

스택의 요소를 배열로 복사합니다.

int[] array = stack.ToArray();
  • 스택의 순서가 반영된 배열이 생성됩니다.

기본 사용 예제

다음은 ConcurrentStack<T>의 기본적인 사용 예제를 보여줍니다.

using System;
using System.Collections.Concurrent;
class Program
{
    static void Main()
    {
        // ConcurrentStack 생성
        ConcurrentStack<int> stack = new ConcurrentStack<int>();
        // 요소 추가
        stack.Push(1);
        stack.Push(2);
        stack.Push(3);
        // 요소 제거 및 출력
        if (stack.TryPop(out int result))
        {
            Console.WriteLine($"Pop된 요소: {result}");  // 출력: Pop된 요소: 3
        }
        // 스택의 맨 위 요소 확인
        if (stack.TryPeek(out int topElement))
        {
            Console.WriteLine($"현재 맨 위 요소: {topElement}");  // 출력: 현재 맨 위 요소: 2
        }
        // 스택의 모든 요소 출력
        Console.WriteLine("스택의 모든 요소:");
        foreach (var item in stack)
        {
            Console.WriteLine(item);  // 출력: 2, 1
        }
    }
}

위 예제에서는 ConcurrentStack에 요소를 추가하고, 맨 위의 요소를 제거하거나 확인하는 기본적인 작업을 보여줍니다.

롤백 가능한 작업 관리

ConcurrentStack<T>는 스택에 데이터를 추가했다가 롤백할 수 있도록 설계하기에 적합합니다. 예를 들어, 트랜잭션 기반 시스템에서 작업을 롤백해야 하는 상황에서, 처리된 작업을 순차적으로 취소하는 데 활용할 수 있습니다.

using System;
using System.Collections.Concurrent;
public class TransactionManager
{
    private ConcurrentStack<Action> rollbackActions = new ConcurrentStack<Action>();
    public void AddTransaction(Action transaction, Action rollback)
    {
        try
        {
            transaction.Invoke();
            rollbackActions.Push(rollback); // 롤백 작업 저장
        }
        catch
        {
            Rollback();
        }
    }
    public void Rollback()
    {
        while (rollbackActions.TryPop(out Action rollback))
        {
            rollback.Invoke(); // 롤백 작업 실행
            Console.WriteLine("Rolled back a transaction.");
        }
    }
}
  • 작업이 실패한 경우 Rollback 메서드를 호출하여 저장된 작업들을 순차적으로 롤백함으로써 데이터 일관성을 유지합니다.

캐싱과 데이터 재사용

ConcurrentStack<T>는 다중 스레드 환경에서 반복적으로 필요한 데이터를 캐싱하고 재사용하는 데 유리합니다. 예를 들어, 데이터베이스 연결이나 HTTP 요청 객체 등을 미리 만들어 두고 스택에 넣어두면 필요할 때마다 재사용할 수 있습니다.

using System;
using System.Collections.Concurrent;
public class ConnectionPool
{
    private ConcurrentStack<DatabaseConnection> connectionStack = new ConcurrentStack<DatabaseConnection>();
    public ConnectionPool(int initialSize)
    {
        for (int i = 0; i < initialSize; i++)
        {
            connectionStack.Push(new DatabaseConnection()); // 초기 DB 연결 생성
        }
    }
    public DatabaseConnection GetConnection()
    {
        if (connectionStack.TryPop(out DatabaseConnection connection))
        {
            return connection;
        }
        return new DatabaseConnection(); // 스택이 비어있으면 새로운 연결 생성
    }
    public void ReleaseConnection(DatabaseConnection connection)
    {
        connectionStack.Push(connection); // 사용 후 스택에 다시 추가
    }
}
public class DatabaseConnection
{
    public void Open() { /* 연결 열기 */ }
    public void Close() { /* 연결 닫기 */ }
}
  • DB 연결 객체를 풀에서 꺼내 사용하고 다시 반납하여 자원 재사용을 최적화합니다.
  • 연결이 빈번한 애플리케이션에서 성능을 크게 개선할 수 있습니다.