본문 바로가기
프로그래밍 기법

개발자의 필수 도구: 디자인 패턴, 왜 중요하고 어떻게 쓸까? 🛠️

by Marco Backman 2025. 7. 5.



안녕하세요, 소프트웨어 개발에 관심 있는 모든 분! 오늘은 효율적이고 유지보수가 쉬운 코드를 작성하는 데 필수적인 개념, 바로 디자인 패턴(Design Patterns)에 대해 이야기해보려 합니다. 정보처리 기사 시험을 준비하시는 분들께도 단골 출제 주제이니 꼭 알아두세요!

 

이전 디자인 패턴에 대한 게시글을 보시면 예시 소스코드를 보여주면서 자세하게 다루었지만 정보처리 기사를 목표를 하는 분들을 위해 따로 요약본을 만들자고 생각해 이렇게 총 정리본을 작성 해 봅니다.

 

정보처리기사에는 주로 특정 디자인 패턴에 대해 설명하고 어떤 디자인 패턴인지를 물어보거나, 나열 된 설명이 행위, 구조, 생성 핵심 패턴 중 어디에 속하는지 물어보는 문제가 많이 나옵니다. 개인적인 생각으로는 행위 패턴 항목이 제일 헷갈린다고 생각되므로 행위패턴의 각자 특징과 차이점을 정확히 구분 하셔야 한다고 생각합니다.

 


1. 생성 패턴 (Creational Patterns)

객체 생성 방식을 캡슐화하고 추상화하여, 클라이언트가 직접 객체를 생성하는 방식에서 벗어나 유연하고 독립적으로 객체를 생성할 수 있도록 돕습니다.

1.1. 팩토리 메서드 (Factory Method)

  • 목적: 객체를 생성하는 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다. 즉, 객체 생성 책임을 서브클래스에 위임합니다.
  • 구조: Creator (팩토리 메서드를 선언하는 추상 클래스/인터페이스)와 ConcreteCreator (팩토리 메서드를 구현하여 Product 객체를 생성하는 클래스)로 구성됩니다. Product는 생성될 객체의 인터페이스입니다.
  • 장점:
    • 확장성: 새로운 Product 클래스가 추가되더라도 ConcreteCreator만 추가하면 되므로, 기존 코드를 수정할 필요가 적습니다(개방-폐쇄 원칙 준수).
    • 느슨한 결합: 클라이언트 코드가 구체적인 Product 클래스와 직접적으로 결합되지 않아 유연합니다.
  • 단점:
    • 패턴을 도입함으로써 클래스 수가 늘어나 복잡성이 증가할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 프레임워크나 라이브러리를 개발할 때, 클라이언트가 생성할 객체의 타입을 미리 알 수 없거나, 객체 생성을 서브클래스에 맡기고 싶을 때 사용합니다.
    • 예: 로깅 시스템에서 콘솔 로거, 파일 로거 등 다양한 로거를 생성해야 할 때.

1.2. 추상 팩토리 (Abstract Factory)

  • 목적: 구체적인 클래스를 지정하지 않고도 관련 객체들의 집합(패밀리)을 생성하는 인터페이스를 제공합니다. 제품군을 한 번에 생성하는 데 유용합니다.
  • 구조: AbstractFactory (제품군을 생성하는 추상 인터페이스), ConcreteFactory (구체적인 제품군을 생성하는 클래스), AbstractProduct (제품의 추상 인터페이스), ConcreteProduct (구체적인 제품 클래스)로 구성됩니다.
  • 장점:
    • 관련된 제품 객체들을 일관성 있게 생성할 수 있도록 보장합니다.
    • 클라이언트 코드가 구체적인 ConcreteFactory나 ConcreteProduct와 분리되어 유연성을 높입니다.
  • 단점:
    • 새로운 종류의 제품(인터페이스)을 추가하려면 모든 팩토리와 제품 클래스를 변경해야 하므로, 확장성이 떨어질 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 서로 관련 있는 여러 객체들을 함께 생성해야 할 때 (예: UI 툴킷에서 Windows 스타일과 Mac 스타일의 버튼, 체크박스 등을 동시에 생성).
    • 시스템이 여러 제품군 중 하나를 사용해야 하는데, 런타임에 결정되어야 할 때.

