singleton이란 강의를 들으면서 간단해 보이는 클래스인 것 같은데 생각하고 고려해야 할 부분이 정말 많다는걸 알게 되었다.
특히 객체지향을 이해함에 있어 한줄 한줄 코드존재의이유를 생각하다보니 객체의 생명주기 관리와 캡슐화, 그리고 ‘유효한 상태’라는 oo의 원칙, 지향적(?)에 맞진 않는지, 한편으론 실용적 관점에서 이 파괴가 어떤 문제점을 일으킬까? 등 고민들이 이어졌다.
점점 배움의 후반부엔 싱글톤의 사용성 여부 (객제지향적이지 않다라는 의견) 고민과 이에 대한 대안과 함께 실무에서는 어떤 관점으로 봐야할지에 대해 나름 생각한 부분을 들여다 보려고 한다.
먼저 싱글턴의 코드가 왜 이렇게 구조화 될 수 밖에 없는지를 이해해보자.
싱글턴 패턴은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로 전역 접근을 제공한다는 목적을 달성한다.
아래 코드는 위의 설계를 그대로 구현한 것이다.
class Singleton {
private static _instance: Singleton | null = null;
private constructor() {}
static getInstance() {
if (!this._instance) {
this._instance = new Singleton();
}
return this._instance;
}
}
export default Singleton;
TypeScript
복사
어떻게 하면 한 클래스에 인스턴스를 2개 이상 만들지 않게하지? 를 자문하면서 코드를 작성하면 소스코드에서 뜻하는바가 무엇인지 이해가 더 잘된다.
2번 이상 인스턴스를 만들 수 없다는걸 어떻게 보장할 수 있을까? public 이 아닌 static private멤버 변수에 instance를 할당해 해당 클래스 내에서만 접근할 수 있도록 만든다.
그리고 객체의 인스턴스를 외부에서 만들지 못하게 하는건 어떻게 가능할까?
private constructor() {} 를 사용하면 타입스크립트에서 인스턴스를 생성하려고할 때 에러를 내줄 수 있다. 그렇게 되면 어쨌거나 외부에서 인스턴스를 한번은 생성해야 하는 접근이 필요한데 이를 static method를 통해 외부에서 접근 할 수 있게한다.
그런데 타입스크립트,자바스크립트 진영에서는 비슷하게 객체가 있다. 이점에서 왜 싱글턴을 사용해야 하는지 좀 더 면밀하게 생각해봐야 한다.
static getInstance() {
this._instance = new Singleton();
}
TypeScript
복사
여기에서 좀 더 나아가 같은 싱글턴 패턴이지만 getInstance를 다음과 같이 분리한 코드를 분리했다.
class GraphicsResourceManager {
private static _instance: GraphicsResourceManager | null = null;
private constructor() {}
static createInstance() {
this._instance = new GraphicsResourceManager();
}
static getInstance() {
// 싱글턴 인스턴스가 null일 경우를 대비해 assert추가
if (!this._instance) {
//런타임 중에 , createInstance가 호출되었다는걸 가정
throw new Error('GraphicsResourceManager is not initialized');
}
return this._instance;
}
}
export default GraphicsResourceManager;
TypeScript
복사
객체지향 원칙 중 하나인 항상 유효한 상태는
객체가 생성된 직후부터 일관된 상태를 가져야 한다
getInstance()는 항상 createInstance()가 먼저 호출된다는 전제를 깔고 있다면, 이 코드는 과연 ‘객체가 언제나 유효한 상태를 유지해야 한다’는 객체지향 원칙에 맞는 걸까?
유효한 상태
여기서 말하는 언제나 유효한 상태를 유지하는 설계는 객체가 생성된 순간부터 파괴될 때까지 어떤 public메서드를 호출하더라도 잘 정의된 동작만 수행하도록 만드는 설계원칙을 말한다.
좀 더 구체적으로는 불완전한 상태 금지라는 의미가 내포될 수 있다. 객체가 만들어진 이후 초기화가 아직 끝나지 않아서 호출하면 안되는 메서드 같은 것이 없어야 한다. 예를 들어 getInstance를 호출하기 전에 반드시 createInstance를 호출해야한다면 이 객체는 아직 불완전한 상태(유효한상태)를 허용하고 있는 셈이다.
정상적인 사용흐름에서 throw가 발생하거나, 특정 메서드를 호출하면 런타임 오류가 터질 수 있다면 항상 유효하다고 보기 어렵다. 사용자가 불변식을 깨지 않도록 안전하게 설계해야 한다.
클래스 외부에서 먼저 ~~를 호출하고 그다음에 ~~를 해야 한다처럼 추가 규칙이 강요하지 않아야 한다. 객체 스스로 상태를 관리하거나 생성자/팩토리에서 모든 필수정보를 받아 유효한 상태만 허용하게 한다.
언제나 유효한 상태를 유지하면 각 메서드가 전제하는 조건이 명확해져 테스트 작성이 쉬워지고 유지보수중에 불변식이 깨질 위험도 줄어든다.
다시 말해 객체 스스로 자신의 상태를 책임지고 외부 호출자가 이를 관리할 필요가 없어야 한다.
이 관점에서 보면 싱글톤 구현은 OO원칙중 하나인 자기자신이 유효한 상태를 유지해야 한다를 위반하고 있다
결국 이 원칙은 객체가 사용자의 실수를 허용하지 않도록 설계하는것에 가깝다. 싱글턴이라면 모듈 초기화 시점에 바로 인스턴스를 만들어 두거나 getInstance가 내부적으로 생성까지 책임지는 방식으로 이런 철학을 지킬 수 있을것이다.
사람들이 따라갈수있는 수칙, 프로세스, 코딩 표준으로 해결해야 할 문제를 괜히 코드에서 해결하겠다고, 프로그램 구조에서 해결하겠다고 복잡하게 만들어도 실제 실수는 줄어들지 않을 수 있다.
모든 것이 반드시 개체여야 하는가? 코드에는 작성자의 의도가 담기기 마련이므로, 내가 작성한 코드가 정말 객체지향적인가 하는 고민까지 하게 된다. 그러나 이번 일을 통해 깨달은 점은, 모든 것이 개체여야만 하는 실용적인 이유는 없다는 것이다. 객체지향의 원리 속에서도, 상황에 따라 필요한 도구를 적절히 활용하는 것이 중요하며, 싱글톤 역시 그러한 도구 중 하나로 이해하고 받아들이는 것이 바람직하다는 점을 알게 되었다.
