static은 인스턴스가 아닌 타입 자체에 속하는 멤버를 선언할 때 사용하는 키워드입니다. 메모리를 객체마다 만들지 않고 애플리케이션 도메인(또는 로드 컨텍스트) 단위로 단 한 번 공유한다는 점이 핵심입니다. 실무에서 자주 만나는 패턴과 주의점을 중심으로 정리합니다.
1. 기본 개념과 문법
static 멤버는 타입명으로 직접 접근합니다. 인스턴스 없이 호출되며 모든 인스턴스가 값을 공유합니다.
using System;
class Counter
{
public static int Total; // 모든 인스턴스가 공유
public Counter() => Total++;
}
class Program
{
static void Main()
{
new Counter();
new Counter();
Console.WriteLine(Counter.Total); // 2
}
}
static 멤버에서는 this를 사용할 수 없으며 인스턴스 멤버에도 접근할 수 없습니다.
2. static 필드/메서드/프로퍼티
유틸성 로직, 전역 설정 값, 캐시 등에 적합합니다.
using System;
static class Utils
{
public static string AppName { get; } = "MyApp";
public static int ParseOrDefault(string? s, int defaultValue = 0)
=> int.TryParse(s, out var n) ? n : defaultValue;
}
class Program
{
static void Main()
{
Console.WriteLine(Utils.AppName);
Console.WriteLine(Utils.ParseOrDefault("123"));
}
}
3. static 생성자와 초기화 시점
static 생성자(타입 이니셜라이저)는 타입이 처음 사용되기 직전에 한 번만 실행됩니다. 무거운 작업이나 예외 발생은 피하는 것이 좋습니다.
using System;
class App
{
public static readonly string Root;
static App() // 한 번만 실행
{
Root = Environment.CurrentDirectory;
Console.WriteLine("App initialized");
}
}
class Program
{
static void Main()
{
Console.WriteLine(App.Root); // 접근 시 최초 초기화 보장
}
}
명시적 static 생성자가 없으면 런타임은 beforefieldinit 규칙에 따라 더 이른 시점에 초기화할 수 있습니다. 초기화 시점에 의존하는 코드라면 명시적 static 생성자를 두는 것이 안전합니다.
4. const vs static readonly
const는 컴파일 타임 상수이며 호출측 IL에 값이 박제됩니다. static readonly는 런타임에 한 번만 할당되는 불변 값입니다.
using System;
class Constants
{
public const double VatRate = 0.1; // 컴파일 타임 상수
public static readonly DateTime StartedAt = DateTime.UtcNow; // 런타임 상수
}
버전업 시 const 변경은 참조 어셈블리를 다시 빌드해야 반영됩니다. 런타임 계산이 필요하거나 버전 안정성이 중요하면 static readonly를 선호합니다.
5. static 클래스와 확장 메서드
static 클래스는 인스턴스화할 수 없고 오직 static 멤버만 가집니다. 확장 메서드는 반드시 static 클래스의 static 메서드로 선언합니다.
public static class StringExtensions
{
public static bool IsCapitalized(this string s)
=> !string.IsNullOrEmpty(s) && char.IsUpper(s[0]);
}
6. using static 디렉티브
자주 쓰는 정적 멤버를 클래스명 없이 호출할 수 있습니다.
using static System.Math;
class Program
{
static void Main()
{
double v = Sqrt(3) + Pow(2, 3); // Math. 생략
}
}
7. 싱글톤과 Lazy 초기화
싱글톤은 static으로 표현하기 쉽습니다. Lazy<T>를 사용하면 스레드 안전한 지연 초기화를 간단히 구현합니다.
using System;
public sealed class Config
{
private Config() { }
private static readonly Lazy<Config> _instance = new(() => new Config());
public static Config Instance => _instance.Value;
}
8. 스레드 안전성: 공유 상태 주의
static 필드는 모든 스레드가 공유합니다. 불변이면 안전하지만, 변경 가능한(static mutable) 상태는 동기화가 필요합니다.
using System.Collections.Concurrent;
public static class Cache
{
private static readonly ConcurrentDictionary<string, string> _map = new();
public static void Add(string k, string v) => _map[k] = v; // 스레드 안전
}
단순 컬렉션을 쓸 때는 lock으로 보호합니다. 초기화-only 패턴(static readonly, Lazy)로 불변 설계를 지향합니다.
9. 제네릭 타입의 static은 타입 인자별로 분리
제네릭 타입의 static 필드는 타입 인자 조합별로 별도의 저장소를 가집니다.
using System;
class Box<T>
{
public static int Count;
}
class Program
{
static void Main()
{
Box<int>.Count++;
Box<string>.Count += 2;
Console.WriteLine(Box<int>.Count); // 1
Console.WriteLine(Box<string>.Count); // 2
}
}
10. static 로컬 함수와 static 람다
로컬 함수/람다에 static을 붙이면 외부 변수를 캡처하지 못하게 하여 할당/클로저 비용을 줄일 수 있습니다.
using System;
class Program
{
static void Main()
{
int SumEvens(ReadOnlySpan<int> data)
{
static bool IsEven(int n) => (n & 1) == 0; // 캡처 금지
var sum = 0;
foreach (var n in data)
if (IsEven(n)) sum += n;
return sum;
}
Func<int, int> square = static (int x) => x * x; // 캡처 금지 람다
Console.WriteLine(square(5));
}
}
11. 자주 하는 실수와 베스트 프랙티스
1) static 필드에 변경 가능한 컬렉션을 두고 락 없이 쓰는 실수는 데이터 레이스를 초래합니다. Concurrent 컬렉션이나 lock을 사용합니다.
2) static 생성자에서 예외가 발생하면 TypeInitializationException으로 감싸져 타입 전체가 사용 불가능해질 수 있습니다. 예외를 처리하거나 초기화를 단순화합니다.
3) const 값을 공개 API로 노출하고 버전업 시 재배포를 잊으면 호출측이 오래된 값을 사용할 수 있습니다. static readonly를 고려합니다.
4) 테스트 간 격리가 필요한 경우 static 상태는 테스트 순서에 따라 간섭을 일으킵니다. Reset 훅을 제공하거나 인스턴스 주입으로 전환합니다.
5) 진입점(Main)과 같은 필수 정적 멤버 외에는 가능한 불변(static readonly)과 함수형 스타일을 유지해 예측 가능성을 높입니다.
정리하면, static은 성능과 단순화를 가져오지만 공유 상태의 위험도 함께 동반합니다. 초기화 시점, 스레드 안전성, 불변 설계를 염두에 두고 활용하면 견고한 코드를 작성할 수 있습니다.
'C#' 카테고리의 다른 글
| C# 생성자와 소멸자 (0) | 2026.04.15 |
|---|---|
| C# ref, out, in 매개변수 한 번에 정리 (0) | 2026.04.14 |
| C# 컬렉션 초기화와 Index/Range (1) | 2026.04.13 |
| C# 애트리뷰트 (Attribute) 정의와 활용 (0) | 2026.04.10 |
| C# StringBuilder로 문자열 성능 최적화 (0) | 2026.04.10 |