1.3. 빌더 (Builder)

  • 목적: 복잡한 객체의 생성 단계를 분리하여, 동일한 생성 절차에서 다른 표현(객체)을 만들 수 있게 합니다. 특히, 많은 매개변수를 가지는 객체를 단계별로 생성할 때 유용합니다.
  • 구조: Builder (객체 생성 단계를 정의하는 인터페이스), ConcreteBuilder (실제로 객체 부분을 생성하고 조립하는 클래스), Director (빌더를 사용하여 객체 생성 과정을 지시하는 클래스), Product (생성될 복합 객체)로 구성됩니다.
  • 장점:
    • 복잡한 객체 생성 코드를 클라이언트로부터 분리하여 가독성을 높입니다.
    • 동일한 생성 과정을 통해 다양한 표현의 객체를 만들 수 있습니다.
    • 단계별로 객체를 생성할 수 있어 유연하게 객체를 조립할 수 있습니다.
  • 단점:
    • 객체의 필드가 적은 경우에는 오히려 코드 복잡성을 증가시킬 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 객체의 생성 과정이 복잡하고 여러 단계가 필요할 때.
    • 동일한 객체 생성 과정을 통해 다양한 종류의 객체를 생성해야 할 때 (예: 웹 페이지 빌더에서 다양한 레이아웃의 페이지를 생성).
    • 생성자의 매개변수가 너무 많을 때 (텔레스코핑 생성자 문제 해결).

1.4. 프로토타입 (Prototype)

  • 목적: 기존 객체를 복사(clone)하여 새 객체를 생성합니다. 객체 생성 비용이 높을 때, 또는 객체를 동적으로 생성해야 할 때 유용합니다.
  • 구조: Prototype (자신을 복사하는 메서드를 선언하는 인터페이스)와 ConcretePrototype (실제로 자신을 복사하는 클래스)로 구성됩니다.
  • 장점:
    • 객체를 생성하는 비용이 매우 비쌀 때, 복사를 통해 성능을 향상시킬 수 있습니다.
    • 클라이언트 코드가 구체적인 클래스에 의존하지 않고 객체를 생성할 수 있습니다.
    • 런타임에 동적으로 새로운 객체를 생성할 수 있습니다.
  • 단점:
    • 복잡한 객체 그래프(다른 객체를 참조하는 객체)를 복사할 때, 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)를 주의해야 합니다. 깊은 복사는 구현이 복잡해질 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 객체를 생성하는 비용이 많이 들 때.
    • 런타임에 동적으로 객체의 타입을 결정하고 생성해야 할 때 (예: 게임에서 동일한 몬스터를 여러 개 생성).
    • 객체 생성 과정이 복잡하고, 기존 객체를 기반으로 유사한 객체를 만들고 싶을 때.

1.5. 싱글턴 (Singleton)

  • 목적:떤 클래스의 인스턴스를 오직 하나만 생성하고, 그 인스턴스에 대한 전역적인 접근을 제공합니다.
  • 구조: private 생성자를 통해 외부에서 직접 인스턴스를 생성하는 것을 막고, static 메서드를 통해 유일한 인스턴스를 반환하도록 합니다.
  • 장점:
    • 전역적인 접근이 필요한 유일한 객체를 관리할 때 용이합니다 (예: 설정 관리자, 로거, 데이터베이스 커넥션 풀).
    • 객체 생성 비용이 높은 경우, 불필요한 객체 생성을 막아 자원을 절약할 수 있습니다.
  • 단점:
    • 전역 상태: 전역 객체이기 때문에 애플리케이션의 다른 부분에 영향을 미치기 쉽고, 테스트하기 어려울 수 있습니다.
    • 의존성: 클라이언트 코드가 싱글턴에 직접 의존하게 되어 결합도가 높아질 수 있습니다.
    • 멀티스레드 환경: 멀티스레드 환경에서 올바른 동기화 처리를 하지 않으면 여러 인스턴스가 생성될 수 있는 문제가 발생할 수 있습니다 (이중 잠금 검사, 정적 초기화 등으로 해결).
  • 언제 사용하면 좋을까요?
    • 시스템에 단 하나의 인스턴스만 존재해야 하는 경우 (예: OS의 파일 시스템, 스풀러).
    • 애플리케이션 전체에서 공유되어야 하는 리소스(설정, 로거, DB 커넥션 풀 등)를 관리할 때.

