이 글을 공부하기 위해 번역 및 정리한 글입니다.
어플리케이션 전체에서 하나의 전역 인스턴스를 공유하는 패턴
싱글톤은 한 번만 인스턴스화될 수 있고, 전역으로 접근할 수 있는 클래스입니다. 그렇게 만들어진 하나의 인스턴스는 어플리케이션 전역에서 공유될 수 있어서 어플리케이션의 전역 상태를 관리하기 좋습니다.
우선, ES2015 클래스를 사용해서 싱글톤이 어떻게 사용되는지 확인해봅시다. 다음 예시에서 Counter 클래스를 만들어봅시다.
다음과 같이 클래스를 만들면 우리는 여러 개의 Counter 인스턴스를 만들 수 있기 때문에 싱글톤 패턴의 기준에 맞지 않는 방식입니다.
let counter = 0;
class Counter {
getInstance() { // 인스턴스 반환하는 메소드
return this;
}
getCount() { // counter 변수 값 반환하는 메소드
return counter;
}
increment() { // counter 변수에 1 증가시키는 메소드
return ++counter;
}
decrement() { // counter 변수 1 감소시키는 메소드
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.getInstance() === counter2.getInstance()); // false
new 메소드를 두 번 호출하여 counter1과 counter2는 서로 다른 인스턴스를 만들었습니다. 이 두 개의 Counter 인스턴스 각각의 getInstance 메소드로 각 인스턴스를 받아 비교해본 결과 둘은 서로 다르다는 것을 알 수 있습니다. 즉, 위 방식은 하나의 클래스로 여러 개의 인스턴스를 만들 수 있기 때문에 싱글톤 패턴에 맞지 않는 상태입니다.
하나의 인스턴스만 생성되도록하는 한 가지 방법은 "instance"라는 변수를 생성하는 것입니다. Counter 클래스의 생성자 함수에서 새로운 인스턴스를 만들 때 "instance"에 새로운 인스턴스를 할당하는 것이 아닌 해당 인스턴스를 참조하는 값을 설정하면 됩니다. 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!
위와 같은 방식으로 하나의 클래스가 여러 개의 인스턴스로 만들어지는 일이 없을 것입니다.
지금부터는 Counter 인스턴스를 counter.js 파일 바깥으로 export 해봅시다! 그러기 위해서는 인스턴스를 freeze해야합니다. Object.freeze 메소드는 클래스를 사용하려는 코드가 싱글톤을 수정할 수 없도록 보장합니다. frozen된 인스턴스의 속성들은 추가되거나 수정될 수 없어서 의도치않게 싱글톤에 다른 값이 덮어쓰여지는 위험을 줄일 수 있습니다.
...
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;
이제 Counter를 어플리케이션이 구현해봅시다. 원 글에서는 vanilaJS로 예제를 구현했지만 이 글에서는 Vue3 + typescript로 구현하는 방법을 공유합니다. vue에서 싱글톤으로 상태 관리하기 위해서는 store나 composable을 만들어 사용하면 전역에서 하나의 인스턴스에 접근해 상태 관리를 할 수 있습니다. 다음은 composable로 싱글톤 Counter를 만드는 예제입니다.
composable
// composables/useCounter.ts
import { ref } from 'vue';
const counter = ref<number>(0);
export function useCounter() {
function increment() {
counter.value++;
}
function decrement() {
counter.value--;
}
return {
counter,
increment,
decrement,
}
}
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';
const { counter, increment, decrement } = useCounter();
</script>
<template>
<div>{{ counter }}</div>
<button @click="increment">+ 1</button>
<button @click="decrement">- 1</button>
</template>
여러 개의 파일에서 각각 useCounter 컴포저블을 import하고 counter 값에 접근 및 수정하면 동일한 counter 인스턴스가 변경됩니다. 각각 useCounter를 import해서 사용하는 A와 B라는 파일이 있다고 가정해봅시다. A와 B 파일에서 increment 함수를 실행시키면 useCounter 컴포저블 파일에 정의한 counter 전역 변수의 값이 증가합니다. 모든 파일에서 동일한 인스턴스에 접근하기 때문에 increment 함수를 실행한 위치는 중요하지 않습니다. 물론, 예제와 달리 useCounter 함수 내부에 counter 변수를 선언하면 useCounter 컴포저블을 사용한 컴포넌트마다 별도의 counter 인스턴스가 생성되고 개별적으로 접근 및 수정할 수 있습니다. 사실 컴포저블에서 전역 변수를 사용하는 것은 좋지 않다고.. 들었는데 왜 였는지는 까먹었습니다. 더 공부해서 수정해야겠습니다. 전역 변수를 사용하려면 store를 사용하는 편이 좋습니다. 저는 pinia를 사용하여 스토어를 만듭니다.
Tradeoffs
오직 하나의 인스턴스를 만들어 사용하는 방식은 잠재적으로 많은 메모리를 절약하게 됩니다. 각각의 새 인스턴스를 위해 메모리를 할당하는 대신, 어플리케이션 전역에서 참조되는 하나의 인스턴스에만 메모리를 할당하면 됩니다. 하지만, 싱글톤은 사실 anti-pattern으로 여겨지고 자바스크립트에서는 피해야 하는 패턴으로 여겨집니다..ㅎㅎ..
Java나 C++과 같은 많은 프로그래밍 언어에서는 Javascript처럼 바로 객체를 생성하는 것이 불가능합니다. 객체 지향 프로그래밍 언어에서는 객체를 만들기 위해서 우선 클래스를 생성해야 합니다. 생성된 객체는 클래스의 인스턴스 값을 가집니다.
그래서 위 예제에서 보여준 클래스 구현은 투머치..합니다. Javascript에서는 객체를 직접 생성할 수 있기 때문에 다음과 같이 간단히 일반적인 객체를 만들기만 하면 싱글톤 패턴이 됩니다.
let count = 0;
const counter = {
increment() {
return ++count;
},
decrement() {
return --count;
}
};
Object.freeze(counter);
export { counter };
싱글톤 패턴의 단점 1: Testing
싱글톤에 의존하는 코드를 테스트하는 것은 까다로울 수 있습니다. 매번 새로운 인스턴스를 생성할 수 없기 때문에, 모든 테스트는 이전 테스트의 전역 인스턴스 수정에 의존합니다. 이 경우에는 테스트의 순서가 중요하며, 작은 수정 하나가 전체 테스트 스위트의 실패로 이어질 수 있습니다. 테스트 후에는 테스트에서 가한 수정을 초기화하기 위해 전체 인스턴스를 재설정해야 합니다.
싱글톤 패턴의 단점 2: Dependency hiding
예를 들어 superConter 라는 컴포넌트에서 counter 싱글톤을 가져와 사용한다고 가정해봅시다. 이 경우 다른 파일에서 superCounter 컴포넌트를 불러와 사용하고 superCounter에 선언된 increment 메소드를 호출할 수 있습니다. 이 경우 사용자의 의도와 다르게 싱글톤 값이 수정될 수 있고 이는 예상과 다른 동작일 수 있으며 어플리케이션 전체에 해당 인스턴스를 사용하는 모든 부분에 영향을 미치게 됩니다. 즉, 의존성 파악이 어렵습니다.
import Counter from "./counter";
export default class SuperCounter {
constructor() {
this.count = 0;
}
increment() {
Counter.increment();
return (this.count += 100);
}
decrement() {
Counter.decrement();
return (this.count -= 100);
}
}
Global Behavior
싱글톤 인스턴스는 전체 앱에서 참조할 수 있어야 합니다. 전역 변수는 이름 그대로 전역에서 접근할 수 있고 같은 상태를 보여줘야 합니다.
하지만 전역 변수를 사용하는 것은 일반적으로 좋은 설계라고 할 수 없습니다. 전역 범위 오염은 실수로 전역 변수의 값을 덮어쓰게 되어 예상치 못한 동작을 유발할 수 있습니다.
ES2015에서는 전역 변수를 만드는 것은 흔하지 않다고 말합니다. 새롭게 등장한 let과 const 키워드는 block-scoped 변수 선언에 사용됩니다. 이 키워드를 사용하여 변수들을 block-scoped로 선언하게 하여 전역에서 예상치 못하게 값이 오염되는 상황을 방지하도록 했습니다. Javascript의 새로운 모듈 시스템은 전역 범위의 오염 없이 전역적으로 값이 접근할 수 있는 보다 쉬운 방법을 만들었습니다. 모듈로부터 값을 export하고 다른 파일에서 그 값들을 import할 수 있게 했습니다.
그러나 싱글톤의 일반적인 사용 사례는 애플리케이션 전반에 걸쳐 일종의 전역 상태를 가지는 것입니다. 코드베이스의 여러 부분이 동일한 가변 객체에 의존하면 예상치 못한 동작이 발생할 수 있습니다.
보통 코드의 특정 부분이 전역 상태 내 값을 수정하고 다른 부분들이 해당 데이터를 가져다 씁니다. 아직 데이터에 적절한 값이 입력되지 않은 상태에서 다른 곳에서 사용되면 안되기 때문에 이 경우에는 실행 순서가 중요합니다. 전역 상태를 사용할 때 데이터 흐름을 이해하는 것은 애플리케이션이 커질수록 매우 복잡해지며, 수십 개의 구성 요소가 서로에게 의존합니다.
State management in React
React에서는 싱글톤 패턴 대신 Redux나 Context와 같은 상태 관리 툴을 사용하여 전역 상태를 관리합니다. 상태 관리 툴들과 싱글톤 패턴은 전역에서 접근 가능하다는 점에서 비슷하지만 상태 관리 툴들은 readonly 상태만 제공한다는 점이 다릅니다. Redux를 사용할 대 오직 reducer 함수만이 상태를 변경할 수 있고 컴포넌트는 오직 dispatcher를 통해서만 action을 보낼 수 있습니다.
Vue에 대한 설명을 첨가하자면 vue에서는 pinia나 vuex로 상태 관리를 하는데 이 역시 readonly 상태만 제공합니다. pinia로 예를 들면 defineStore를 통해 전역에서 접근 가능한 값을 선언하고 이 값들은 오직 actions내에서 선언한 함수들로만 변경이 가능합니다.
이 툴들을 사용하여 전역 상태를 사용하는 것의 단점들이 모두 사라지는 것은 아니지만 적어도 컴포넌트가 상태를 직접 변경할 수 없기 때문에 의도한대로 전역 상태를 관리하는데에는 도움이 됩니다.
References
- https://codesandbox.io/s/vue-singleton-component-5lcz6?file=/src/components/Singleton.js
'웹 개발 > Web Development' 카테고리의 다른 글
Biome) zero-dependency tool chain | Formatter, Linter (0) | 2024.02.03 |
---|---|
디자인 패턴) Proxy Pattern (0) | 2023.07.07 |
테스트툴) Playwright 란 (0) | 2023.03.04 |
Vuepress란 무엇인가 (0) | 2023.02.06 |
/etc/hosts 로컬에서 ip 주소 호스트네임 등록해 두고 사용하기 (0) | 2021.06.16 |