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 โ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
๋์ผํ ์ธ์คํด์ค ๋ฐํ
// ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ 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 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// โ 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()
}
}
}// โ 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()) }
}
}// โ ์จ๊ฒจ์ง ์์กด์ฑ - ํด๋์ค๋ง ๋ณด๊ณ ์์กด๊ด๊ณ ํ์
๋ถ๊ฐ
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) {
// ์์กด์ฑ์ด ๋ช
์์
}
}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์ผ๋ก ๊ด๋ฆฌ๋๋ค.
// 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: ์ง์ 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 | Static |
|---|---|---|
| ์์ | ๊ฐ๋ฅ | ๋ถ๊ฐ๋ฅ |
| ์ธํฐํ์ด์ค ๊ตฌํ | ๊ฐ๋ฅ | ๋ถ๊ฐ๋ฅ |
| ์ง์ฐ ์ด๊ธฐํ | ๊ฐ๋ฅ | ์ด๋ ค์ |
| ํ ์คํธ (Mock) | ๊ฐ๋ฅ (DI ์) | ์ด๋ ค์ |
| ๋คํ์ฑ | ๊ฐ๋ฅ | ๋ถ๊ฐ๋ฅ |
- Effective Java 3/E - Item 3: Enforce the singleton property with a private constructor or an enum type
- Head First Design Patterns - Singleton ์ฑํฐ