InfoGrab DocsInfoGrab Docs

디자인 패턴

요약

이 페이지에서는 권장 디자인 패턴과 안티패턴을 다룹니다. 이 문서에 디자인 패턴을 추가할 때는 해결하는 문제를 명확히 기술하세요. 다음 디자인 패턴은 공통 문제를 해결하기 위한 권장 접근 방식입니다. 안티패턴은 처음에는 좋은 접근 방식처럼 보일 수 있지만, 이점보다 더 많은 문제를 초래한다는 것이 입증되었습니다.

이 페이지에서는 권장 디자인 패턴과 안티패턴을 다룹니다.

이 문서에 디자인 패턴을 추가할 때는 해결하는 문제를 명확히 기술하세요. 디자인 안티패턴을 추가할 때는 방지하는 문제를 명확히 기술하세요.

패턴#

다음 디자인 패턴은 공통 문제를 해결하기 위한 권장 접근 방식입니다. 특정 패턴이 해당 상황에 적합한지 평가할 때는 적절한 판단이 필요합니다. 패턴이라고 해서 반드시 모든 문제에 좋은 해결책은 아닙니다.

안티패턴#

안티패턴은 처음에는 좋은 접근 방식처럼 보일 수 있지만, 이점보다 더 많은 문제를 초래한다는 것이 입증되었습니다. 이러한 패턴은 일반적으로 피해야 합니다.

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의 수명 주기와 구현 세부사항은 모두 컴포넌트 외부(대부분 페이지 진입점)에서 관리됩니다.

디자인 패턴

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의 수명 주기와 구현 세부사항은 모두 컴포넌트 외부(대부분 페이지 진입점)에서 관리됩니다.