디자인 패턴
이 페이지에서는 제안된 디자인 패턴 및 안티 패턴에 대해 다룹니다.
참고: 이 문서에 디자인 패턴을 추가할 때 해결하는 문제를 명확히 명시해야 합니다. 디자인 안티 패턴을 추가할 때는 막는 문제를 명확하게 명시해야 합니다.
패턴
다음은 일반적인 문제를 해결하는 데 사용하는 제안된 디자인 패턴입니다. 특정 패턴이라고 해서 항상 효과적인 것은 아니므로 특정 상황에서 그 패턴이 적합한지 신중하게 평가해야 합니다.
안티 패턴
안티 패턴은 처음에는 좋은 방법으로 보일 수 있지만, 이러한 패턴이 더 많은 문제를 유발한다는 것이 밝혀졌습니다. 그러므로 이러한 패턴은 일반적으로 피해야 합니다.
GitLab 코드베이스 전반에는 이러한 안티 패턴을 사용한 역사적 사례가 있을 수 있습니다. 따라서 해당 레거시 패턴을 사용하는 코드에 손을 대거나 리팩토링할 때 신중하게 판단해야 합니다.
참고: 새로운 기능에 대해서 안티 패턴이 반드시 금지되는 것은 아니지만, 다른 접근 방법을 강력히 권장합니다.
공유 전역 객체
공유 전역 객체는 어디서든 액세스할 수 있는 인스턴스로, 명확한 소유자가 없는 특성을 가지고 있습니다.
다음은 Vuex Store에 이 패턴이 적용된 예시입니다:
const createStore = () => new Vuex.Store({
actions,
state,
mutations
});
// 이 모듈에 대한 모든 참조가 동일한 스토어 인스턴스를 사용하도록 강제하는 것에 주목하세요.
// 또한 스토어를 가져와서 자동으로 폐기할 수 있는 방법이 없습니다.
//
// 대안으로 `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와 상호작용하여 Monaco를 사용하는 데 일부 상태를 관리합니다. Editor 클래스의 싱글톤 성격 때문에 RepoEditor 컴포넌트도 싱글톤이어야 합니다. 이 컴포넌트의 여러 인스턴스는 실제로 Editor의 인스턴스를 소유하는 사람이 없기 때문에 프로덕션에 문제가 발생할 수 있습니다.
왜 Java와 같은 다른 언어에서 싱글톤 패턴이 인기가 있을까요?
이것은 Java와 같은 언어의 제한 때문입니다. Java에서는 모든 것을 클래스로 래핑해야 하기 때문에 그렇습니다. JavaScript에서는 객체 및 함수 리터럴과 같은 요소를 가지고 모듈에 유틸리티 함수를 내보내는 방식으로 많은 문제를 해결할 수 있습니다.
싱글톤 패턴이 실제로 적절한 경우는 언제인가요?
싱글톤은 한 가지 인스턴스만 존재해야 하는 것을 강제하는 문제를 해결합니다. 다음과 같은 드문 경우에 싱글톤이 적합할 수 있습니다.
- 하드웨어 제한과 같이 반드시 1개의 인스턴스만 가져야 하는 리소스를 관리해야 하는 경우
- 실제 cross-cutting concern(예: 로깅)이 있고, 싱글톤이 가장 간단한 API를 제공하는 경우
심지어 이러한 시나리오에서도 싱글톤 패턴 사용을 피하는 것이 좋습니다.
싱글톤 패턴에 대한 대안은 무엇인가요?
유틸리티 함수
상태를 관리할 필요가 없을 때 모듈에서 유틸리티 함수를 내보낼 수 있습니다.
// bad - Singleton
export class ThingUtils {
static create() {
if(this.instance) {
return this.instance;
}
this.instance = new ThingUtils();
return this.instance;
}
bar() { /* ... */ }
fuzzify(id) { /* ... */ }
}
// good - Utility functions
export const bar = () => { /* ... */ };
export const fuzzify = (id) => { /* ... */ };
의존성 주입
의존성 주입은 모듈의 의존성을 외부에서 주입받도록 선언함으로써 결합을 깨는 방식입니다(예: 생성자 매개변수, 명시적인 의존성 주입 프레임워크 및 Vue의 provide/inject
를 통해).
// bad - 싱글톤에 결합된 Vue 컴포넌트
export default {
created() {
this.mediator = MyFooMediator.getInstance();
},
};
// good - Vue 컴포넌트가 의존성을 선언함
export default {
inject: ['mediator']
};
// bad - 우리는 싱글톤이 수명주기의 어디에 있는지 확신할 수 없기 때문에 여기서 초기화를 수행합니다.
export class Foo {
constructor() {
Bar.getInstance().init();
}
stuff() {
return Bar.getInstance().doStuff();
}
}
// good - 이 종속성을 생성자 인자로 받겠습니다.
// 또한 수명주기를 관리하는 것은 우리의 책임이 아닙니다.
export class Foo {
constructor(bar) {
this.bar = bar;
}
stuff() {
return this.bar.doStuff();
}
}
이 예에서 mediator
의 수명주기 및 구현 세부 정보는 모두 컴포넌트 밖(아마도 페이지 진입점)에서 관리됩니다.