헤드 퍼스트 디자인 패턴 - 5장 싱글톤 패턴 in Swift

koi·2022년 10월 24일
1
post-thumbnail

◽️ 인스턴스를 하나만 만들어야하는 클래스의 필요성

  • 스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체, 프린터나 그래픽 카드 같은 디바이스를 위한 디바이스 드라이버 등..
  • 이런 형식의 객체를 쓸 때는 인스턴스를 두 개 이상 만들게 되면 프로그램이 이상하게 돌아간다든가 자원을 불필요하게 잡아먹는다든가 결과의 일관성이 없어지는 문제가 생길 수 있음

따라서 인스턴스를 하나만 만들어야하는 클래스가 필요하다!


◽️ 싱글톤 패턴

  • 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴
  • 싱글톤 패턴을 사용하면 전역 변수를 사용할 때와 마찬가지로 객체 인스턴스를 어디서든지 액세스할 수 있도록 할 수 있음
  • 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야함
  • 클래스의 객체가 자원을 많이 잡아먹는 경우에는 lazily 생성 기법이 유용하게 사용될 수 있음

전역 변수를 사용했을 때의 단점

  • 전역 변수에 객체를 대입하면 *애플리케이션이 시작될 때 객체가 생성되기 때문에 한번도 쓰지 않는다면 자원을 낭비하는 객체가 된다.
    *swift의 경우 타입 저장 프로퍼티가 해당되며 자동으로 lazy로 선언되어 필요할 때 생성된다.
  • 싱글톤 패턴을 쓰면 필요할 때만 객체를 만들 수 있다.

고전적인 싱글톤 패턴 구현법

public class Singleton {
	private static Singleton uniqueInstance;
    // 기타 인스턴스 변수
	private Singleton() {}