2. 구조 패턴 (Structural Patterns)

클래스나 객체들을 조합하여 더 큰 구조를 만드는 방법과 관련된 패턴입니다. 서로 다른 인터페이스를 가진 객체들을 함께 작동하도록 연결하거나, 객체들 간의 관계를 조직화하는 데 중점을 둡니다.

2.1. 어댑터 (Adapter)

  • 목적: 인터페이스가 호환되지 않는 클래스들을 함께 동작할 수 있도록 묶어주는 패턴입니다. 기존 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환합니다.
  • 구조: Target (클라이언트가 사용하는 인터페이스), Adaptee (호환되지 않는 기존 클래스), Adapter (Target 인터페이스를 구현하고 Adaptee 객체를 사용하여 Target 인터페이스에 맞게 요청을 변환하는 클래스)로 구성됩니다.
  • 장점:
    • 기존 클래스의 코드를 수정하지 않고 재사용할 수 있게 합니다.
    • 클라이언트와 Adaptee 간의 의존성을 줄여줍니다.
  • 단점:
    • 새로운 어댑터 클래스가 추가되어 복잡성이 증가할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 기존에 구현된 클래스를 사용하고 싶지만, 인터페이스가 맞지 않아 사용할 수 없을 때.
    • (예: 레거시 시스템의 라이브러리를 최신 시스템에 연동해야 할 때)
    • (예: 외부 라이브러리 사용 시, 라이브러리의 인터페이스를 내 프로젝트의 인터페이스에 맞게 변환할 때)

2.2. 브릿지 (Bridge)

  • 목적: 추상화와 구현을 분리하여, 각각 독립적으로 변경될 수 있도록 합니다. 즉, 기능 클래스와 구현 클래스를 별도로 관리하여 둘 다 독립적으로 확장할 수 있게 합니다.
  • 구조: Abstraction (상위 레벨의 추상화 인터페이스), RefinedAbstraction (추상화를 확장하는 클래스), Implementor (하위 레벨의 구현 인터페이스), ConcreteImplementor (Implementor를 구현하는 클래스)로 구성됩니다.
  • 장점:
    • 추상화와 구현의 독립적인 확장이 가능해집니다. 클래스 수가 폭발적으로 증가하는 것을 막을 수 있습니다 (예: N가지 모양과 M가지 색상을 N*M개의 클래스로 만들지 않고 N+M개의 클래스로 만들 수 있음).
    • 컴파일 타임에 결합되는 것을 피하고 런타임에 구현을 변경할 수 있습니다.
  • 단점:
    • 초기 설계 복잡성이 증가할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 기능과 구현이 모두 독립적으로 확장되어야 할 때.
    • (예: 다양한 OS에서 동작하는 GUI 라이브러리 개발 시, OS별 그래픽 API를 추상화할 때)
    • (예: 다양한 포맷(HTML, JSON, XML)으로 데이터를 출력해야 할 때)

2.3. 컴포지트 (Composite)

  • 목적: 객체들의 계층 구조(부분-전체 계층)를 형성하여, 개별 객체와 복합 객체를 동일하게 다룰 수 있게 합니다.
  • 구조: Component (개별 객체와 복합 객체에 공통된 인터페이스), Leaf (계층 구조의 개별 객체), Composite (자식 객체를 가질 수 있는 복합 객체)로 구성됩니다.
  • 장점:
    • 클라이언트가 복합 객체와 개별 객체를 구분하지 않고 동일한 인터페이스로 다룰 수 있습니다.
    • 트리 구조의 데이터를 쉽게 표현하고 조작할 수 있습니다.
  • 단점:
    • 모든 Component가 Leaf와 Composite에 공통적으로 적용 가능한 메서드만 가질 수 있도록 제한됩니다.
  • 언제 사용하면 좋을까요?
    • 객체들을 트리 구조로 표현하고 싶을 때.
    • 클라이언트가 개별 객체와 복합 객체를 동일하게 처리해야 할 때 (예: GUI 컴포넌트, 파일 시스템).

