Rust 동시성 타입: Send와 Sync로 보장하는 스레드 안전성
Rust의 소유권 시스템과 타입 시스템을 활용한 컴파일 타임 동시성 안전성 보장 메커니즘 완벽 분석
싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 이 인스턴스에 대한 전역 접근점을 제공하는 생성 디자인 패턴입니다. 하지만 멀티스레드 환경에서는 단순한 구현으로는 여러 인스턴스가 생성될 수 있어, **스레드 안전성(Thread Safety)**을 보장하는 기법이 필수적입니다.
이 글에서는 싱글톤 패턴의 구조, 멀티스레드 환경에서의 문제점, 그리고 안전한 구현 방법을 상세히 다룹니다.
애플리케이션에서 하나의 인스턴스만 필요한 경우가 많습니다.
사용 사례:
문제 상황:
class DatabaseConnection {
init {
println("Creating expensive database connection...")
Thread.sleep(1000) // 비용이 큰 초기화
}
fun query(sql: String) {
println("Executing: $sql")
}
}
// 여러 곳에서 인스턴스 생성 → 비효율적
fun main() {
val conn1 = DatabaseConnection() // 1초 대기
val conn2 = DatabaseConnection() // 또 1초 대기
val conn3 = DatabaseConnection() // 또 1초 대기
// 총 3초 + 메모리 낭비
}class UnsafeSingleton private constructor() {
companion object {
private var instance: UnsafeSingleton? = null
fun getInstance(): UnsafeSingleton {
if (instance == null) {
instance = UnsafeSingleton()
}
return instance!!
}
}
}멀티스레드 환경에서의 문제:
시간 스레드 A 스레드 B
─────────────────────────────────────────────────────
t1 if (instance == null)
t2 if (instance == null)
t3 → true, instance = ...
t4 → true, instance = ...
결과: 두 개의 인스턴스 생성! [주의]
실제 테스트:
fun testUnsafeSingleton() {
val instances = mutableSetOf<UnsafeSingleton>()
val threads = (1..100).map {
Thread {
instances.add(UnsafeSingleton.getInstance())
}
}
threads.forEach { it.start() }
threads.forEach { it.join() }
println("Created ${instances.size} instances") // 1보다 클 수 있음!
}가장 간단하고 안전한 방법입니다.
class EagerSingleton private constructor() {
companion object {
val instance = EagerSingleton()
}
fun doSomething() {
println("Eager singleton instance: $this")
}
}
// 사용
EagerSingleton.instance.doSomething()장점:
단점:
인스턴스 생성을 지연시키면서 스레드 안전성을 보장합니다.
class SynchronizedSingleton private constructor() {
companion object {
@Volatile
private var instance: SynchronizedSingleton? = null
fun getInstance(): SynchronizedSingleton {
return synchronized(this) {
if (instance == null) {
instance = SynchronizedSingleton()
}
instance!!
}
}
}
}장점:
단점:
synchronized 블록 진입 (성능 오버헤드)성능과 안전성을 동시에 만족하는 최적 방법입니다.
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun createInstance(): Singleton {
// 첫 번째 체크 (synchronized 없이)
return instance ?: synchronized(this) {
// 두 번째 체크 (synchronized 안에서)
instance ?: Singleton().also { instance = it }
}
}
}
fun print() {
println("Singleton instance: $this")
}
}동작 원리:
1. 첫 번째 null 체크 (instance ?:)
→ instance가 이미 존재하면 즉시 반환 (락 없음!)
→ instance가 null이면 synchronized 블록 진입
2. synchronized 블록 진입
→ 다른 스레드가 이미 인스턴스를 생성했을 수 있으므로
→ 다시 한 번 null 체크
3. 두 번째 null 체크 통과 시
→ 인스턴스 생성 및 반환
@Volatile의 역할:
@Volatile은 두 가지를 보장합니다:
@Volatile 없을 때의 문제:
// @Volatile 없으면 문제 발생 가능
// Thread A: instance = Singleton() 실행
// 1. 메모리 할당
// 2. 생성자 호출
// 3. instance에 할당
// ↓ 컴파일러 최적화로 순서 변경될 수 있음
// 1. 메모리 할당
// 3. instance에 할당 (생성자 호출 전!)
// 2. 생성자 호출
// Thread B: 불완전한 객체 참조 가능!Kotlin/Java에서 가장 권장되는 방식입니다.
class HolderSingleton private constructor() {
companion object {
fun getInstance(): HolderSingleton {
return Holder.instance
}
}
private object Holder {
val instance = HolderSingleton()
}
fun doSomething() {
println("Holder singleton: $this")
}
}장점:
@Volatile 불필요동작 원리:
1. HolderSingleton 클래스 로드
→ Holder는 아직 로드되지 않음
2. getInstance() 최초 호출
→ Holder 클래스 로드 시작
→ JVM이 클래스 초기화 시 스레드 안전 보장
→ instance = HolderSingleton() 실행
3. 이후 호출
→ 이미 초기화된 instance 반환
Java/Kotlin에서 가장 안전한 싱글톤 구현입니다.
enum class EnumSingleton {
INSTANCE;
fun doSomething() {
println("Enum singleton: $this")
}
private var counter = 0
fun incrementCounter(): Int {
return ++counter
}
}
// 사용
EnumSingleton.INSTANCE.doSomething()
EnumSingleton.INSTANCE.incrementCounter()장점:
단점:
애플리케이션 전역 설정을 관리하는 ConfigManager를 싱글톤으로 구현합니다.
data class AppConfig(
val apiBaseUrl: String,
val apiKey: String,
val timeout: Long,
val maxRetries: Int,
val enableLogging: Boolean
)
class ConfigManager private constructor() {
@Volatile
private var config: AppConfig? = null
companion object {
@Volatile
private var instance: ConfigManager? = null
fun getInstance(): ConfigManager {
return instance ?: synchronized(this) {
instance ?: ConfigManager().also { instance = it }
}
}
}
fun loadConfig(config: AppConfig) {
synchronized(this) {
if (this.config != null) {
throw IllegalStateException("Config already loaded")
}
this.config = config
}
}
fun getConfig(): AppConfig {
return config ?: throw IllegalStateException("Config not loaded")
}
fun updateApiKey(newKey: String) {
synchronized(this) {
val current = getConfig()
config = current.copy(apiKey = newKey)
}
}
}
// 사용
fun main() {
val manager = ConfigManager.getInstance()
manager.loadConfig(
AppConfig(
apiBaseUrl = "https://api.example.com",
apiKey = "secret-key",
timeout = 5000,
maxRetries = 3,
enableLogging = true
)
)
println("API URL: ${manager.getConfig().apiBaseUrl}")
manager.updateApiKey("new-secret-key")
println("Updated API Key: ${manager.getConfig().apiKey}")
}class Singleton {
private static instance: Singleton | null = null;
private counter = 0;
private constructor() {
// private 생성자로 외부 인스턴스화 방지
}
static getInstance(): Singleton {
if (this.instance === null) {
this.instance = new Singleton();
}
return this.instance;
}
incrementCounter(): number {
return ++this.counter;
}
getCounter(): number {
return this.counter;
}
}
// 사용
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true
singleton1.incrementCounter();
console.log(singleton2.getCounter()); // 1 (같은 인스턴스)class SingletonFactory {
private static instances = new Map<any, any>();
static getInstance<T>(constructor: new () => T): T {
if (!this.instances.has(constructor)) {
this.instances.set(constructor, new constructor());
}
return this.instances.get(constructor);
}
static resetInstance<T>(constructor: new () => T): void {
this.instances.delete(constructor);
}
static resetAll(): void {
this.instances.clear();
}
}
// 사용
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class Cache {
private data = new Map<string, any>();
set(key: string, value: any) {
this.data.set(key, value);
}
get(key: string): any {
return this.data.get(key);
}
}
const logger1 = SingletonFactory.getInstance(Logger);
const logger2 = SingletonFactory.getInstance(Logger);
console.log(logger1 === logger2); // true
const cache1 = SingletonFactory.getInstance(Cache);
const cache2 = SingletonFactory.getInstance(Cache);
console.log(cache1 === cache2); // trueES6 모듈은 기본적으로 싱글톤처럼 동작합니다.
// logger.ts
class Logger {
private logs: string[] = [];
log(message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}`;
this.logs.push(logEntry);
console.log(logEntry);
}
getLogs(): string[] {
return [...this.logs];
}
clearLogs(): void {
this.logs = [];
}
}
// 모듈 레벨에서 인스턴스 생성
export const logger = new Logger();
// app.ts
import { logger } from './logger';
logger.log('Application started');
// another-file.ts
import { logger } from './logger';
logger.log('Processing request');
// 동일한 logger 인스턴스 사용import java.util.concurrent.ConcurrentLinkedQueue
class Connection(val id: Int) {
fun execute(query: String) {
println("[Connection-$id] Executing: $query")
}
fun close() {
println("[Connection-$id] Closed")
}
}
class ConnectionPool private constructor() {
private val pool = ConcurrentLinkedQueue<Connection>()
private val maxConnections = 10
private var connectionCount = 0
companion object {
@Volatile
private var instance: ConnectionPool? = null
fun getInstance(): ConnectionPool {
return instance ?: synchronized(this) {
instance ?: ConnectionPool().also {
instance = it
println("ConnectionPool initialized")
}
}
}
}
fun getConnection(): Connection {
return pool.poll() ?: synchronized(this) {
if (connectionCount < maxConnections) {
Connection(++connectionCount).also {
println("Created connection ${it.id}")
}
} else {
throw RuntimeException("Connection pool exhausted")
}
}
}
fun releaseConnection(connection: Connection) {
pool.offer(connection)
println("Released connection ${connection.id}")
}
fun getPoolSize(): Int = pool.size
fun getTotalConnections(): Int = connectionCount
}
// 사용
fun main() {
val pool = ConnectionPool.getInstance()
val conn1 = pool.getConnection()
conn1.execute("SELECT * FROM users")
val conn2 = pool.getConnection()
conn2.execute("SELECT * FROM orders")
pool.releaseConnection(conn1)
pool.releaseConnection(conn2)
println("Pool size: ${pool.getPoolSize()}") // 2
println("Total connections: ${pool.getTotalConnections()}") // 2
}enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR
}
interface LogEntry {
level: LogLevel;
message: string;
timestamp: Date;
context?: Record<string, any>;
}
class Logger {
private static instance: Logger | null = null;
private logs: LogEntry[] = [];
private minLevel: LogLevel = LogLevel.INFO;
private constructor() {}
static getInstance(): Logger {
if (this.instance === null) {
this.instance = new Logger();
}
return this.instance;
}
setMinLevel(level: LogLevel): void {
this.minLevel = level;
}
private log(level: LogLevel, message: string, context?: Record<string, any>): void {
if (level < this.minLevel) return;
const entry: LogEntry = {
level,
message,
timestamp: new Date(),
context
};
this.logs.push(entry);
const levelName = LogLevel[level];
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
console.log(`[${levelName}] ${message}${contextStr}`);
}
debug(message: string, context?: Record<string, any>): void {
this.log(LogLevel.DEBUG, message, context);
}
info(message: string, context?: Record<string, any>): void {
this.log(LogLevel.INFO, message, context);
}
warn(message: string, context?: Record<string, any>): void {
this.log(LogLevel.WARN, message, context);
}
error(message: string, context?: Record<string, any>): void {
this.log(LogLevel.ERROR, message, context);
}
getLogs(level?: LogLevel): LogEntry[] {
if (level === undefined) {
return [...this.logs];
}
return this.logs.filter(entry => entry.level === level);
}
clearLogs(): void {
this.logs = [];
}
}
// 사용
const logger = Logger.getInstance();
logger.setMinLevel(LogLevel.DEBUG);
logger.debug('Debug message', { userId: 123 });
logger.info('User logged in', { userId: 123, ip: '192.168.1.1' });
logger.warn('High memory usage', { usage: 85 });
logger.error('Database connection failed', { error: 'ECONNREFUSED' });
console.log(`Total logs: ${logger.getLogs().length}`);
console.log(`Error logs: ${logger.getLogs(LogLevel.ERROR).length}`);type Subscriber = (state: AppState) => void;
interface AppState {
user: { id: string; name: string } | null;
theme: 'light' | 'dark';
notifications: string[];
}
type Action =
| { type: 'SET_USER'; payload: { id: string; name: string } }
| { type: 'LOGOUT' }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'ADD_NOTIFICATION'; payload: string }
| { type: 'CLEAR_NOTIFICATIONS' };
class Store {
private static instance: Store | null = null;
private state: AppState;
private subscribers: Set<Subscriber> = new Set();
private constructor() {
this.state = {
user: null,
theme: 'light',
notifications: []
};
}
static getInstance(): Store {
if (this.instance === null) {
this.instance = new Store();
}
return this.instance;
}
getState(): AppState {
return { ...this.state };
}
dispatch(action: Action): void {
this.state = this.reduce(this.state, action);
this.notify();
}
private reduce(state: AppState, action: Action): AppState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'LOGOUT':
return { ...state, user: null };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
case 'CLEAR_NOTIFICATIONS':
return { ...state, notifications: [] };
default:
return state;
}
}
subscribe(callback: Subscriber): () => void {
this.subscribers.add(callback);
// Unsubscribe 함수 반환
return () => {
this.subscribers.delete(callback);
};
}
private notify(): void {
this.subscribers.forEach(callback => callback(this.state));
}
}
// 사용
const store = Store.getInstance();
const unsubscribe = store.subscribe(state => {
console.log('State updated:', state);
});
store.dispatch({ type: 'SET_USER', payload: { id: '123', name: 'John' } });
store.dispatch({ type: 'SET_THEME', payload: 'dark' });
store.dispatch({ type: 'ADD_NOTIFICATION', payload: 'Welcome!' });
console.log('Current state:', store.getState());
unsubscribe(); // 구독 해제[주의] 잘못된 사용:
// 모든 것을 싱글톤으로 만들면 안 됨
class UserService private constructor() {
companion object {
val instance = UserService()
}
}
class ProductService private constructor() {
companion object {
val instance = ProductService()
}
}
// 의존성 주입으로 해결 가능한 경우[권장] 대안:
// 의존성 주입 사용
class UserService(private val repository: UserRepository)
class ProductService(private val repository: ProductRepository)
// DI 컨테이너에서 생명주기 관리[주의] 문제:
class PaymentProcessor {
fun process(amount: Double) {
val logger = Logger.getInstance() // 하드 코딩된 의존성
logger.log("Processing $amount")
// ...
}
}
// 테스트 시 Logger를 Mock으로 교체하기 어려움[권장] 해결:
// 의존성 주입으로 변경
class PaymentProcessor(private val logger: Logger) {
fun process(amount: Double) {
logger.log("Processing $amount")
// ...
}
}
// 테스트
class PaymentProcessorTest {
@Test
fun testProcess() {
val mockLogger = MockLogger()
val processor = PaymentProcessor(mockLogger)
processor.process(100.0)
// mockLogger 검증
}
}[주의] 문제:
class SerializableSingleton private constructor() : Serializable {
companion object {
val instance = SerializableSingleton()
}
var counter = 0
}
// 역직렬화 시 새 인스턴스 생성![권장] 해결:
class SerializableSingleton private constructor() : Serializable {
companion object {
val instance = SerializableSingleton()
}
var counter = 0
// 역직렬화 시 기존 인스턴스 반환
private fun readResolve(): Any {
return instance
}
}또는 Enum 사용:
enum class SafeSingleton {
INSTANCE;
var counter = 0
}import kotlin.system.measureTimeMillis
fun benchmarkSingletons() {
val iterations = 10_000_000
// Eager Initialization
val eagerTime = measureTimeMillis {
repeat(iterations) {
EagerSingleton.instance
}
}
// DCL
val dclTime = measureTimeMillis {
repeat(iterations) {
val obj = Singleton.createInstance()
}
}
// Holder Pattern
val holderTime = measureTimeMillis {
repeat(iterations) {
HolderSingleton.getInstance()
}
}
println("Eager: ${eagerTime}ms")
println("DCL: ${dclTime}ms")
println("Holder: ${holderTime}ms")
}
// 결과 (예시):
// Eager: 45ms
// DCL: 52ms
// Holder: 48ms결론:
싱글톤 패턴은 강력하지만 신중하게 사용해야 합니다.
핵심 권장사항:
적용 시기:
주의사항:
올바르게 구현된 스레드 안전 싱글톤은 안전하고 효율적인 전역 상태 관리를 제공합니다.
Rust의 소유권 시스템과 타입 시스템을 활용한 컴파일 타임 동시성 안전성 보장 메커니즘 완벽 분석
실행 취소와 재실행을 지원하는 유연한 시스템 설계를 위한 커맨드 패턴 구현
의존성을 낮추고 확장성을 높이는 팩토리 메서드 패턴의 구조와 실전 구현 가이드