
싱글톤 패턴
싱글톤 패턴이란?
- 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴을 말합니다.
- 1회에 한하여 인스턴스화가 가능하며 전역에서 접근 가능한 클래스를 말합니다.
- 싱글톤 인스턴스는 앱 전역에서 공유되기 때문에 앱 전역 상태를 관리하기에 적합합니다.
- 보통 데이터 베이스 연결 모듈에 많이 사용합니다.
- node.js mongoose의 데이터베이스 연결하는 connect() 함수는 싱글톤 인스턴스를 반환하는 함수입니다.
let counter = 0; class Counter { getInstance() { return this; } getCount() { return counter; } increment() { return ++counter; } decrement() { return --counter; } }
`getInstance` 메서드는 인스턴스의 값 자체를 반환합니다.
`getCount` 메서드는 counter 변수의 현재 값을 반환합니다.
`increment` 메서드는 counter 변수의 값을 1씩 증가시킵니다.
`decrement` 메서드는 couner 변수의 값을 1씩 감소시킵니다.
하지만 이 클래스 문법은 싱글톤의 기준을 충족하지 않습니다. 그 이유는 싱글톤은 인스턴스화가 한 번만 되어야하는데Counter 클래스를 사용해서 여러개의 인스턴스를 만들 수 있기 때문입니다.
let counter = 0; class Counter { getInstance() { return this; } getCount() { return counter; } increment() { return ++counter; } decrement() { return --counter; } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); //false
위의 코드를 살펴보면 new 키워드를 활용한 생성자 함수를 두 번 호출하여 인스턴스를 만들었기 때문에 counter1과 counter2에 각각 다른 인스턴스가 할당됩니다. 즉, 두 인스턴스는 서로 다른 인스턴스가 됩니다. getInstance를 활용하여 반환한 값은 counter1, counter2가 다른 인스턴스를 참조하고 있기때문에 다른 값을 반환한다고 말할 수 있습니다.(값 자체를 보면 같아 보이지만 엄격히 말하면 별개의 인스턴스 입니다.)
이를 싱글톤 패턴의 하나의 인스턴스만을 반환하는 Counter 클래스를 만들어 보겠습니다.
하나의 인스턴스를 만드는 확실한 방법은 instance 라는 변수를 선언하여 사용하는 것입니다. Counter 클래스에 생성자 함수(constructor) 안에서 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의 인스턴스를 내보내기(export) 해보겠습니다. 그 전에, 해당 인스턴스를 freeze 해야합니다. Object.freeze 메서드는 객체를 사용하는 쪽에서 해당 객체를 수정할 수 없도록 하는 역할을 합니다. freeze 된 인스턴스의 프로퍼티는 변경되거나 추가되지 않습니다. 그리고 이를 통해 의도치 않게 발생하는 싱글톤 인스턴스의 값이 덮어씌어지는 등의 위험을 줄입니다. 코드로 작성해보겠습니다.
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 singleTonCounter = Object.freeze(new Counter()); export default singleTonCounter;
싱글톤 패턴을 왜 사용하는가?
싱글톤 패턴의 장점
인스턴스를 한 번만 만들수 있도록 제한하면(싱글톤 패턴) 많은 메모리 공간을 절약할 수 있습니다. 인스턴스를 만들때마다 매번 메모리 공간을 차지하는 것을 방지하고 하나의 인스턴스에 대한 메모리 공간만을 사용하기 때문입니다.
싱글톤 패턴의 단점
객체 리터럴 사용하기
하지만 싱글톤 패턴은 안티패턴(anti-pattern)이나 자바스크립트에서는 사용을 피해야하는 것으로 여겨집니다. C++, Java와 같은 많은 프로그래밍 언어에서 JavaScript 에서 처럼 객체를 직접 만드는 것은 불가능 합니다. 객체지향 프로그래밍 언어들은 클래스를 만드는 것을 통해 객체를 생성합니다. 위에서 JavaScript 를 통해 만든 예제와 같이 클래스를 통해 만들어진 객체는 클래스의 인스턴스가 됩니다.
JavaScript 에서는 직접적으로 객체를 만들 수 있기 때문에 위에서 구현한 싱글톤 패턴 코드는 약간 오버 엔지니어링이라 할 수 있습니다. 객체를 직접 만드는 방식(객체 리터럴)을 통해 다른 프로그래밍 언어에서와 같은 결과를 얻을 수 있습니다.
TDD 테스트 주도 개발 방식에서 문제 & 테스팅의 문제
싱글톤 패턴으로 작성된 코드를 테스트 하는 것이 까다롭습니다. 테스트 실행에 순서가 생기게 된다면 작은 수정 사항이 전체 테스트 실패로 이어질 수 있기 때문입니다. 또한 테스트 진행 후에 테스트를 위해 변경하였던 인스턴스 전체를 초기화해주어야 하는 번거로움이 생기기도 합니다. 이는 특히 테스트 주도 개발 방식에서 문제가 될 수 있는데 TDD는 주로 단위 테스트를 진행하는데, 단위 테스트의 경우 테스트가 서로 독립적이여야하고 테스트를 어떤 순서로든 실행할 수 있어야 합니다. 하지만 싱글톤 패턴의 경우 미리 생성된 하나의 인스턴스를 바탕으로 구현하는 패턴이기 때문에 매 테스트 마다 독립적인 인스턴스를 만들기 어렵고 실행 순서가 보장되지 않는 경우에는 해당 인스턴스의 값을 보장할 수 없습니다.
명확하지 않은 의존성(내용 추가)
모듈간의 결합을 강하게 만들게 됩니다. 하지만 이를 의존성 주입을 통해 해결할 수 있습니다. 인스턴스를 매번 생성할 수 없기 때문에 모든 테스트는 이전 테스트의 전역 인스턴스를 수정할 수 밖에 없습니다.
전역에서의 동작
싱글톤 인스턴스는 앱 전체에서 참조할 수 있어야 합니다. 싱글톤 인스턴스는 전역 변수와 같은 행동양식을 보이는데 전역 변수는 전역 스코프(global scope)를 갖기 때문에 앱 어디서든 참조할 수 있습니다. 하지만 전역 변수를 갖는 것은 일반적으로 좋지 않은 설계 방식이라 할 수 있는데 해당 변수의 값이 예상치 못한 곳에서 변경될 수 있고 에러를 발생시킬 수 있기 때문입니다.
보통 전역 스코프에 존재하는 변수들의 값을 변경한 후에 다른 곳에 해당 변수들을 사용하게 됩니다. 이때 실행 순서가 중요해집니다. 이 경우 변수들의 값을 사용한 후에 변수의 값을 변경하는 것은 의도한 결과가 아니기 때문입니다. 앱의 규모가 점점 커지면서 위와 같이 데이터(변수의 값)의 흐름을 파악하는 것이 어렵게 됩니다.
React 에서의 상태관리
React 에서는 싱글톤 패턴을 사용하기 보다는 주로 전역 상태를 상태 관리 툴인 redux 또는 React Context API 를 통해 관리합니다. 전역 상태를 관리하는 것이 싱글톤에서와 비슷하게 보이지만 읽기 전용(read-only) 상태를 제공합니다. 반면 싱글톤은 인스턴스의 값을 직접 수정할 수 있게 됩니다. Redux 를 사용하는 경우, 컴포넌트가 디스패치(dispatch)를 통해 액션(action)을 보낸 후에 순수함수의 형태인 리듀서(reducer)를 통해서만 상태를 변경할 수 있기 때문에 싱글톤과 비교하여 상태를 변경하는 통로가 제한적이므로 안전하다고 할 수 있습니다.
이러한 전역 상태 관리 툴을 사용하는 것으로 전역 상태를 갖는 것의 단점들이 완전히 해결되는 것은 아니지만 컴포넌트 자체가 직접적으로 상태를 변경하지 않기 때문에 전역 상태가 의도한 대로 변경된다는 사실을 확신할 수 있습니다.
참고
https://www.freecodecamp.org/news/singleton-design-pattern-with-javascript/#what-is-a-design-pattern
'ETC' 카테고리의 다른 글
NHN FORWARD 2022 Conference 후기 (0) | 2022.12.05 |
---|