2.4. 데코레이터 (Decorator)

  • 목적: 객체에 동적으로 새로운 기능을 추가합니다. 상속의 대안으로, 객체를 감싸는 형태로 기능을 확장합니다.
  • 구조: Component (원래 객체와 데코레이터가 구현할 인터페이스), ConcreteComponent (원래 객체), Decorator (Component 인터페이스를 구현하고 Component 객체를 참조하는 추상 데코레이터), ConcreteDecorator (실제로 기능을 추가하는 데코레이터)로 구성됩니다.
  • 장점:
    • 상속에 비해 유연하게 기능을 추가하거나 조합할 수 있습니다.
    • 런타임에 객체에 새로운 책임을 추가할 수 있습니다.
    • 클래스 수가 폭발적으로 증가하는 것을 막을 수 있습니다 (예: 커피에 휘핑크림, 시럽, 초코 등을 무제한으로 추가할 때).
  • 단점:
    • 객체의 핵심 기능을 파악하기 어려워질 수 있습니다 (여러 데코레이터가 중첩될 경우).
    • 작은 객체들이 많아져서 설계가 복잡해질 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 객체에 동적으로 추가적인 기능을 부여하고 싶을 때.
    • 상속 대신 유연하게 기능을 확장하고 싶을 때 (예: 입출력 스트림에 버퍼링, 압축, 암호화 등의 기능 추가).

2.5. 퍼사드 (Facade)

  • 목적: 서브시스템의 복잡한 구조를 단순화된 인터페이스로 제공합니다. 서브시스템의 여러 클래스에 대한 접근을 하나의 통합된 인터페이스를 통해 제공합니다.
  • 구조: Facade (서브시스템의 복잡성을 숨기고 단순화된 메서드를 제공하는 클래스)와 Subsystem Classes (퍼사드에 의해 사용되는 실제 복잡한 클래스들)로 구성됩니다.
  • 장점:
    • 클라이언트와 서브시스템 간의 결합도를 낮춥니다.
    • 서브시스템을 더 쉽게 사용하고 이해할 수 있게 합니다.
    • 서브시스템의 변경으로부터 클라이언트를 보호합니다.
  • 단점:
    • 퍼사드가 너무 많은 책임을 지게 되면 God Object가 될 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 복잡한 서브시스템에 대한 접근을 간소화하고 싶을 때.
    • 서브시스템을 계층화하고 싶을 때.
    • (예: 미디어 재생 시스템에서 오디오, 비디오, 자막 등을 모두 제어하는 단일 MediaPlayer 퍼사드).

2.6. 플라이웨이트 (Flyweight)

  • 목적: 많은 수의 작은 객체를 효율적으로 다루기 위해 객체 공유를 사용합니다. 객체의 일부 상태를 외부화하여 공유함으로써 메모리 사용량을 줄입니다.
  • 구조: Flyweight (공유될 수 있는 상태를 가진 객체의 인터페이스), ConcreteFlyweight (공유 가능한 상태를 구현하는 클래스), UnsharedConcreteFlyweight (공유될 수 없는 상태를 가진 클래스), FlyweightFactory (플라이웨이트 객체를 생성하고 관리하는 팩토리)로 구성됩니다.
  • 장점:
    • 메모리 사용량을 크게 줄일 수 있습니다 (수많은 객체가 필요한 경우).
    • 객체 생성 비용을 절감할 수 있습니다.
  • 단점:
    • 상태의 분리(공유 가능한 내부 상태와 공유 불가능한 외부 상태)가 필요하여 설계가 복잡해질 수 있습니다.
    • 다중 스레드 환경에서 동기화 문제가 발생할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 애플리케이션이 많은 수의 객체를 사용하고, 이 객체들이 대부분 동일한 데이터를 공유하는 경우 (예: 게임에서 수많은 나무나 풀잎 객체, 텍스트 에디터의 문자 객체).

