Skip to content

Latest commit

ย 

History

History
599 lines (482 loc) ยท 17.1 KB

File metadata and controls

599 lines (482 loc) ยท 17.1 KB

Singleton Pattern

๊ฐœ๋…

Singleton ํŒจํ„ด์€ ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ์˜ค์ง ํ•˜๋‚˜๋งŒ ์ƒ์„ฑ๋˜๋„๋ก ๋ณด์žฅํ•˜๊ณ , ๊ทธ ์ธ์Šคํ„ด์Šค์— ๋Œ€ํ•œ ์ „์—ญ ์ ‘๊ทผ์ ์„ ์ œ๊ณตํ•˜๋Š” ํŒจํ„ด์ด๋‹ค.

GoF ์ƒ์„ฑ ํŒจํ„ด ์ค‘ ํ•˜๋‚˜๋กœ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ๋‹จ ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋งŒ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•œ๋‹ค. ํ•ต์‹ฌ์€ "์œ ์ผํ•œ ์ธ์Šคํ„ด์Šค"์™€ "์ „์—ญ ์ ‘๊ทผ"์ด๋‹ค.

ํ•ต์‹ฌ ์›์น™

์›์น™ ์„ค๋ช…
๋‹จ์ผ ์ธ์Šคํ„ด์Šค ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋Š” ์˜ค์ง ํ•˜๋‚˜๋งŒ ์กด์žฌ
์ „์—ญ ์ ‘๊ทผ ์–ด๋””์„œ๋“  ๋™์ผํ•œ ์ธ์Šคํ„ด์Šค์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
์ง€์—ฐ ์ดˆ๊ธฐํ™” ํ•„์š”ํ•œ ์‹œ์ ์— ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ (์„ ํƒ์ )
์ƒ์„ฑ ์ œ์–ด ์™ธ๋ถ€์—์„œ new๋กœ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ๋ถˆ๊ฐ€

์™œ ํ•„์š”ํ•œ๊ฐ€

ํ•ด๊ฒฐํ•˜๋ ค๋Š” ๋ฌธ์ œ

// โŒ ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์ผ๊ด€์„ฑ ์—†๋Š” ์ƒํƒœ ๋ฐœ์ƒ
class DatabaseConnection {
    private var connectionPool: List<Connection> = mutableListOf()
    
    fun getConnection(): Connection {
        // ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๋ฉด ์ปค๋„ฅ์…˜ ํ’€์ด ๋ถ„์‚ฐ๋จ
        return connectionPool.firstOrNull() ?: createNewConnection()
    }
}

// ๊ฐ๊ฐ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค - ์ปค๋„ฅ์…˜ ํ’€์ด ๊ณต์œ ๋˜์ง€ ์•Š์Œ
val db1 = DatabaseConnection()
val db2 = DatabaseConnection()
val db3 = DatabaseConnection()
  • ๋ฆฌ์†Œ์Šค๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•˜๋Š” ๊ฐ์ฒด๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ ์ƒ์„ฑ๋จ
  • ์„ค์ • ์ •๋ณด๊ฐ€ ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Œ
  • ๋ฉ”๋ชจ๋ฆฌ ๋‚ญ๋น„ ๋ฐ ์ƒํƒœ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ
  • ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ค์›€

์ œ๊ณตํ•˜๋Š” ๊ฐ€์น˜

  • ๋ฆฌ์†Œ์Šค ์ ˆ์•ฝ: ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์ƒ์„ฑํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ 
  • ์ƒํƒœ ์ผ๊ด€์„ฑ: ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋™์ผํ•œ ์ƒํƒœ ๊ณต์œ 
  • ์ „์—ญ ์ ‘๊ทผ: ์–ด๋””์„œ๋“  ์‰ฝ๊ฒŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ์ƒ์„ฑ ์ œ์–ด: ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์‹œ์ ๊ณผ ๋ฐฉ๋ฒ•์„ ์™„์ „ํžˆ ์ œ์–ด

๋™์ž‘ ์›๋ฆฌ

๊ตฌ์กฐ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Singleton                           โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  - instance: Singleton          โ† ์œ ์ผํ•œ ์ธ์Šคํ„ด์Šค ์ €์žฅ       โ”‚
โ”‚  - Singleton()                  โ† private ์ƒ์„ฑ์ž            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  + getInstance(): Singleton     โ† ์ „์—ญ ์ ‘๊ทผ์                โ”‚
โ”‚  + businessLogic()              โ† ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ”‚ ํ˜ธ์ถœ
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  getInstance()  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  getInstance()  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Client A โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚          โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ Client B โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                 โ”‚ instance โ”‚                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚          โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  getInstance()  โ”‚          โ”‚  getInstance()  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Client C โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚          โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ Client D โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             ๋™์ผํ•œ ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜

