초콜릿 공장에서 초콜릿을 끓여서 다른 공정으로 넘기는 놈(Chocolate Boiler)가 있다.
이것의 코드 예시는 다음과 같다.
class ChocolateBoiler {
private:
bool empty = true;
bool boiled = false;
public:
// 초콜릿이 보일러에 없으면 채움.
void fill();
// 보일러에 초콜릿이 꽉차고 다 끓이면 다른 공정에 넘김.
void drain();
// 보일러에 초콜릿이 꽉차고 끓지 않은 상태라면 끓임.
void boil();
// 초콜릿 보일러가 비었는지 확인.
bool isempty() { return empty; }
// 초콜릿 보일러 속 초콜릿이 끓었는지 확인.
bool isBoiled() { return boiled; }
};
초콜릿 보일러의 인스턴스를 반드시 1개만 생성하도록 하려면 어떻게 코드를 수정할 수 있을까?
class ChocolateBoiler {
private:
bool empty = true;
bool boiled = false;
static ChocolateBoiler* chocolateBoilerInstance;
ChocolateBoiler() {}
public:
static ChocolateBoiler* getInstance() {
if (chocolateBoilerInstance == nullptr) {
chocolateBoilerInstance = new ChocolateBoiler();
}
return chocolateBoilerInstance;
}
void fill();
void drain();
void boil();
bool isempty() { return empty; }
bool isBoiled() { return boiled; }
};
이 때는 copy 생성자와 대입 연산자를 삭제하여 객체의 복사를 방지한다.
class ChocolateBoilerObject {
private:
bool empty = true;
bool boiled = false;
ChocolateBoilerObject() {}
public:
static ChocolateBoilerObject& getInstance() {
static ChocolateBoilerObject instance;
return instance;
}
void fill();
void drain();
void boil();
bool isempty() { return empty; }
bool isBoiled() { return boiled; }
ChocolateBoilerObject(const ChocolateBoilerObject& obj) = delete;
ChocolateBoilerObject& operator =(const ChocolateBoilerObject&) = delete;
};
유의해서 봐야할 점은 3가지다.
static ChocolateBoilerObject& getInstance() {
static ChocolateBoilerObject instance;
return instance;
}
인스턴스를 static으로 로컬에서 선언하는 이유는 다음과 같다.
static으로 선언하면 해당 변수의 값은 변경될 수 없나요?
- 메모리 구조
| |
| |
--------- <- 다른 데이터
| |
| |
--------- <- 전역 변수 영역
| |
| | <- static ChocolateBoilerObject instance1;
--------- <- 스택 영역 (함수 호출 시 사용됨)
| |
| |
| |
--------- <- 힙 영역 (동적 메모리 할당 시 사용됨)
- 이렇게 전역 변수 영역에 instance가 할당되는데 이는 프로그램이 실행과 동시에 종료까지 유지되기 때문에 같은 타입의 다른 값을 할당할 수 없음.
int main() {
// 위 메모리 상의 instance1 반환.
ChocolateBoilerObject& obj1 = ChocolateBoilerObject::getInstance();
// 위 메모리 상의 instance1 반환.
ChocolateBoilerObject& obj2 = ChocolateBoilerObject::getInstance();
};
이렇게 다른 변수로 인스턴스를 반환받아도 같은 인스턴스를 반환하여 class의 인스턴스가 1개만 생성되도록 보장하는 것이다.(Singleton)
그리고 생성된 객체의 참조를 반환한다.
참조를 반환하는 이유
- 실제 객체를 반환하게 되면 객체의 복사본을 반환 됨 > 다른 메모리 location에 객체들이 복사된다. 이는 Singleton 원칙(class의 객체를 1개만 유지)에 위배하는 것이다.
잠시 참조(Reference)가 뭔가요?
- 참조는 pointer + 포인터에 접근으로 생각하면 된다.
즉, 객체의 메모리 주소 + 접근하는 것이다.
- 예를 들어 Chocolate& obj = Chocolate(); 라면 "초콜릿 객체가 생성된 주소를 반환받고 거기에 접근할게~) 이다.
- 예시 코드
class ChocolateBoilerObject { public: // ... static ChocolateBoilerObject getInstance() { // 객체를 반환 static ChocolateBoilerObject instance; return instance; } // ... }; int main() { ChocolateBoilerObject obj1 = ChocolateBoilerObject::getInstance(); ChocolateBoilerObject obj2 = ChocolateBoilerObject::getInstance(); // obj1과 obj2는 서로 다른 객체의 복사본. // 이는 Singleton 패턴의 목적에 위배됨. }
ChocolateBoilerObject(const ChocolateBoilerObject& obj) = delete;
ChocolateBoilerObject& operator =(const ChocolateBoilerObject&) = delete;
위 코드에서는 복사 생성자와 복사 할당 연산자를 삭제해줬다.
함수에서 객체를 반환하거나 인자로 객체를 전달할 때 복사가 일어날 때 호출되는 것이 Copy Constructor다.
예를 들어 value가 42인 객체를 obj2에 할당하려고 한다.
obj1 obj2
+-----------+ +-----------+
| value: 42 | ---복사 생성자---> | value: 42 |
+-----------+ +-----------+
그럼 obj1의 참조를 전달받아 해당 객체에 접근해 값을 obj2 객체에 복사한다.
그래서 싱글톤 패턴은 해당 클래스의 객체는 1개만 생성되어야 하고 그 객체를 복사가 되면 안된다.(그럼 그 클래스의 객체가 2개가 되어버림...)
따라서 다음과 같이 복사 생성자를 삭제해줬다.
ChocolateBoilerObject(const ChocolateBoilerObject& obj) = delete;
또한 복사 할당 연산자도 삭제해줬는데, 이유는 똑같다.
ChocolateBoilerObject& operator =(const ChocolateBoilerObject&) = delete;
int main() {
ChocolateBoilerObject& instance = ChocolateBoilerObject::getInstance();
ChocolateBoilerObject instance2;
// 복사 할당 연산자 호출
instance2 = instance;
// 컴파일 에러(복사 할당 연산사 삭제)
};
클래스의 인스턴스의 개수를 1개로 보장하고 그 인스턴스로의 전역 접근을 제공.
1) 인스턴스의 개수를 하나로 보장
2) 인스턴스의 전역적인 접근 제공
스레드 1과 2에서 동시에 다음 코드를 실행한다고 생각해보자.
static ChocolateBoiler* getInstance() {
if (chocolateBoilerInstance == nullptr) {
chocolateBoilerInstance = new ChocolateBoiler();
}
return chocolateBoilerInstance;
}
스레드 1과 2가 12:00:00에 동시에 if (chocolateBoilerInstance == nullptr)을 실행함.
"스레드1: chocolateBoilerInstance야 너 null이니?"
"chocolateBoilerInstance: 네!"
"스레드1: 어 그래? ㅇㅋ 객체 생성해줘"
"chocolateBoilerInstance: 새로운 객체1를 할당했어요""스레드2: chocolateBoilerInstance야 너 null이니?"
"chocolateBoilerInstance: 네!"
"스레드2: 어 그래? ㅇㅋ 객체 생성해줘"
"chocolateBoilerInstance: 새로운 객체2를 할당했어요"
이렇게 멀티쓰레드 환경에서 인스턴스 변수가 null일 때는 동시에 이를 실행하게 되면 한 클래스의 객체가 2개가 생성될 수도 있다는 문제가 있다.
이를 방지하기 위해 java에서는 synchronized 키워드를 사용해 동시 접속을 방지하여 해결한다.

음.. 근데 문제가 될 수 있는 객체가 초기화되기 전(null)만 그런데... getInstance 메서드를 호출할 때 마다 synchronized 키워드가 적용되니까 오히려 손해 아닌가..? 에서 문제의식을 갖게 된다.