2.7. 프록시 (Proxy)

  • 목적: 다른 객체에 대한 접근을 제어하기 위한 대리자(프록시)를 제공합니다. 실제 객체 대신 프록시 객체를 통해 요청을 전달하여, 접근 제어, 로딩 지연, 로깅 등 다양한 추가 작업을 수행할 수 있습니다.
  • 구조: Subject (클라이언트가 사용하고 프록시와 실제 객체가 구현할 인터페이스), RealSubject (실제 객체), Proxy (RealSubject와 동일한 인터페이스를 구현하고 RealSubject에 대한 접근을 제어하는 클래스)로 구성됩니다.
  • 장점:
    • 실제 객체에 대한 접근을 제어할 수 있습니다 (보안, 로깅, 캐싱, 지연 로딩 등).
    • 실제 객체의 생성이나 초기화를 지연시켜 자원을 절약할 수 있습니다.
  • 단점:
    • 추가적인 클래스(프록시)가 생겨 시스템의 복잡도가 증가할 수 있습니다.
    • 요청 처리 시간이 약간 증가할 수 있습니다 (프록시 계층 추가로 인한 오버헤드).
  • 언제 사용하면 좋을까요?
    • 객체에 대한 접근을 보호하거나 제어해야 할 때.
    • 객체의 생성 비용이 비싸서 필요할 때만 생성하고 싶을 때 (지연 로딩).
    • 원격 객체에 대한 로컬 대표 객체가 필요할 때 (원격 프록시).
    • (예: 이미지 뷰어에서 고해상도 이미지를 로드하기 전에 미리보기 이미지를 보여주는 프록시).

3. 행위 패턴 (Behavioral Patterns)

객체들 간의 알고리즘이나 책임 분배와 관련된 패턴입니다. 객체들이 어떻게 상호작용하고 책임을 분담하는지를 정의하여 시스템의 유연성과 재사용성을 높입니다.

3.1. 책임 연쇄 (Chain of Responsibility)

  • 목적: 요청을 처리할 수 있는 핸들러를 찾을 때까지 일련의 객체 체인을 따라 요청을 전달합니다. 송신자와 수신자를 분리합니다.
  • 구조: Handler (요청을 처리하는 인터페이스), ConcreteHandler (실제 요청을 처리하거나 다음 핸들러로 전달하는 클래스)로 구성됩니다. 각 핸들러는 다음 핸들러를 참조합니다.
  • 장점:
    • 송신자가 수신자를 명시적으로 알 필요 없이 요청을 보낼 수 있습니다.
    • 런타임에 체인을 동적으로 구성할 수 있어 유연성이 높습니다.
  • 단점:
    • 요청이 항상 처리된다는 보장이 없습니다 (체인의 끝까지 가도 처리할 핸들러가 없을 수 있음).
    • 체인을 따라가는 과정에서 성능 오버헤드가 발생할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 요청을 처리할 객체가 여러 개 있고, 런타임에 처리 대상을 결정해야 할 때 (예: GUI 이벤트 처리, 로깅 레벨 처리, 결재 시스템).

3.2. 커맨드 (Command)

  • 목적: 요청을 객체로 캡슐화하여, 매개변수화하거나 로깅, 실행 취소 등의 기능을 지원합니다.
  • 구조: Command (실행할 작업을 선언하는 인터페이스), ConcreteCommand (Command 인터페이스를 구현하고 Receiver에 대한 작업을 바인딩하는 클래스), Receiver (실제로 작업을 수행하는 객체), Invoker (Command 객체를 가지고 실행을 요청하는 객체), Client (ConcreteCommand를 생성하고 Invoker에 설정하는 객체)로 구성됩니다.
  • 장점:
    • 요청을 수행하는 객체(Receiver)와 요청을 하는 객체(Invoker)를 분리하여 느슨한 결합을 제공합니다.
    • 큐에 요청을 저장하거나, 요청을 로깅하고 실행 취소/재실행 기능을 쉽게 구현할 수 있습니다.
  • 단점:
    • 요청마다 별도의 커맨드 클래스를 생성해야 하므로 클래스 수가 증가할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 실행 취소(Undo) 기능을 구현해야 할 때.
    • 매크로(여러 명령을 묶어 하나의 명령으로 실행) 기능을 구현해야 할 때.
    • GUI 버튼 클릭 등 이벤트 처리 시스템에서 요청을 객체로 다루고 싶을 때.

