디자인 패턴
이 페이지는 제안된 디자인 패턴과 반패턴(anti-patterns)에 대해 다룹니다.
참고:
디자인 패턴을 이 문서에 추가할 때는 반드시 해결하는 문제를 명확히 설명하세요.
디자인 반패턴을 추가할 때는 예방하는 문제를 명확히 설명하세요.
패턴
다음 디자인 패턴은 일반적인 문제를 해결하기 위한 제안된 접근 방식입니다. 특정 패턴이 귀하의 상황에 맞는지 평가할 때 신중하게 판단하세요. 패턴이라고 해서 반드시 귀하의 문제에 적합하다는 의미는 아닙니다.
반패턴
반패턴은 처음에는 좋은 접근 방법처럼 보일 수 있지만, 이들이 가져오는 해악이 이익보다 더 많다는 것이 입증되었습니다. 일반적으로 이러한 것들은 피하는 것이 좋습니다.
GitLab 코드베이스 전반에 걸쳐 이러한 반패턴의 역사적인 사용이 존재할 수 있습니다. 신중하게
이러한 레거시 패턴 중 하나를 사용하는 코드를 수정할 때 리팩토링 여부를 결정하세요.
참고:
신규 기능의 경우 반패턴이 반드시 금지되는 것은 아니지만, 다른 접근 방식을 찾는 것이 강력히 권장됩니다.
공유 전역 객체
공유 전역 객체는 어디에서나 접근할 수 있는 인스턴스이며, 따라서 명확한 소유자가 없습니다.
다음은 Vuex Store에 적용된 이 패턴의 예입니다:
const createStore = () => new Vuex.Store({
actions,
state,
mutations
});
// 이 모듈에 대한 모든 참조가 동일한 단일 인스턴스의 저장소를 사용하도록 강제하고 있습니다.
// 또한 import 시점에 저장소를 생성하며, 자동으로 이를 폐기할 수 있는 것이 없습니다.
//
// 대안으로, `createStore`를 내보내고 클라이언트가 저장소의 생명주기와 인스턴스를 관리하도록 해야 합니다.
export default createStore();
공유 전역 객체가 발생시키는 문제는 무엇입니까?
공유 전역 객체는 어디에서나 접근할 수 있기 때문에 편리합니다. 그러나 편리함이 항상 그 무거운 대가를 초월하지는 않습니다:
- 소유권 없음. 이러한 객체에 대한 명확한 소유자가 없으므로 비결정적이고 영구적인 생명주기를 가집니다. 이는 특히 테스트에 문제가 될 수 있습니다.
- 접근 제어 없음. 공유 전역 객체가 일부 상태를 관리할 때 이는 이 객체에 대한 접근 제어가 없기 때문에 매우 버그가 많고 어려운 결합 상황을 초래할 수 있습니다.
- 순환 참조 가능성. 공유 전역 객체는 하위 모듈이 자신을 참조하는 모듈을 참조할 수 있기 때문에 순환 참조 상황을 생성할 수 있습니다. (예시로 이 MR을 참조하세요).
이 패턴이 문제가 되는 것으로 확인된 몇 가지 역사적 예시는 다음과 같습니다:
공유 전역 객체 패턴이 실제로 적절하게 사용될 수 있는 경우는 언제입니까?
공유 전역 객체는 무언가를 전역적으로 접근 가능하게 만드는 문제를 해결합니다. 이 패턴은 다음과 같은 경우에 적절할 수 있습니다:
- 책임이 진정으로 전역적이며 애플리케이션 전반에서 참조되어야 할 때
(예: 애플리케이션 전체에서 사용하는 이벤트 버스).
이러한 시나리오에서도 공유 전역 객체 패턴을 피하는 것이 좋습니다. 사이드 이펙트는 악명 높게 다루기 어려울 수 있습니다.
참고 문헌
자세한 내용은 C2 위키의 전역 변수는 나쁩니다(Global Variables Are Bad)를 참조하세요.
싱글턴
고전적인 싱글턴 패턴은 오직 하나의 인스턴스만 존재하도록 보장하는 접근 방식입니다.
이 패턴의 예는 다음과 같습니다:
class MyThing {
constructor() {
// ...
}
// ...
}
MyThing.instance = null;
export const getThingInstance = () => {
if (MyThing.instance) {
return MyThing.instance;
}
const instance = new MyThing();
MyThing.instance = instance;
return instance;
};
싱글턴이 일으키는 문제는 무엇인가요?
오직 하나의 인스턴스만 존재해야 한다는 것은 큰 가정입니다. 대개 싱글턴은 잘못 이용되며, 그것과 참조하는 모듈 간에 매우 강한 결합을 초래합니다.
이 패턴이 문제를 일으킨 몇 가지 역사적 예는 다음과 같습니다:
이곳은 싱글턴이 자주 발생시키는 몇 가지 폐해입니다:
-
비결정론적 테스트. 싱글턴은 비결정론적 테스트를 장려합니다. 왜냐하면 단일 인스턴스가 개별 테스트에 걸쳐 공유되어 한 테스트의 상태가 다른 테스트로 누수되는 경우가 많기 때문입니다.
-
높은 결합도. 내부적으로 싱글턴 클래스의 클라이언트는 모두 객체의 특정 단일 인스턴스를 공유합니다. 이는 이 패턴이 공유 전역 객체의 문제를 모두 상속받음을 의미하며, 소유권의 명확한 정의와 접근 제어가 없습니다. 이로 인해 버그가 발생하기 쉽고 복잡한 상황을 초래할 수 있습니다.
-
전염성. 싱글턴은 전염성이 있습니다. 특히 상태를 관리하는 경우 그렇습니다. Web IDE에서 사용되는 구성 요소 RepoEditor를 고려해보세요. 이 구성 요소는 상태를 관리하는 싱글턴 Editor와 인터페이스를 수행합니다. Editor 클래스의 싱글턴 특성으로 인해, 구성 요소
RepoEditor
도 이제 싱글턴이어야 합니다. 이 구성 요소의 여러 인스턴스는Editor
의 인스턴스를 진정으로 소유하는 사람이 없기 때문에 프로덕션 문제를 일으킬 것입니다.
왜 싱글턴 패턴이 Java와 같은 다른 언어에서 인기가 있나요?
이는 Java와 같은 언어의 제한 사항 때문입니다. 모든 것이 클래스에 감싸져야 하기 때문입니다. JavaScript에서는 여러 문제를 유틸리티 함수를 내보내는 모듈로 해결할 수 있는 객체 및 함수 리터럴과 같은 것이 있습니다.
싱글턴 패턴이 실제로 적절할 수 있는 경우는 언제인가요?**
싱글턴은 단 하나의 인스턴스만 존재하도록 강제하는 문제를 해결합니다. 다음과 같은 드문 경우에 싱글턴이 적절할 수 있습니다:
- 반드시 단 하나의 인스턴스만 있어야 하는 자원을 관리해야 하는 경우(즉, 하드웨어 제한).
- 실제 교차 문제가 존재하고, 싱글턴이 가장 간단한 API를 제공하는 경우.
이러한 시나리오에서도 싱글턴 패턴을 피하는 것이 좋습니다.
Singleton 패턴의 대안은 무엇인가요?
유틸리티 함수
상태를 관리할 필요가 없을 때, 우리는 클래스 인스턴화와 관련 없이 모듈에서 유틸리티 함수를 내보낼 수 있습니다.
// 나쁨 - Singleton
export class ThingUtils {
static create() {
if(this.instance) {
return this.instance;
}
this.instance = new ThingUtils();
return this.instance;
}
bar() { /* ... */ }
fuzzify(id) { /* ... */ }
}
// 좋음 - 유틸리티 함수
export const bar = () => { /* ... */ };
export const fuzzify = (id) => { /* ... */ };
의존성 주입
의존성 주입은 모듈의 의존성을 모듈 외부에서 주입되도록 선언하여 결합을 깨는 접근 방식입니다 (예: 생성자 매개변수, 진정한 의존성 주입 프레임워크, Vue의 provide/inject
등).
// 나쁨 - Singleton에 결합된 Vue 컴포넌트
export default {
created() {
this.mediator = MyFooMediator.getInstance();
},
};
// 좋음 - 의존성을 선언하는 Vue 컴포넌트
export default {
inject: ['mediator']
};
// 나쁨 - 싱글톤의 라이프사이클이 어디에 있는지 확신이 없으므로 여기서 초기화합니다.
export class Foo {
constructor() {
Bar.getInstance().init();
}
stuff() {
return Bar.getInstance().doStuff();
}
}
// 좋음 - 이 의존성을 생성자 인수로 받읍시다.
// 라이프사이클을 관리하는 것도 우리의 책임이 아닙니다.
export class Foo {
constructor(bar) {
this.bar = bar;
}
stuff() {
return this.bar.doStuff();
}
}
이 예제에서 mediator
의 라이프사이클과 구현 세부 사항은 모두 컴포넌트 외부에서 관리됩니다 (대부분 페이지 진입점일 가능성이 높습니다).