	public static Singleton getInstance() {

		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
	// 기타 메소드
}
  • uniqueInstance에 하나밖에 없는 인스턴스가 저장됨
  • uniqueInstance가 null이라면 (아직 인스턴스가 만들어지지 않았다면) private로 선언된 생성자를 이용해서 Singleton객체를 만든다음 uniqueInstance 변수에 객체를 대입함

이렇게 하면 인스턴스가 필요한 상황이 닥치기 전에는 아예 인스턴스를 생성하지 않게 되고 이런 방법을 게으른 인스턴스 생성 lazy instantiation 이라고 부른다.

그냥 보기에는 코드에 문제가 없어보이지만 만약 멀티 스레드로 가동한다면 어떻게 될까?

이런 식으로 싱글톤인데도 불구하고 두 개의 인스턴스가 생성되게 된다.


멀티 스레딩 해결 방법

1️⃣ getInstance() 메소드를 동기화

public class Singleton {
	private static Singleton uniqueInstance;
	private Singleton() {}
	public static synchronized Singleton getInstance() { // 동기화
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
}
  • 동기화가 꼭 필요한 시점은 메소드가 시작될 때 뿐이다.
    즉 싱글톤 인스턴스를 대입하고 나면 이 메소드를 동기화 상태로 유지시킬 필요가 없다. 불필요한 오버헤드만 증가시킬 뿐이다.
  • getInstance() 메소드가 큰 부담을 주지 않는다면 놔둬도 괜찮다
  • 메소드를 동기화하면 성능이 100배 정도 저하된다.

2️⃣ 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들기

public class Singleton {
	private static Singleton uniqueInstance = new Singleton(); // 생성
	private Singleton() {}
	public static synchronized Singleton getInstance() {
		return uniqueInstance;
	}
}
  • 애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면,또는 인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면 처음부터 Singleton인스턴스를 만들어 버리는 것도 괜찮은 방법이다.

3️⃣ DCL(Double-Checking Locking)

public class Singleton {
	private volatile static Singleton uniqueInstance;
	private Singleton() {}
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			synchronized (Singleton.class) {
				if (uniqueInstance == null) {
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}
  • DCL을 사용하면 인스턴스가 생성되어 있는지 확인한 다음, 생성되지 않았을 때만 동기화를 할 수 있다.
  • 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 된다.

참고
volatile 키워드를 사용하면 자바의 일종의 최적화인 리오더링(보통 컴파일 과정에서 일어나며, 프로그래머가 만들어낸 코드는 컴파일 될 때 좀더 빠르게 실행될 수 있도록 조작이 가해져 최적하됨)을 회피하여 읽기와 쓰기순서를 보장 멀티스레딩을 쓰더라도 uniqueInstance변수가 Singleton 인스턴스로 초기화 되는 과정이 올바르게 진행되도록 할 수 있다. 하지만 DCL은 자바1.5이상의 버전에서만 사용가능하다. 자바 1.4 및 그 전에 나온 버전의 JVM 중에는 volatile 키워드를 사용하더라도 동기화가 잘 안되는 것이 많다.


💡 그렇다면, Swift의 싱글톤은 Thread-Safe할까?

static을 사용해 타입 프로퍼티로 인스턴스를 생성하면 사용 시점에 초기화(lazy)된다.
따라서 Singleton Instance가 최초 생성되기 전까진 메모리에 올라가지 않고, Dispatch_once도 자동 적용되어서 별 코드 없이도 Instance가 여러 개 생성되지 않는, Thread-Safe한 Singleton이 된다.

우리의 swift는 별다른 코드없이도 static 으로 인스턴스를 생성할 경우에 Thread-Safe하다고 합니다..👍


◽️ Swift로 싱글톤 구현해보기

Swift로 싱글턴 패턴은 아래와 같이 만들 수 있다.
아래의 코드는 싱글턴을 정의하는데에 필요한 기본 코드이다.
필요에 따라서 코드를 추가로 작성하면 된다.

class Singleton {
    // 1
    static let defaults = Singleton()
    // 2
    private init() { }
}
  1. 유일한 객체가 될 프로퍼티를 static으로 정의한 후, 자기 자신을 할당해준다. (프로퍼티의 이름이 정해져있지는 않지만 보통 shared, common, defaults, basic 등의 이름을 자주 사용함)
  2. 초기화 메서드의 접근 제한을 설정하여 외부에서는 초기화가 불가능하도록 설정한다.

🍞 케로로 빵 가게 만들어보기


Struct로 구현할 수는 없을까?

야곰닷넷 - Singleton Pattern

class는 참조 타입, struct는 값 타입이다.
따라서 struct로 싱글톤을 만들면, 싱글톤 객체를 인스턴스화할 때 유일하지 못한 객체가 되게 된다.

struct Singleton {
    static let shared = Singleton()
    private init() { }
}

func address(of object: UnsafeRawPointer) -> String {
    let address = Int(bitPattern: object)
    return String(format: "%p", address)
}

var singleton1 = Singleton.shared
var singleton2 = Singleton.shared

print(address(of: &singleton1))
print(address(of: &singleton2))

/* 
0x1030fca90
0x1030fca98
*/

만약 구조체를 인스턴스화해서 사용하지 않는다면 싱글톤처럼 사용할 수 있다.

◽️ 싱글톤의 장단점

  • 유일한 객체를 만들어서 다양한 객체들에게 공유되는 객체를 만들 수 있다
  • 재사용이 가능하여 메모리 낭비를 방지할 수 있다
  • 객체지향 관점에서 본다면 인스턴스들 간에 결합도가 높아져서 OCP(개방-폐쇄 원칙, Open-Closed Principle) 을 위배하게 된다

🤔 그래서 static 쓰는 거랑 뭐가 다른거지??

swift에서의 static (타입)저장 프로퍼티는 자동으로 lazy 연산되니까, 객체를 한번도 사용하지 않는다고 하더라도 메모리적인 부분에서 문제점이 없다. 다중 스레드일 경우에도 한번만 초기화되는 것을 보장한다.

static (타입)메소드는 인스턴스를 만들 필요없이 접근 가능하고..

static은 상속 후 재정의가 불가능하다는 점, 싱글톤은 상속이 가능하다는 점이 있는 것 같다.

그럼 static 변수, 메소드를 사용하지 않고 따로 싱글톤을 만들어서 쓰는 이유는 뭐지? 라는 생각이 정리하면서 들었다..

stackoverflow - Difference between static function and singleton class in swift

주요 목적이 임시 핸들 외에 변수에 데이터를 저장할 필요가 없는 논리적 계산/조작 또는 작업을 제공하는 것이라면 항상 정적 클래스를 선택해야 합니다. 예를 들어 여기에서 언급한 유틸리티 클래스입니다. 이미지 크기 조정, 파일 읽기, 데이터 구조 구문 분석과 같은 유틸리티 방법은 정적 방법을 사용하는 것이 가장 좋습니다.

클래스가 여러 변수에 중요한 정보를 저장해야 하는 경우 리소스 액세스 제한(예: 데이터베이스의 동시 트랜잭션 수 제한 또는 동시 네트워크 호출 수 제한), 자주 리소스를 할당 및 할당 해제(런타임에 메모리 관리)하는 경우 다음을 수행하는 것이 가장 좋습니다. Singleton 클래스 사용(예: db 구조에 액세스하기 위한 클래스, 네트워크 리소스 관리 등)

데이터(특히 중요한 데이터)를 저장하는 게 주 목적인 경우에는 싱글톤을 사용하고,
딱히 데이터를 저장할 필요없고 단순히 논리적 계산, 조작 또는 연산이 필요할 경우에는 정적 메소드와 정적 변수가 있는 클래스를 쓰는게 적절하다는 말 같다!

읽어보니 어느정도 이해가 된다. 정말 하나의 인스턴스가 있어야 하고 그것이 공유되어야하는 이유가 있는 경우에는 싱글톤을 사용하고, 그게 아닌 경우에는 static을 쓰면 될 것 같다.

profile
Don't think, just do 🎸

2개의 댓글

comment-user-thumbnail
2022년 10월 24일

케로로빵으로 바꿔주세요.

1개의 답글