3.3. 이터레이터 (Iterator)

  • 목적: 컬렉션(집합 객체)의 내부 표현을 노출하지 않고도 그 요소들에 순차적으로 접근하는 방법을 제공합니다.
  • 구조: Iterator (원소에 접근하고 순회하는 인터페이스), ConcreteIterator (특정 컬렉션에 대한 순회를 구현하는 클래스), Aggregate (이터레이터를 생성하는 인터페이스), ConcreteAggregate (이터레이터를 생성하는 실제 컬렉션 클래스)로 구성됩니다.
  • 장점:
    • 다양한 종류의 컬렉션에 대한 통일된 순회 인터페이스를 제공합니다.
    • 컬렉션의 내부 구조가 변경되어도 클라이언트 코드에 영향을 주지 않습니다.
    • 동시에 여러 개의 순회를 지원할 수 있습니다.
  • 단점:
    • 단순한 컬렉션의 경우 이 패턴을 도입하는 것이 과도한 복잡성을 유발할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 컬렉션의 내부 구조에 상관없이 요소에 접근하고 싶을 때.
    • 다양한 종류의 컬렉션에 대해 통일된 순회 인터페이스를 제공해야 할 때 (예: 배열, 리스트, 해시맵 등).

3.4. 옵저버 (Observer)

  • 목적: 객체의 상태가 변경되면, 그 객체를 주시하고 있는 다른 객체들에게 자동으로 변경 사항을 알리는 패턴입니다. 일대다 의존성을 정의합니다.
  • 구조: Subject (관찰 대상, 옵저버를 등록/해지하고 상태 변경 시 알림을 보내는 객체), Observer (Subject의 변경을 통지받을 객체들의 인터페이스), ConcreteSubject (실제 상태를 가지고 변경 시 Observer에게 알리는 클래스), ConcreteObserver (Subject의 변경을 통지받아 특정 작업을 수행하는 클래스)로 구성됩니다.
  • 장점:
    • Subject와 Observer 간의 느슨한 결합을 제공합니다.
    • 확장성이 용이하여 새로운 Observer를 쉽게 추가할 수 있습니다.
  • 단점:
    • 데이터가 변경될 때마다 모든 Observer에게 알림을 보내므로, 알림 순서가 중요하거나 너무 많은 Observer가 있을 경우 성능 문제가 발생할 수 있습니다.
    • 때로는 예상치 못한 업데이트를 야기할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 어떤 객체의 변경 사항을 다른 여러 객체들에게 알려야 할 때 (예: GUI 컴포넌트의 이벤트 처리, 뉴스레터 구독, 주식 시세 알림).
    • 객체의 상태 변화에 따라 다른 객체의 동작을 자동으로 변경해야 할 때.

3.5. 전략 (Strategy)

  • 목적: 알고리즘군을 정의하고, 각 알고리즘을 캡슐화하며, 이들을 상호 교환 가능하게 만듭니다. 런타임에 알고리즘을 선택할 수 있게 합니다.
  • 구조: Strategy (알고리즘을 위한 인터페이스), ConcreteStrategy (Strategy 인터페이스를 구현하는 실제 알고리즘 클래스), Context (Strategy 객체를 가지고 클라이언트 요청을 위임하는 클래스)로 구성됩니다.
  • 장점:
    • 알고리즘을 사용하는 클라이언트와 알고리즘 구현을 분리하여 유연성을 높입니다.
    • 런타임에 알고리즘을 쉽게 변경할 수 있습니다.
    • 상속 대신 위임을 통해 행동을 재사용할 수 있어 클래스 계층 구조를 단순화합니다.
  • 단점:
    • 알고리즘이 적을 경우 오히려 클래스 수가 많아져 복잡성이 증가할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 관련된 알고리즘군이 있고, 이들을 런타임에 선택적으로 사용하고 싶을 때 (예: 결제 방법(신용카드, 계좌이체, 간편결제), 정렬 알고리즘(버블 정렬, 퀵 정렬)).
    • 조건문(if-else if)으로 많은 알고리즘 분기를 처리하는 것을 피하고 싶을 때.