Java ๊ธฐ๋ณธ ๊ตฌํ˜„

// ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ Singleton (Thread-safe ํ•˜์ง€ ์•Š์Œ)
public class BasicSingleton {
    private static BasicSingleton instance;
    
    // private ์ƒ์„ฑ์ž - ์™ธ๋ถ€์—์„œ new ๋ถˆ๊ฐ€
    private BasicSingleton() {}
    
    public static BasicSingleton getInstance() {
        if (instance == null) {
            instance = new BasicSingleton();
        }
        return instance;
    }
}

// Thread-safe Singleton (synchronized)
public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton() {}
    
    // synchronized๋กœ ๋™๊ธฐํ™” - ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ€๋Šฅ
    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

// Double-Checked Locking (DCL)
public class DCLSingleton {
    // volatile: ๋ฉ”๋ชจ๋ฆฌ ๊ฐ€์‹œ์„ฑ ๋ณด์žฅ
    private static volatile DCLSingleton instance;
    
    private DCLSingleton() {}
    
    public static DCLSingleton getInstance() {
        if (instance == null) {                    // 1์ฐจ ๊ฒ€์‚ฌ (๋ฝ ์—†์ด)
            synchronized (DCLSingleton.class) {
                if (instance == null) {            // 2์ฐจ ๊ฒ€์‚ฌ (๋ฝ ์•ˆ์—์„œ)
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

// Bill Pugh Singleton (๊ถŒ์žฅ) - Initialization-on-demand holder
public class BillPughSingleton {
    private BillPughSingleton() {}
    
    // static inner class๋Š” ์™ธ๋ถ€ ํด๋ž˜์Šค ๋กœ๋“œ ์‹œ ๋กœ๋“œ๋˜์ง€ ์•Š์Œ
    // getInstance() ํ˜ธ์ถœ ์‹œ์ ์— ๋กœ๋“œ๋˜์–ด ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    private static class SingletonHolder {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    
    public static BillPughSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

// Enum Singleton (๊ฐ€์žฅ ์•ˆ์ „) - Joshua Bloch ๊ถŒ์žฅ
public enum EnumSingleton {
    INSTANCE;
    
    public void businessLogic() {
        // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
    }
}

Kotlin ๊ตฌํ˜„

// Kotlin object - ์–ธ์–ด ๋ ˆ๋ฒจ์—์„œ Singleton ์ง€์›
object DatabaseConfig {
    var host: String = "localhost"
    var port: Int = 5432
    var database: String = "mydb"
    
    fun getConnectionString(): String {
        return "jdbc:postgresql://$host:$port/$database"
    }
}

// ์‚ฌ์šฉ - ๋ณ„๋„์˜ getInstance() ํ˜ธ์ถœ ๋ถˆํ•„์š”
val connectionString = DatabaseConfig.getConnectionString()
DatabaseConfig.host = "production-db.example.com"
// lazy ์ดˆ๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
class AppConfig private constructor() {
    var appName: String = ""
    var version: String = ""
    var debug: Boolean = false
    
    companion object {
        // by lazy๋Š” thread-safeํ•˜๊ณ  ์ตœ์ดˆ ์ ‘๊ทผ ์‹œ ์ดˆ๊ธฐํ™”
        val instance: AppConfig by lazy { 
            AppConfig().apply {
                // ์ดˆ๊ธฐํ™” ๋กœ์ง
                loadFromFile()
            }
        }
    }
    
    private fun loadFromFile() {
        // ์„ค์ • ํŒŒ์ผ์—์„œ ๋กœ๋“œ
        appName = "MyApp"
        version = "1.0.0"
        debug = false
    }
}

// ์‚ฌ์šฉ
val config = AppConfig.instance
println(config.appName)  // MyApp

์ฃผ์˜์‚ฌํ•ญ

1. ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ๋™๊ธฐํ™” ํ•„์ˆ˜

// โŒ Race Condition ๋ฐœ์ƒ ๊ฐ€๋Šฅ
class UnsafeSingleton {
    companion object {
        private var instance: UnsafeSingleton? = null
        
        fun getInstance(): UnsafeSingleton {
            if (instance == null) {  // Thread A, B ๋™์‹œ ์ง„์ž… ๊ฐ€๋Šฅ
                instance = UnsafeSingleton()  // ์ธ์Šคํ„ด์Šค๊ฐ€ 2๊ฐœ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Œ
            }
            return instance!!
        }
    }
}

// โœ… Kotlin object ์‚ฌ์šฉ - Thread-safe ๋ณด์žฅ
object SafeSingleton {
    // Kotlin object๋Š” JVM ๋ ˆ๋ฒจ์—์„œ thread-safe
}

// โœ… ๋˜๋Š” lazy ์‚ฌ์šฉ
class SafeLazySingleton {
    companion object {
        val instance: SafeLazySingleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            SafeLazySingleton()
        }
    }
}

2. ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์›€

// โŒ Singleton ์ง์ ‘ ์‚ฌ์šฉ - Mock ๋ถˆ๊ฐ€
class OrderService {
    fun createOrder(order: Order) {
        // Singleton ์ง์ ‘ ์ฐธ์กฐ - ํ…Œ์ŠคํŠธ ์‹œ DB ์—ฐ๊ฒฐ๋จ
        DatabaseConnection.instance.execute("INSERT INTO orders ...")
    }
}

// โœ… ์˜์กด์„ฑ ์ฃผ์ž…์œผ๋กœ ํ•ด๊ฒฐ
interface DatabaseConnection {
    fun execute(sql: String)
}

object ProductionDatabaseConnection : DatabaseConnection {
    override fun execute(sql: String) {
        // ์‹ค์ œ DB ์—ฐ๊ฒฐ
    }
}

class OrderService(
    private val database: DatabaseConnection  // ์ฃผ์ž…๋ฐ›์Œ
) {
    fun createOrder(order: Order) {
        database.execute("INSERT INTO orders ...")
    }
}

// ํ…Œ์ŠคํŠธ
class OrderServiceTest {
    @Test
    fun `์ฃผ๋ฌธ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ`() {
        val mockDatabase = mockk<DatabaseConnection>()
        val service = OrderService(mockDatabase)
        
        service.createOrder(testOrder)
        
        verify { mockDatabase.execute(any()) }
    }
}

3. ์ „์—ญ ์ƒํƒœ๋กœ ์ธํ•œ ์ˆจ๊ฒจ์ง„ ์˜์กด์„ฑ

// โŒ ์ˆจ๊ฒจ์ง„ ์˜์กด์„ฑ - ํด๋ž˜์Šค๋งŒ ๋ณด๊ณ  ์˜์กด๊ด€๊ณ„ ํŒŒ์•… ๋ถˆ๊ฐ€
class PaymentProcessor {
    fun process(payment: Payment) {
        val config = AppConfig.instance  // ์ˆจ๊ฒจ์ง„ ์˜์กด์„ฑ
        val logger = Logger.instance     // ์ˆจ๊ฒจ์ง„ ์˜์กด์„ฑ
        // ...
    }
}

// โœ… ๋ช…์‹œ์  ์˜์กด์„ฑ
class PaymentProcessor(
    private val config: AppConfig,
    private val logger: Logger
) {
    fun process(payment: Payment) {
        // ์˜์กด์„ฑ์ด ๋ช…์‹œ์ 
    }
}

4. ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ ์œ„๋ฐ˜ ๊ฐ€๋Šฅ

Singleton์€ "์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ๊ด€๋ฆฌ"์™€ "๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง" ๋‘ ๊ฐ€์ง€ ์ฑ…์ž„์„ ๊ฐ€์ง„๋‹ค.

// โŒ ๋‘ ๊ฐ€์ง€ ์ฑ…์ž„์ด ํ˜ผ์žฌ
object OrderManager {
    // ์ฑ…์ž„ 1: ์ธ์Šคํ„ด์Šค ๊ด€๋ฆฌ (object ํ‚ค์›Œ๋“œ๋กœ ์•”์‹œ์ )
    
    // ์ฑ…์ž„ 2: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
    fun createOrder() { }
    fun cancelOrder() { }
    fun processPayment() { }
}

// โœ… Spring์—์„œ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ
@Service  // Spring์ด Singleton์œผ๋กœ ๊ด€๋ฆฌ
class OrderManager {
    // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ๋‹ด๋‹น
    fun createOrder() { }
    fun cancelOrder() { }
}

์‹ค์ „ ์ ์šฉ

์ ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

์ƒํ™ฉ ์˜ˆ์‹œ
์„ค์ • ๊ด€๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •, ํ™˜๊ฒฝ ๋ณ€์ˆ˜
๋กœ๊น… ๋กœ๊ทธ ์ธ์Šคํ„ด์Šค ๊ณต์œ 
์บ์‹œ ์ „์—ญ ์บ์‹œ ๋งค๋‹ˆ์ €
์Šค๋ ˆ๋“œ ํ’€ ์ปค๋„ฅ์…˜ ํ’€, ์Šค๋ ˆ๋“œ ํ’€
๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฐ์ฒด ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ, ์„œ๋น„์Šค ๋กœ์ผ€์ดํ„ฐ

๋กœ๊น… ์‹œ์Šคํ…œ ์˜ˆ์ œ

object Logger {
    enum class Level { DEBUG, INFO, WARN, ERROR }
    
    private var minLevel: Level = Level.INFO
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    
    fun setLevel(level: Level) {
        minLevel = level
    }
    
    fun debug(message: String) = log(Level.DEBUG, message)
    fun info(message: String) = log(Level.INFO, message)
    fun warn(message: String) = log(Level.WARN, message)
    fun error(message: String, throwable: Throwable? = null) {
        log(Level.ERROR, message)
        throwable?.printStackTrace()
    }
    
    private fun log(level: Level, message: String) {
        if (level.ordinal >= minLevel.ordinal) {
            val timestamp = dateFormat.format(Date())
            val threadName = Thread.currentThread().name
            println("[$timestamp] [$threadName] ${level.name}: $message")
        }
    }
}

// ์–ด๋””์„œ๋“  ๋™์ผํ•œ ๋กœ๊ฑฐ ์‚ฌ์šฉ
class UserService {
    fun createUser(name: String) {
        Logger.info("Creating user: $name")
        // ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋กœ์ง
        Logger.debug("User created successfully")
    }
}

class OrderService {
    fun processOrder(orderId: String) {
        Logger.info("Processing order: $orderId")
        try {
            // ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ๋กœ์ง
        } catch (e: Exception) {
            Logger.error("Order processing failed", e)
        }
    }
}

์บ์‹œ ๋งค๋‹ˆ์ € ์˜ˆ์ œ

object CacheManager {
    private val cache = ConcurrentHashMap<String, CacheEntry>()
    private val defaultTtlMs = 5 * 60 * 1000L  // 5๋ถ„
    
    data class CacheEntry(
        val value: Any,
        val expireAt: Long
    )
    
    fun <T> get(key: String): T? {
        val entry = cache[key] ?: return null
        
        return if (System.currentTimeMillis() < entry.expireAt) {
            @Suppress("UNCHECKED_CAST")
            entry.value as T
        } else {
            cache.remove(key)
            null
        }
    }
    
    fun put(key: String, value: Any, ttlMs: Long = defaultTtlMs) {
        cache[key] = CacheEntry(
            value = value,
            expireAt = System.currentTimeMillis() + ttlMs
        )
    }
    
    fun remove(key: String) {
        cache.remove(key)
    }
    
    fun clear() {
        cache.clear()
    }
    
    fun size(): Int = cache.size
}

// ์‚ฌ์šฉ
class ProductService {
    fun getProduct(id: Long): Product {
        val cacheKey = "product:$id"
        
        // ์บ์‹œ์—์„œ ๋จผ์ € ์กฐํšŒ
        CacheManager.get<Product>(cacheKey)?.let { return it }
        
        // DB์—์„œ ์กฐํšŒ
        val product = productRepository.findById(id)
        
        // ์บ์‹œ์— ์ €์žฅ
        CacheManager.put(cacheKey, product)
        
        return product
    }
}

Spring ํ™˜๊ฒฝ: Singleton Scope

Spring์—์„œ๋Š” ๋นˆ์ด ๊ธฐ๋ณธ์ ์œผ๋กœ Singleton์œผ๋กœ ๊ด€๋ฆฌ๋œ๋‹ค.

// Spring Bean์€ ๊ธฐ๋ณธ์ ์œผ๋กœ Singleton
@Service
class UserService(
    private val userRepository: UserRepository
) {
    // ๋ชจ๋“  ์š”์ฒญ์—์„œ ๋™์ผํ•œ ์ธ์Šคํ„ด์Šค ์‚ฌ์šฉ
    fun findById(id: Long): User = userRepository.findById(id).orElseThrow()
}

// Singleton์ด์ง€๋งŒ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ - DI ๋•๋ถ„
@SpringBootTest
class UserServiceTest {
    @MockBean
    private lateinit var userRepository: UserRepository
    
    @Autowired
    private lateinit var userService: UserService
    
    @Test
    fun `์‚ฌ์šฉ์ž ์กฐํšŒ ํ…Œ์ŠคํŠธ`() {
        // Mock ์ฃผ์ž…๋œ ์ƒํƒœ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
    }
}
// ๋ช…์‹œ์  Scope ์ง€์ •
@Configuration
class AppConfig {
    
    @Bean
    @Scope("singleton")  // ๊ธฐ๋ณธ๊ฐ’, ์ƒ๋žต ๊ฐ€๋Šฅ
    fun singletonBean(): MyService = MyService()
    
    @Bean
    @Scope("prototype")  // ๋งค๋ฒˆ ์ƒˆ ์ธ์Šคํ„ด์Šค
    fun prototypeBean(): MyService = MyService()
}

Before/After ๋น„๊ต

// โŒ Before: ์ง์ ‘ Singleton ํŒจํ„ด ๊ตฌํ˜„
class ConnectionPool private constructor() {
    private val connections = mutableListOf<Connection>()
    
    companion object {
        @Volatile
        private var instance: ConnectionPool? = null
        
        fun getInstance(): ConnectionPool {
            return instance ?: synchronized(this) {
                instance ?: ConnectionPool().also { instance = it }
            }
        }
    }
    
    fun getConnection(): Connection {
        return connections.firstOrNull { it.isAvailable() }
            ?: createNewConnection()
    }
}

// ์‚ฌ์šฉ
val pool = ConnectionPool.getInstance()
val conn = pool.getConnection()

// โœ… After: Spring์— ์œ„์ž„ + HikariCP ์‚ฌ์šฉ
@Configuration
class DataSourceConfig {
    @Bean
    fun dataSource(): DataSource {
        return HikariDataSource().apply {
            jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
            username = "user"
            password = "password"
            maximumPoolSize = 10
        }
    }
}

// Spring์ด Singleton์œผ๋กœ ๊ด€๋ฆฌ, ํ…Œ์ŠคํŠธ๋„ ์‰ฌ์›€
@Service
class UserRepository(
    private val dataSource: DataSource  // ์ฃผ์ž…๋ฐ›์Œ
) {
    fun findById(id: Long): User {
        dataSource.connection.use { conn ->
            // ...
        }
    }
}

๊ตฌํ˜„ ๋ฐฉ์‹ ๋น„๊ต

๋ฐฉ์‹ Thread-Safe Lazy ์žฅ์  ๋‹จ์ 
๊ธฐ๋ณธ ๋ฐฉ์‹ โŒ โœ… ๋‹จ์ˆœํ•จ ๋™์‹œ์„ฑ ๋ฌธ์ œ
synchronized โœ… โœ… ์•ˆ์ „ํ•จ ์„ฑ๋Šฅ ์ €ํ•˜
DCL โœ… โœ… ์„ฑ๋Šฅ ์ข‹์Œ ์ฝ”๋“œ ๋ณต์žก
Bill Pugh โœ… โœ… ๊น”๋”, ์„ฑ๋Šฅ ์ข‹์Œ Java ์ „์šฉ
Enum โœ… โŒ ์ง๋ ฌํ™”/๋ฆฌํ”Œ๋ ‰์…˜ ์•ˆ์ „ ์ƒ์† ๋ถˆ๊ฐ€
Kotlin object โœ… โœ… ๊ฐ„๋‹จ๋ช…๋ฃŒ Kotlin ์ „์šฉ

Singleton vs Static

๋น„๊ต ํ•ญ๋ชฉ Singleton Static
์ƒ์† ๊ฐ€๋Šฅ ๋ถˆ๊ฐ€๋Šฅ
์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ ๊ฐ€๋Šฅ ๋ถˆ๊ฐ€๋Šฅ
์ง€์—ฐ ์ดˆ๊ธฐํ™” ๊ฐ€๋Šฅ ์–ด๋ ค์›€
ํ…Œ์ŠคํŠธ (Mock) ๊ฐ€๋Šฅ (DI ์‹œ) ์–ด๋ ค์›€
๋‹คํ˜•์„ฑ ๊ฐ€๋Šฅ ๋ถˆ๊ฐ€๋Šฅ

์ฐธ๊ณ  ์ž๋ฃŒ

์„œ์ 

์ถ”์ฒœ ์‚ฌ์ดํŠธ

๊ด€๋ จ TIL