디자인 패턴
GitLab v19.1이 페이지에서는 권장 디자인 패턴과 안티패턴을 다룹니다. 이 문서에 디자인 패턴을 추가할 때는 해결하는 문제를 명확히 기술하세요. 다음 디자인 패턴은 공통 문제를 해결하기 위한 권장 접근 방식입니다. 안티패턴은 처음에는 좋은 접근 방식처럼 보일 수 있지만, 이점보다 더 많은 문제를 초래한다는 것이 입증되었습니다.
이 페이지에서는 권장 디자인 패턴과 안티패턴을 다룹니다.
이 문서에 디자인 패턴을 추가할 때는 해결하는 문제를 명확히 기술하세요. 디자인 안티패턴을 추가할 때는 방지하는 문제를 명확히 기술하세요.
패턴#
다음 디자인 패턴은 공통 문제를 해결하기 위한 권장 접근 방식입니다. 특정 패턴이 해당 상황에 적합한지 평가할 때는 적절한 판단이 필요합니다. 패턴이라고 해서 반드시 모든 문제에 좋은 해결책은 아닙니다.
안티패턴#
안티패턴은 처음에는 좋은 접근 방식처럼 보일 수 있지만, 이점보다 더 많은 문제를 초래한다는 것이 입증되었습니다. 이러한 패턴은 일반적으로 피해야 합니다.
GitLab 코드베이스 전반에 걸쳐 이러한 안티패턴의 과거 사용 사례가 있을 수 있습니다. 이러한 레거시 패턴을 사용하는 코드를 다룰 때 리팩토링 여부를 결정할 때는 적절한 판단을 활용하세요.
새 기능의 경우 안티패턴이 반드시 금지되는 것은 아니지만, 다른 접근 방식을 찾는 것을 강력히 권장합니다.
공유 전역 객체 (Shared Global Object)#
공유 전역 객체(Shared Global Object)는 어디서든 접근할 수 있어 명확한 소유자가 없는 인스턴스입니다.
다음은 Vuex Store에 이 패턴을 적용한 예입니다:
const createStore = () => new Vuex.Store({
actions,
state,
mutations
});
// Notice that we are forcing all references to this module to use the same single instance of the store.
// We are also creating the store at import-time and there is nothing which can automatically dispose of it.
//
// As an alternative, we should export the `createStore` and let the client manage the
// lifecycle and instance of the store.
export default createStore();
공유 전역 객체는 어떤 문제를 일으키나요?#
공유 전역 객체는 어디서든 접근할 수 있어 편리합니다. 그러나 편의성이 항상 그 높은 비용을 상쇄하지는 않습니다:
-
소유권 없음. 이러한 객체에는 명확한 소유자가 없으므로 비결정적이고 영구적인 수명 주기를 갖게 됩니다. 이는 테스트에서 특히 문제가 될 수 있습니다.
-
접근 제어 없음. 공유 전역 객체가 일부 상태를 관리할 때, 이 객체에 대한 접근 제어가 없기 때문에 매우 버그가 많고 어려운 결합 상황이 발생할 수 있습니다.
-
순환 참조 가능성. 공유 전역 객체는 공유 전역 객체의 하위 모듈이 자기 자신을 참조하는 모듈을 참조할 수 있기 때문에 일부 순환 참조 상황을 만들 수 있습니다(예시는 이 머지 리퀘스트 참고).
다음은 이 패턴이 문제적으로 식별된 과거 사례입니다:
공유 전역 객체 패턴이 실제로 적합한 경우는 언제인가요?#
공유 전역 객체는 무언가를 전역적으로 접근 가능하게 만드는 문제를 해결합니다. 이 패턴은 다음의 경우에 적합할 수 있습니다:
- 책임이 진정으로 전역적이고 애플리케이션 전체에서 참조되어야 할 때 (예: 애플리케이션 전체 이벤트 버스).
이러한 시나리오에서도 부작용을 추론하기 매우 어렵기 때문에 공유 전역 객체 패턴을 피하는 것을 고려하세요.
참조#
더 많은 정보는 C2 위키의 Global Variables Are Bad를 참고하세요.
싱글톤 (Singleton)#
클래식 싱글톤 패턴은 하나의 인스턴스만 존재하도록 보장하는 접근 방식입니다.
다음은 이 패턴의 예입니다:
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를 예로 들어보겠습니다. 이 컴포넌트는 Monaco 작업을 위한 일부 상태를 관리하는 싱글톤 Editor와 인터페이스합니다. Editor 클래스의 싱글톤 특성 때문에, 컴포넌트
RepoEditor도 싱글톤이 될 수 밖에 없습니다. 이 컴포넌트의 여러 인스턴스는Editor의 인스턴스를 진정으로 소유하는 사람이 없기 때문에 프로덕션 문제를 일으킬 수 있습니다.
왜 싱글톤 패턴이 Java와 같은 다른 언어에서 인기가 있나요?#
이는 Java와 같은 언어의 제한 때문으로, 모든 것이 클래스로 래핑되어야 합니다. JavaScript에서는 객체와 함수 리터럴 같은 것이 있어 유틸리티 함수를 내보내는 모듈로 많은 문제를 해결할 수 있습니다.
싱글톤 패턴이 실제로 적합한 경우는 언제인가요?#
싱글톤은 하나의 인스턴스만 존재하도록 강제하는 문제를 해결합니다. 다음과 같은 드문 경우에 싱글톤이 적합할 수 있습니다:
-
정확히 1개의 인스턴스만 가져야 하는 리소스를 관리해야 할 때(즉, 일부 하드웨어 제한).
-
진정한 횡단 관심사(cross-cutting concern)(예: 로깅)가 있고 싱글톤이 가장 간단한 API를 제공할 때.
이러한 시나리오에서도 싱글톤 패턴을 피하는 것을 고려하세요.
싱글톤 패턴의 대안은 무엇인가요?#
유틸리티 함수 (Utility Functions)#
상태를 관리할 필요가 없을 때, 클래스 인스턴스화와 무관하게 모듈에서 유틸리티 함수를 내보낼 수 있습니다.
// 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) => { /* ... */ };
의존성 주입 (Dependency Injection)#
의존성 주입(Dependency Injection)은 모듈의 의존성을 모듈 외부에서 주입하도록 선언함으로써 결합을 끊는 접근 방식입니다(예: 생성자 매개변수, 정통 의존성 주입 프레임워크, 심지어 Vue의 provide/inject).
// bad - Vue component coupled to Singleton
export default {
created() {
this.mediator = MyFooMediator.getInstance();
},
};
// good - Vue component declares dependency
export default {
inject: ['mediator']
};
// bad - We're not sure where the singleton is in it's lifecycle so we init it here.
export class Foo {
constructor() {
Bar.getInstance().init();
}
stuff() {
return Bar.getInstance().doStuff();
}
}
// good - Lets receive this dependency as a constructor argument.
// It's also not our responsibility to manage the lifecycle.
export class Foo {
constructor(bar) {
this.bar = bar;
}
stuff() {
return this.bar.doStuff();
}
}
이 예시에서 mediator의 수명 주기와 구현 세부사항은 모두 컴포넌트 외부(대부분 페이지 진입점)에서 관리됩니다.