3.6. 템플릿 메서드 (Template Method)

  • 목적: 알고리즘의 뼈대(템플릿)를 정의하고, 일부 단계의 구현은 서브클래스에 위임합니다. 알고리즘의 구조를 변경하지 않고 서브클래스에서 특정 단계를 재정의할 수 있도록 합니다.
  • 구조: AbstractClass (템플릿 메서드를 정의하고, 일부 추상 메서드를 선언하는 클래스), ConcreteClass (추상 메서드를 구현하여 알고리즘의 특정 단계를 완성하는 서브클래스)로 구성됩니다.
  • 장점:
    • 알고리즘의 공통된 부분을 상위 클래스에 정의하여 코드 중복을 줄입니다.
    • 알고리즘의 구조를 변경하지 않고 특정 단계를 재정의하여 유연성을 제공합니다.
  • 단점:
    • 서브클래스가 알고리즘의 전체 흐름을 바꾸기 어렵습니다 (정의된 뼈대를 따라야 함).
    • 일부 단계가 너무 적을 경우 과도한 추상화가 될 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 여러 클래스가 거의 동일한 알고리즘을 가지고 있지만, 특정 단계만 다르게 구현해야 할 때 (예: 데이터 처리 과정(파일 읽기, 파싱, 처리, 쓰기)에서 파싱 방식만 다를 때).
    • 알고리즘의 특정 부분을 서브클래스에 강제하거나, 후킹 메서드를 통해 선택적으로 확장할 수 있게 하고 싶을 때.

3.7. 비지터 (Visitor)

  • 목적: 객체 구조(클래스들)를 변경하지 않고, 그 구조를 이루는 요소들에 대한 새로운 연산을 정의합니다.
  • 구조: Visitor (각 요소 클래스에 대한 visit 메서드를 선언하는 인터페이스), ConcreteVisitor (Visitor 인터페이스를 구현하여 특정 연산을 수행하는 클래스), Element (accept 메서드를 정의하는 인터페이스), ConcreteElement (accept 메서드를 구현하여 Visitor를 받아들이는 클래스), ObjectStructure (Element 객체들을 관리하는 클래스)로 구성됩니다.
  • 장점:
    • 새로운 연산을 추가할 때 기존 객체 구조를 수정할 필요가 없습니다 (개방-폐쇄 원칙 준수).
    • 관련된 연산을 하나의 Visitor 클래스에 모아 관리할 수 있습니다.
  • 단점:
    • 새로운 Element 클래스를 추가하려면 모든 Visitor를 수정해야 합니다.
    • 캡슐화를 위반할 수 있습니다 (Visitor가 Element의 내부 상태에 접근해야 할 때).
    • 양방향 디스패치(double dispatch) 메커니즘을 이해하고 구현해야 하므로 복잡성이 증가할 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 복잡한 객체 구조에 대해 여러 가지 연산을 수행해야 하는데, 이 연산들이 객체 구조 자체에 정의되어 있으면 혼란스러울 때.
    • 객체 구조에 새로운 기능을 자주 추가해야 하지만, 구조 자체는 자주 변경되지 않을 때 (예: 컴파일러의 AST(추상 구문 트리)에 대한 코드 생성, 타입 검사 등의 연산).

