본문 바로가기

스폰지밥으로 공부하는 swift/객체지향과 디자인패턴

싱글턴 패턴이란?

728x90

싱글턴 패턴이 뭐죠?

싱글턴 패턴은 클래스의 인스턴스가 오직 하나만 생성되어 전역적으로 접근 가능하도록 보장하는 디자인 패턴입니다.

 

왜 써야하는거죠?

싱글턴 패턴을 사용하면 전역적으로 하나의 인스턴스가 생겼다는 것이 보장되면서도 어디서든 쉽게 접근이 가능해요.

즉 전역적으로 접근이 가능하지만 일관된 객체 생성이 보장되어야 할 때 유용합니다!

 

말이 좀 딱딱한 것 같아서 바로 집게리아를 활용해 예시를 들어보겠습니다 ㅎㅎ

 

 

class CashRegisterManager {
    // 싱글턴 인스턴스를 저장할 static property를 선언
    static let shared = CashRegisterManager()

    // 초기화를 private으로 선언하여 외부에서 인스턴스 생성을 막기
    private init() {}

    // 계산대 관리에 필요한 메서드들
    func openRegister() {
        print("계산대가 열렸습니다.")
    }

    func closeRegister() {
        print("계산대가 닫혔습니다.")
    }
}

// 어디서든지 CashRegisterManager의 인스턴스에 접근할 수 있어요
CashRegisterManager.shared.openRegister()

싱글턴 패턴을 사용하지 않는다면?

싱글턴 패턴을 사용하지 않고 CashRegisterManager를 구현해 보았는데요, 이렇게 구현을 한다면 각각의 객체가 필요한 시점에 새로운 인스턴스를 생성하여 사용하게 됩니다.

 

이런 구현 방식은 모든 직원이 자신만의 계산대를 가지게 되는 것과 비슷하다고 볼 수 있어요. 코드에서 보시다시피 계산대의 상태가 일관되지 않고 중앙에서 관리하기가 어려워지죠.

class CashRegisterManager {
    var cashOnHand: Double = 1000.0  // 계산대에 있는 현금

    func addCash(amount: Double) {
        cashOnHand += amount
        print("현금이 추가되었습니다. 현재 현금: \(cashOnHand)")
    }

    func removeCash(amount: Double) {
        if cashOnHand >= amount {
            cashOnHand -= amount
            print("현금이 인출 되었습니다. 현재 현금: \(cashOnHand)")
        } else {
            print("충분한 현금이 없습니다.")
        }
    }
}

// 집게리아의 다른 부분에서 계산대를 사용
func processTransactions() {
    let managerForMorningShift = CashRegisterManager()  // 아침 교대용 계산대 매니저
    managerForMorningShift.addCash(amount: 50.0)
    
    let managerForEveningShift = CashRegisterManager()  // 저녁 교대용 계산대 매니저
    managerForEveningShift.removeCash(amount: 30.0)
}

processTransactions()

 

싱글턴 패턴을 사용한다면 아래 나온 문제점들을 편리하게 개선할 수 있게 됩니다 :) 

 

1. 상태 일관성 및 데이터 무결성

각 인스턴스가 독립적으로 작동하기 때문에, 현금의 총액을 정확하게 계산하거나 보고하는 것이 어려워요.


2. 메모리 자원 낭비

새로운 인스턴스를 불필요하게 많이 생성하게 되면, 메모리 사용이 늘어나고 시스템 자원이 낭비될 수 있어요.

 

3. 중복 코드

계산대를 관리하는 로직이 여러 인스턴스에 걸쳐 중복될 수 있어요.

 

단점은 없나요?

싱글턴 패턴의 사용은 전역적으로 접근 가능한 인스턴스를 통해 편의성을 제공하지만, 동시에 데이터의 일관성 유지, 코드 디버깅, 시스템의 결합도, 그리고 테스트 용이성 측면에서 복잡성과 제약을 가져올 수 있습니다.

 

class CashRegisterManager {
    static let shared = CashRegisterManager()
    private var cashOnHand: Double = 1000.0  // 계산대에 있는 현금

    private init() {}

    func addCash(amount: Double) {
        cashOnHand += amount
        print("현금이 추가되었습니다. 현재 잔액: \(cashOnHand)")
    }

    func removeCash(amount: Double) {
        if cashOnHand >= amount {
            cashOnHand -= amount
            print("현금이 제거되었습니다. 현재 현금: \(cashOnHand)")
        } else {
            print("충분한 현금이 없습니다.")
        }
    }
}

// 집게리아의 다른 부분에서 계산대의 현금을 변경합니다.
func makeTransaction() {
    CashRegisterManager.shared.addCash(amount: 50.0)
}

func refundTransaction() {
    CashRegisterManager.shared.removeCash(amount: 30.0)
}

makeTransaction()  // 거래가 발생해 현금이 들어감
refundTransaction()  // 환불이 일어나 현금이 나감

  1. 동시성 이슈: 여러 부분에서 동시에 CashRegisterManager의 메서드를 호출한다면, 현금의 정확한 금액을 추적하기 어려워질 수 있어요. 환불과 거래가 동시에 일어난다면 현금의 최종 금액이 얼마인지 예측하기 어렵고 위 그림처럼 대혼란이 발생할겁니다 ㅎㅎ
  2. 디버깅의 어려움: 모든 코드가 shared 인스턴스에 접근할 수 있기 때문에 로직에 문제가 생겼을 때, 어떤 부분의 코드가 문제를 일으켰는지 찾기가 어려워집니다.
  3. 강한 결합: CashRegisterManager가 계산대의 모든 업무를 책임지게 되면, 계산대와 관련된 모든 기능이 이 싱글턴 인스턴스에 의존하게 됩니다. 이는 코드의 강한 결합을 만들어, 한 부분을 변경하면 싱글턴 인스턴스에 영향을 끼칠 수 있어요.
  4. 테스트의 어려움: 싱글턴 인스턴스는 전역 상태를 가지므로, 테스트 환경에서 이 상태를 초기화하거나 모의 객체(mock object)로 대체하기 어려워요. 이는 테스트를 복잡하게 만들고, 유닛테스트가 어려울 수 있습니다.

한줄 요약