어떻게 프로젝트를 시작하게 되었고, 진행하면서 느낀 개발자와 디자이너의 생생한 스토리를 직접 확인해보세요!
Development
Singleton 패턴
#Front-End
Yujin Son
2024. 11. 7.
Singleton 패턴
Singleton 패턴
1. 사용이유
Singleton 은 1회에 한하여 인스턴스화가 가능하며 전역에서 접근 가능한 클래스를 지칭합니다.
만들어진 Singleton 인스턴스는 앱 전역에서 공유되기 때문에 앱의 전역 상태를 관리하게 적합합니다.
2. ES2015의 클래스로 작성된 Singleton
여러 메서드를 가지는 Counter 클래스를 예제로 들어보겠습니다.
Singleton 패턴의 조건을 만족하지 않는 경우
Singleton은 인스턴스를 단 한 번만 만들 수 있어야 하기 때문입니다. 아래의 코드에서는 Counter 인스턴스를 여러 번 만들 수 있습니다.
let counter = 0
class Counter {
//인스턴스 자체를 반환
getInstance() {
return this
}
//counter변수 반환
getCount() {
return counter
}
//counter변수 1 증가
increment() {
return ++counter
}
//counter변수 1 감소
decrement() {
return --counter
}
}
new 메서드를 두번 호출하여 counter1 과 counter2 를 각각 별개의 인스턴스로 가르키도록 한 경우 입니다.
counter1 과 counter2은 동일한 인스턴스가 아니기 때문에 각 인스턴스의 getInstance 메서드를 호출했을때 반환되는 레퍼런스는 같지 않습니다.
JS에서는 new 키워드를 사용해 클래스를 생성하면 새로운 객체 인스턴스가 만들어지기 때문입니다.
이제는 Counter 클래스가 한번만 만들어질 수 있도록 코드를 수정해 보겠습니다.
그 방법 중 하나는 instance 라는 변수를 만드는 것입니다.
let instance
let counter = 0
class Counter {
constructor() {
if (instance) {
throw new Error('You can only create one instance!')
}
instance = this
}
getInstance() {
return this
}
getCount() {
return counter
}
increment() {
return ++counter
}
decrement() {
return --counter
}
}
const counter1 = new Counter()
const counter2 = new Counter()
// Error: You can only create one instance!
let instance
let counter = 0
class Counter {
constructor() {
if (instance) {
throw new Error('You can only create one instance!')
}
instance = this
}
getInstance() {
return this
}
getCount() {
return counter
}
increment() {
return ++counter
}
decrement() {
return --counter
}
}
// 인스턴스를 freeze 하여 싱글톤 인스턴스의 프로퍼티를 덮어쓰는 실수를 예방할 수 있습니다.
*`Object.freeze`메서드는 객체를 사용하는 쪽에서 직접 객체를 수정할 수 없도록 해 줍니다.
const singletonCounter = Object.freeze(new Counter())
export default singletonCounter
blueButton.js 와 redButton.js 둘 다 counter.js 에서 동일한 Singleton 인스턴스를 import한 것입니다.
인스턴스를 매번 생성할 수 없기 때문에 모든 테스트들은 이전 테스트에서 만들어진 전역 인스턴스를 수정할 수 밖에 없기 때문입니다.
스트들이 실행에 순서가 생기게 되면 작은 수정사항이 전체 테스트의 실패로 이어질 수 있고 하나의 테스트가 끝나면 인스턴스의 변경사항들을 초기화 해 주어야 합니다.
import Counter from "../src/counterTest";
test("incrementing 1 time should be 1", () => {
Counter.increment();
expect(Counter.getCount()).toBe(1);
});
test("incrementing 3 extra times should be 4", () => {
Counter.increment();
Counter.increment();
Counter.increment();
expect(Counter.getCount()).toBe(4);
});
test("decrementing 1 times should be 3", () => {
Counter.decrement();
expect(Counter.getCount()).toBe(3);
});
//index.js
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import Counter from "./counter";
import SuperCounter from "./superCounter";
const counter = new SuperCounter();
counter.increment();
counter.increment();
counter.increment();
console.log("Counter in counter.js: ", Counter.getCount());
const rootElement = document.getElementById("root");
ReactDOM.render(<div className="App">Check the console!</div>, rootElement);
superCouter.js 처럼 다른 모듈로부터 import될 때 Singleton인지 아닌지 분명하지 않습니다.
예제 코드처럼 SuperCounter 를 import 하여 인스턴스를 만들고 메서드를 호출했지만 싱글톤 객체의 값을 수정하게 되었습니다.
여러 Singleton 인스턴스들이 앱에서 공유될 때 이처럼 직접 수정하게 될 수 있고 예외로 이어질 수 있습니다.
전역 동작에서 문제가 생길 수 있습니다.
ES2015에선 전역변수를 생성하는게 일반적이지 않습니다. 새로 만들어진 let, const 키워드들은 변수를 블록 스코프 내에 선언하게 하여, 실수로 전역에 변수를 선언하는것을 예방해 주기 때문입니다. 또 새로운 module 시스템은 export 구문과 import 구분으로 전역 객체를 수정하지 않고 모듈 내에서 전역으로 쓸 수 있는 변수를 만들게 해 줍니다.
그러나 Singleton 패턴은 일반적으로 앱에 전역 상태를 위해 사용하기 때문에, 코드의 여러 부분에서 수정가능한 하나의 객체를 직접 접근하도록 설계하면 예외가 발생하기 쉬워집니다.
따라서 앱의 규모가 커지고 전역 상태를 참조하는 컴포넌트가 많아지며 서로를 참조하는 상황에서는 데이터의 흐름을 파악하기 어려워집니다.
React의 상태관리
리액트에선 전역 상태 관리를 위해 싱글톤 대신에 Redux나 React Context를 자주 사용합니다.
Singleton과 유사해 보이지만 Singleton은 인스턴스의 값을 직접 수정할 수 있는 반면에, 언급한 도구들은 읽기 전용 상태를 제공합니다.
Redux를 사용할 땐 오직 컴포넌트에서 디스패쳐를 통해 넘긴 액션에 대해 실행된 순수함수 리듀서를 통해서만 상태를 업데이트할 수 있습니다.
위에서 언급한 전역 상태에 대한 단점이 모두 사라지는 것은 아니지만. 컴포넌트가 직접 상태를 업데이트하게 두는 것은 아니고 개발자가 의도한대로만 수정되도록 하고 있는 것이다.