3.8. 미디에이터 (Mediator)

  • 목적: 객체들 간의 상호작용을 캡슐화하는 중재자 객체를 정의합니다. 객체들이 직접 통신하는 것을 막고, 중재자를 통해서만 통신하게 하여 결합도를 낮춥니다.
  • 구조: Mediator (중재자 인터페이스), ConcreteMediator (실제 중재자 구현 클래스), Colleague (중재자를 통해 상호작용하는 객체들의 인터페이스), ConcreteColleague (실제 동료 객체)로 구성됩니다.
  • 장점:
    • 객체 간의 직접적인 통신을 제거하여 결합도를 낮춥니다.
    • 객체들 간의 상호작용을 중재자 클래스에 집중시켜 관리와 변경이 용이해집니다.
  • 단점:
    • 중재자 객체가 모든 상호작용을 처리하므로, 복잡해지면 '신(God) 객체'가 될 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 여러 객체들이 복잡하게 서로를 참조하고 상호작용할 때.
    • 객체 간의 상호작용 방식을 재사용하고 싶을 때 (예: GUI 다이얼로그 박스에서 여러 위젯(버튼, 텍스트 필드) 간의 상호작용).

3.9. 메멘토 (Memento)

  • 목적: 객체의 내부 상태를 외부에 노출하지 않고 객체 외부로 저장하고 복원할 수 있도록 합니다. 객체의 스냅샷을 찍는 것과 유사합니다.
  • 구조: Originator (자신의 상태를 저장하고 복원할 객체), Memento (Originator의 상태를 저장하는 객체), Caretaker (Memento를 저장하고 관리하는 객체)로 구성됩니다.
  • 장점:
    • 캡슐화를 위반하지 않고 객체의 상태를 외부에 저장하고 복원할 수 있습니다.
    • 실행 취소(Undo) 기능을 구현하는 데 유용합니다.
  • 단점:
    • 메멘토 객체에 저장되는 상태의 양이 많을 경우 메모리 사용량이 증가할 수 있습니다.
    • 깊은 복사가 필요한 경우 구현이 복잡해질 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 객체의 상태를 저장하고 나중에 특정 시점으로 복원해야 할 때 (예: 텍스트 에디터의 실행 취소/재실행, 게임의 저장/불러오기).

3.10. 상태 (State)

  • 목적: 객체의 내부 상태에 따라 객체의 행동이 달라지도록 허용합니다. 객체가 상태를 변경할 때마다 자신의 클래스를 바꾸는 것과 동일하게 보이도록 만듭니다.
  • 구조: Context (상태 의존적인 행동을 가지는 객체), State (상태에 따른 행동을 정의하는 인터페이스), ConcreteState (State 인터페이스를 구현하는 구체적인 상태 클래스)로 구성됩니다.
  • 장점:
    • 상태에 따른 행동을 별도의 클래스로 분리하여 복잡한 조건문(if-else if 또는 switch-case)을 제거하고 가독성을 높입니다.
    • 새로운 상태를 추가하거나 기존 상태를 변경하기 용이하여 확장성이 좋습니다.
  • 단점:
    • 상태의 수가 적을 경우 오히려 클래스 수가 증가하여 설계가 복잡해질 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 객체의 행동이 내부 상태에 따라 동적으로 변경되어야 할 때 (예: 주문 처리 시스템의 주문 상태(주문 접수, 배송 중, 배송 완료), 신호등, 게임 캐릭터의 상태(걷기, 뛰기, 점프)).

3.11. 인터프리터 (Interpreter)

  • 목적: 주어진 언어에 대한 문법 표현을 정의하고, 이 문법을 사용하여 문장을 해석하는 인터프리터를 포함합니다.
  • 구조: AbstractExpression (문법 규칙에 대한 인터페이스), TerminalExpression (말단 심볼을 위한 클래스), NonterminalExpression (복합 심볼을 위한 클래스), Context (해석기 간에 공유되는 정보를 포함), Client (문장을 구성하고 해석하는 객체)로 구성됩니다.
  • 장점:
    • 새로운 문법 규칙을 쉽게 추가하고 변경할 수 있습니다.
    • 문법을 클래스 계층 구조로 명확하게 표현할 수 있습니다.
  • 단점:
    • 복잡한 문법의 경우 클래스 수가 너무 많아져 관리하기 어려울 수 있습니다.
  • 언제 사용하면 좋을까요?
    • 단순한 언어나 표현식을 파싱하고 해석해야 할 때 (예: SQL 쿼리 파서, 정규 표현식, 계산식 파서).
    • 문법이 자주 변경되지 않거나 상대적으로 단순할 때.