GoF Design Patterns

GoF Design Patterns

Refactoring.GuruDesign Patterns 주제를 정리하며 실습한 내용들을 다루는 글입니다.

.

Creational Design Patterns

생성 디자인 패턴은 기존 코드의 유연성과 재사용을 증가시키는 객체를 생성하는 다양한 방법을 제공

.

Factory Method

factory-method

부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지만, 자식 클래스들이 생성될 객체들의 유형을 변경할 수 있도록 하는 생성 패턴

구상 클래스들로부터 객체들을 생성하는 생성 메서드들로 인식될 수 있다. 구상 클래스들은 객체 생성 중에 사용되지만 팩토리 메서드들의 반환 유형은 일반적으로 추상 클래스 또는 인터페이스로 선언

.

Problem

트럭 물류 관리 어플을 개발했다.

요즘들어 어플이 유명해지면서 해상 물류 회사들로부터 해상 물류 기능을 추가해 달라는 요청이 들어오고 있다.

하지만.. 지금 대부분 코드는 트럭 클래스에 의존되어 있고, 선박 클래스를 추가하기 위해 전체 코드 베이스 변경이 필요한 상황이다. 이후 다른 유형의 물류 교통수단도 추가된다면 다시 전체 코드 베이스 수정이 필요할 것이다.

이대로라면 운송 수단 객체들이 추가될 때마다 많은 조건문들이 생겨나는 매우 복잡한 코드가 작성될텐데..

어떻게 하는 게 좋을까? 😭

.

Solution

Factory Method Pattern은 객체 생성 호출을 특별한 팩토리 메소드에 대한 호출로 대체

  • 자식 클래스들은 팩토리 메서드가 반환하는 객체들의 클래스를 변경 가능

    • 생성자 호출을 팩토리 메소드에게 위임하면서 자식 클래스에서 팩토리 메소드를 오버라이딩하고 생성되는 제품들의 클래스를 변경 가능

  • 약간의 제한이 있지만, 자식 클래스들은 다른 유형의 제품들을 해당 제품들이 공통 기초 클래스 또는 공통 인터페이스가 있는 경우에만 반환 가능

    • ConcreteCreatorA 클래스에 포함된 팩토리 메소드는 ConcreteProductA 객체들을 반환

    • ConcreteCreatorB 클래스에 포함된 팩토리 메소드는 ConcreteProductB 객체들을 반환

.

모든 제품 클래스들이 공통 인터페이스를 구현하는 한, 제품 클래스들의 객체들을 손상시키지 않고 클라이언트 코드 작성 가능

  • 클라이언트는 다양한 자식 클래스들에서 실제로 반환되는 클래스를 알지 못함

  • 클라이언트는 모든 제품을 추상 클래스로 간주하고 메소드가 어떻게 동작하는지 중요하지 않음

public class App {

    private static Logistics creator;

    public void initialize(String type) {
        if ("truck".equals(type)) {
            creator = new RoadLogistics();
            return;
        }

        if ("ship".equals(type)) {
            creator = new SeaLogistics();
            return;
        }

        throw new IllegalArgumentException("Unknown operating system.");
    }

    public static void main(String[] args) {
        App app = new App();

        app.initialize("truck");
        creator.planDelivery(); //=> Truck deliver

        app.initialize("ship");
        creator.planDelivery(); //=> Ship deliver
    }
}

.

Practice

Factory Method Pattern Practice

.

Apply

  • 함께 작동해야 하는 객체들의 정확한 유형들과 의존관계들을 미리 모르는 경우 사용

  • 라이브러리 또는 프레임워크의 사용자들에게 내부 컴포넌트들을 확장하는 방법을 제공하고 싶을 때 사용

  • 기존 객체들을 매번 재구축하는 대신 이들을 재사용하여 시스템 리소스를 절약하고 싶을 때 사용

.

pros and cons

장점.

  • Creator, Product 가 강하게 결합되지 않도록 할 수 있으

  • 단일 책임 원칙(SRP). 제품 생성 코드를 한 곳으로 이동

  • 개방/폐쇄 원칙(OCP). 기존 클라이언트 코드를 훼손하지 않고 새로운 유형의 제품을 추가

단점.

  • 패턴을 구현하기 위해 많은 (자식)클래스 생성이 필요하여 코드가 복잡해질 수 있음

.

Abstract Factory

abstract-factory

관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있도록 하는 생성패턴

팩토리 객체를 반환하는 메서드들의 존재 여부로 쉽게 인식. 그 후 팩토리는 특정 하위 컴포넌트들을 만드는 데 사용

Problem

의자, 소파, 테이블을 판매하는 프로그램을 만들고 있다.

취향별로 디자인을 묶어 제품을 세트로 판매하고 싶다.

A 디자인 세트, B 디자인 세트, C 디자인 세트..

새로운 디자인 세트가 나오게 되면 추가할 때마다 기존 코드를 변경해야 하는 번거로움을 피하고 싶은데..

어떻게 하는 게 좋을까? 😭

.

Solution

\1. 각 제품 디자인 세트​에 해당하는 개별적인 인터페이스를 명시적으로 선언하기

  • 제품의 모든 변형이 위 인터페이스를 따르도록 하기

    • ex. 모든 의자의 변형들은 Chair 인터페이스를 구현

    • ex. 모든 테이블 변형들은 ­Table 인터페이스를 구현.. 등의 규칙을 명시

\2. 추상 팩토리 패턴을 선언하기

  • 추상 팩토리 패턴은 제품 디자인 새트 내의 모든 개별 제품들의 생성 메서드들이 목록화되어있는 인터페이스

    • ex. create­Chair, create­Sofa, create­­Table

\3. 제품 변형 다루기

  • 패밀리의 각 변형에 대해 Abstract­Factory 추상 팩토리 인터페이스를 기반으로 별도의 팩토리 클래스를 생성

  • 팩토리는 특정 종류의 제품을 반환하는 클래스

    • ex. Modern­Furniture­Factory​에서는 다음 객체들만 생성(Modern­Chair, Modern­Sofa​, Modern­Coffee­Table​)

\4. 클라이언트

  • 클라이언트는 자신에 해당하는 추상 인터페이스를 통해 팩토리들과 제품들 모두와 함께 작동해야 한다.

  • 그래야 클라이언트 코드에 넘기는 팩토리의 종류와 제품 변형들을 클라이언트 코드를 손상하지 않으며 자유자재로 변경 가능

  • 클라이언트는 함께 작업하는 팩토리의 구상 클래스에 대해 신경을 쓰지 않아야 한다.

.

Practice

Abstract Factory Pattern Practice

.

Apply

  • 관련된 제품군의 다양한 세트들과 작동해야 하지만 해당 제품들의 구상 클래스들에 의존하고 싶지 않을 경우 사용

    • 새로 추가될 클래스를 미리 알 수 없고, 확장성을 고려할 경우

    • 추상 팩토리가 각 세트에 포함되는 제품들을 다른 제품으로 잘못 생성할 일이 없음

  • 클래스가 있고, 이 클래스의 팩토리 메소드들의 집합의 기본 책임이 뚜렷하지 않을 경우 고려

    • 잘 설계된 프로그램에서 각 클래스는 하나의 책임만 가짐(SRP. 단일 책임 원칙)

.

pros and cons

장점.

  • 팩토리에서 생성되는 제품들의 상호 호환 보장.

  • 구상 제품들과 클라이언트 코드 사이의 단단한 결합을 피할 수 있음.

  • 단일 책임 원칙(SRP). 제품 생성 코드를 한 곳으로 추출하여 쉬운 유지보수 가능.

  • 개방/폐쇄 원칙(OCP). 기존 클라이언트 코드를 훼손하지 않고 제품의 새로운 변형들을 생성 가능.

단점.

  • 새로운 패턴이 추가되면 인터페이스, 클래스가 많이 도입되므로 코드가 필요 이상으로 복잡해질 수 있음.

.

Builder

builder

빌더는 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴

  • 같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현 제작 가능

하나의 생성 메서드와 결과 객체를 설정하기 위한 여러 메서드가 있는 클래스. 빌더 메서드들은 사슬식 연결을 지월

ex. someBuilder.​setValueA(1).​setValueB(2).​create()

.

Problem

많은 필드와 중첩된 객체들을 단계별로 힘들게 초기화해야 하는 복잡한 객체들을 만나보았을 것이다.

이러한 초기화 코드는 일반적으로 많은 매개변수가 있는 거대한 생성자 내부에 묻혀 있다.

더 최악의 상황에는.. 클라이언트 코드 전체에 흩어져 있을 수도 있다.

여기에 특정 케이스에만 사용되는 매개변수들이 조금씩 추가되다 보면 생성자 호출 코드는 알아볼 수 없을 지경이 되어 버릴 것이다..

어떻게 하는 게 좋을까? 😭

.

Solution

빌더 패턴은 자신의 클래스에서 객체 생성 코드를 추출하여 builders(건축업자들)​라는 별도의 객체들로 이동하도록 제안

  • 객체 생성을 일련의 단계들로 정리

  • 객체를 생성하고 싶다면 단계들을 builder 객체에 실행

  • 객체의 특정 설정을 제작하는 데 필요한 단계들만 호출

디렉터

  • 제품을 생성하는 데 사용하는 빌더 단계들에 대한 일련의 호출을 디렉터(관리자)라는 별도의 클래스로 추출

  • Director 클래스는 제작 단계들을 실행하는 순서를 정의하는 반면 Builder는 이러한 단계들에 대한 구현을 제공

  • 디렉터 클래스는 필수가 아니지만, 다양한 생성 루틴들을 배치하여 재사용할 수 있는 좋은 장소가 될 수 있다.

  • 또한, 디렉터 클래스는 클라이언트 코드에서 제품 생성의 세부 정보를 완전히 숨길 수 있다.

    • 클라이언트는 빌더를 디렉터와 연관시키고 디렉터와 생성을 시행한 후 빌더로부터 결과를 얻기만 하면 됩니다.

.

Practice

Builder Pattern Practice

.

Apply

  • '점층적 생성자'를 제거하기 위해 빌더 패턴 사용

    • 필요한 단계들만 사용하여 단계별로 객체들을 생성 가능

    • 패턴 구현 후에는 수십 개의 매개변수를 생성자에 집어넣는 일은 불필요

  • 코드가 일부 제품의 다른 표현(ex. SUV)들​을 생성할 수 있도록 하고 싶을 때 사용

  • 복합체 트리, 기타 복잡한 객체들을 생성

.

pros and cons

장점.

  • 객체들을 단계별로 생성하거나, 생성 단계들을 연기하거나, 재귀적으로 단계들을 실행 가능

  • 제품들의 다양한 표현을 만들 때 같은 생성 코드를 재사용 가능

  • 단일 책임 원칙(SRP). 제품의 비즈니스 로직에서 복잡한 생성 코드 고립 가능

단점.

  • 패턴이 여러 개의 새 클래스들을 생성해야 하므로 코드의 전반적인 복잡성이 증가

.

Prototype

prototype

코드를 각 클래스들에 의존시키지 않고 기존 객체들을 복사할 수 있도록 하는 생성 디자인 패턴

프로토타입은 clone 또는 copy 등의 메서드 유무로 식별 가능

.

Problem

특정한 객체의 복사본을 만들고 싶다.

그렇다면.. 먼저 같은 클래스의 새 객체를 생성하고.. 원본 객체의 모든 필드를 살피고.. 해당 값들을 새 객체에 복사해야 한다.

하지만.. 객체 필드들 중 일부가 비공개라면 모든 객체에 이 방법을 적용할 수 없을 것이다.

그리고.. 객체의 복제본을 생성하려면 객체의 클래스를 알아야 하므로, 코드는 해당 클래스에 의존하게 될 것이다.

또, 인터페이스의 구현 클래스라면 인터페이스만 알고, 그 객체의 구상 클래스는 알지 못할 수 있다.

그렇다면.. 어떻게 하는 게 좋을까? 😭

.

Solution

프로토타입 패턴은 실제로 복제되는 객체들에 복제 프로세스를 위임

  • 복제를 지원하는 모든 객체에 대한 공통 인터페이스를 선언

  • 이 인터페이스를 사용하면 코드를 객체의 클래스에 결합하지 않고도 해당 객체를 복제 가능

  • 일반적으로 이러한 인터페이스에는 단일 clone 메서드만 포함

clone 메서드 구현은 모든 클래스에서 매우 유사

  • 이 메서드는 현재 클래스의 객체를 만든 후 이전 객체의 모든 필드 값을 새 객체로 전달

  • 객체들이 같은 클래스에 속한 다른 객체의 비공개 필드들에 접근​ 가능

프로토타입: 복제를 지원하는 객체

  • 객체들에 수십 개의 필드와 수백 개의 가능한 설정들이 있는 경우 이를 복제하는 것이 서브클래싱의 대안이 될 수 있음

  • 프로그래밍의 프로토타입의 경우 생산과정에 참여하지 않고 자신을 복제하므로 세포의 유사분열 과정과 유사

.

Practice

Prototype Pattern Practice

.

Apply

  • 복사해야 하는 객체들의 구상 클래스들에 코드가 의존하면 안 될 경우 사용

    • 클라이언트 코드가 복제하는 객체들의 구상 클래스들에서 클라이언트 코드를 독립

  • 각자의 객체를 초기화하는 방식만 다른, 자식 클래스들의 수를 줄이고 싶을 경우 사용

    • 다양한 방식으로 설정된 미리 만들어진 객체들의 집합을 프로토타입들로 사용할 수 있도록 제공

    • 일부 설정과 일치하는 자식 클래스를 인스턴스화하는 대신 클라이언트는 간단하게 적절한 프로토타입을 찾아 복제

.

pros and cons

장점.

  • 객체들을 그 구상 클래스들에 결합하지 않고 복제 가능

  • 반복되는 초기화 코드를 제거한 후, 그 대신 미리 만들어진 프로토타입들을 복제하는 방법을 사용

  • 복잡한 객체들을 더 쉽게 생성

  • 복잡한 객체들에 대한 사전 설정들을 처리할 때 상속 대신 사용할 수 있는 방법

단점.

  • 순환 참조가 있는 복잡한 객체들을 복제하는 것은 매우 까다로울 수 있음

.

Singleton

singleton

클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근 지점을 제공하는 생성 디자인 패턴

싱글턴은 캐싱 된 같은 객체를 반환하는 정적 생성 메서드로 식별 가능

.

Problem

싱글턴 패턴은 한 번에 두 가지의 문제를 동시에 해결함으로써 단일 책임 원칙(SRP)을 위반

클래스에 인스턴스가 하나만 존재

  • 생성자 호출은 특성상 반드시 새 객체를 반환해야 하므로 위 행동은 일반 생성자로 구현 불가.

해당 인스턴스에 대한 전역 접근 지점을 제공

  • 프로그램의 모든 곳에서부터 일부 객체에 접근 가능

  • 그러나, 다른 코드가 해당 인스턴스를 덮어쓰지 못하도록 보호

최근에는 싱글턴 패턴이 워낙 대중화되어 패턴이 나열된 문제 중 한 가지만 해결하더라도 그것을 싱글턴이라고 부를 수 있음.

.

Solution

싱글턴의 모든 구현에는 공통적으로 두 단계가 존재

  • 다른 객체들이 싱글턴 클래스와 함께 new 연산자를 사용하지 못하도록 디폴트 생성자를 비공개로 설정

  • 생성자 역할을 하는 정적 생성 메서드 생성

    • 내부적으로 이 메서드는 객체를 만들기 위하여 비공개 생성자를 호출한 후 객체를 정적 필드에 저장

    • 이 메서드에 대한 그다음 호출들은 모두 캐시된 객체 반환

싱글턴 클래스에 접근할 수 있는 경우, 이 코드는 싱글턴의 정적 메서드 호출 가능

  • 따라서 해당 메서드가 호출될 때마다 항상 같은 객체가 반환

.

Practice

Singleton Pattern Practice

단일 스레드에서 기본 싱글턴

  • 기본 싱글턴은 생성자를 숨기고 정적 생성 메서드를 구현

멀티 스레드에서 기본 싱글턴

  • 여러 스레드가 생성 메서드를 동시에 호출할 수 있고, 싱글턴 클래스의 여러 인스턴스를 가져올 수 있음

지연 로딩이 있는 스레드 안전한 싱글턴

  • 싱글턴 객체를 처음 생성하는 동안 스레드들을 동기화

.

Apply

  • 클래스에 모든 클라이언트가 사용할 수 있는 단일 인스턴스만 있어야 할 경우

    • ex. 프로그램에서 공유되는 단일 데이터베이스 객체

    • 클래스의 객체를 생성할 수 있는 모든 수단을 비활성화

    • 새 객체를 생성하거나 객체가 이미 생성되었으면 기존 객체를 반환

  • 전역 변수들을 더 엄격하게 제어해야 할 경우

    • 클래스의 인스턴스가 하나만 있도록 보장

.

pros and cons

장점.

  • 클래스가 하나의 인스턴스만 갖는 것을 보장

  • 인스턴스에 전역 접근 가능

  • 처음 요청될 때만 초기화

단점.

  • 단일 책임 원칙(SRP) 위반 (한 번에 두 가지의 문제를 동시에 해결)

  • 다중 스레드 환경에서 여러 스레드가 싱글턴 객체를 여러번 생성하지 않도록 처리 필요

  • 클라이언트 코드 유닛 테스트의 어려움

    • 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존

    • 싱글턴 클래스의 생성자는 비공개이고 대부분 언어에서 정적 메서드를 오버라이딩하는 것이 불가능

  • 컴포넌트들이 서로에 대해 너무 많이 알고 있을 수 있음

.

Structural Design Patterns

구조 패턴은 구조를 유연하고 효율적으로 유지하면서 객체와 클래스들을 더 큰 구조로 조립하는 방법 제공

.

Adapter

Adapter, Wrapper

호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴

어댑터는 다른 추상/인터페이스 유형의 인스턴스를 받는 생성자의 존재여부로 인식 가능.

어댑터가 메서드 호출을 수신하면, 어댑터는 매개변수들을 적절한 형식으로 변환한 다음 해당 호출을 래핑 된 객체의 하나 또는 여러 메서드에 전달.

.

Problem

XML 형식으로 데이터를 내려주는 API 가 있다.

하지만 우리가 사용하는 라이브러리는 JSON 형식의 데이터로만 동작한다.

XML 형식의 데이터를 주는 API와 JSON 형식의 데이터로 동작하는 라이브러리를 호환시키고 싶은데..

어떻게 하는 게 좋을까? 😭

.

Solution

어댑터는 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 특별한 객체

  • 변환의 복잡성을 숨기기 위해 객체 중 하나를 래핑​(포장)

  • ​래핑된 객체는 어댑터 인식 불가

  • ex. km, m 단위로 동작하는 객체를 ft, mile 같은 영국식 단위로 변환하는 어댑터

데이터를 다양한 형식으로 변환 가능하고 다른 인터페이스를 가진 객체들이 협업하는 데 도움

  • 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만드는 것도 가능

.

Practice

Adapter Pattern Practice

객체 어댑터

  • 객체 합성 원칙을 사용

  • 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑

클래스 어댑터

  • 상속을 사용

  • 어댑터는 동시에 두 객체의 인터페이스를 상속

.

Apply

  • 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 경우

  • 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우

    • 누락된 기능을 어댑터 클래스에 넣고 자식 클래스를 래핑하여 필요한 기능들을 동적으로 획득

.

pros and cons

장점.

  • 단일 책임 원칙(SRP).

    • 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드 분리 가능

  • 개방/폐쇄 원칙(OCP)

    • 클라이언트 코드가 클라이언트 인터페이스를 통해 어댑터와 작동하는 한, 기존의 클라이언트 코드를 손상시키지 않고 새로운 유형의 어댑터들을 프로그램에 도입 가능

단점.

  • 다수의 새로운 인터페이스와 클래스들을 도입해야 하므로 코드의 전반적인 복잡성이 증가

    • 때로는 코드의 나머지 부분과 작동하도록 서비스 클래스를 변경하는 것이 더 간단

.

Bridge

Bridge

브리지는 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조​(추상화 및 구현)​로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴

일부 제어 개체는 의존하는 여러 다른 플랫폼들과 명확하게 구분

.

Problem

모양(원, 직사각형)과 색상(빨간색, 파란색)으로 조합을 만들려고 한다.

빨간색 원, 빨간색 직사각형, 파란색 원, 파란색 직사각형의 조합이 생기게 될텐데 새로운 모양과 색상이 추가될 떄마다 계층 구조는 기하급수적으로 많아지고 코드도 복잡해질 것이다.

복잡성을 줄이려면 어떻게 하는 게 좋을까? 😭

.

Solution

브리지 패턴은 상속에서 객체 합성으로 전환하여 이 문제를 해결

  • 차원 중 하나를 별도의 클래스 계층구조로 추출하여 원래 클래스들이 한 클래스 내에서 모든 상태와 행동들을 갖는 대신 새 계층구조의 객체를 참조

[ AS-IS ]
모양
 ㄴ 빨간색 원
 ㄴ 빨간색 직사각형
 ㄴ 파랑색 원
 ㄴ 파란색 직사각형

[ TO-BE ]
모양
 ㄴ 원
 ㄴ 직사각형


 ㄴ 빨간색
 ㄴ 파란색

추상화와 구현

  • 추상화: 앱의 GUI 레이어(IOS, Window, Linux)

  • 구현: 운영 체제의 API

.

Practice

Bridge Pattern Practice

.

Apply

  • 특정 기능의 여러 변형을 가진 모놀리식 클래스를 여러 클래스 계층구조로 나눌 경우

    • ex. 클래스가 다양한 데이터베이스 서버들과 작동하는 경우

  • 여러 독립 차원에서 클래스를 확장해야 할 경우

    • 모든 작업을 자체적으로 수행하는 대신 추출된 계층구조들에 속한 객체들에게 관련 작업들을 위임

  • 런타임​에 구현을 전환할 수 있어야 할 경우

    • 필드에 새 값을 할당하면 추상화 내부 구현 객체 변경 가능

.

pros and cons

장점.

  • 플랫폼 독립적인 클래스와 앱을 만들 수 있음

  • 클라이언트 코드는 상위 수준의 추상화를 통해 작동하며, 플랫폼 세부 정보에 노출되지 않음

  • 개방/폐쇄 원칙(OCP). 새로운 추상화들과 구현들을 상호 독립적으로 도입 가능

  • 단일 책임 원칙(SRP). 추상화의 상위 수준 논리와 구현의 플랫폼 세부 정보에 집중 가능

단점.

  • 결합도가 높은 클래스에 패턴을 적용하여 코드를 더 복잡하게 만들 수 있음

.

Composite

Composite

복합체 패턴은 객체들을 트리 구조들로 구성한 후, 이러한 구조들과 개별 객체들처럼 작업할 수 있도록 하는 구조 패턴

코드에 객체 트리가 있고 트리의 각 객체가 같은 클래스 계층구조의 일부일 경우

이러한 클래스의 메서드가 작업을 트리의 자식 객체에 위임하고, 이러한 위임을 계층구조의 기초 클래스/인터페이스를 통해 수행

.

Problem

복합체 패턴은 앱의 핵심 모델이 트리로 표현될 수 있을 때만 사용.

.

아래와 같이 복잡한 주문이 들어왔다.

주문의 총 가격을 구해야 할 때, 현실 세계라면 모든 상자를 푼 후 내부의 모든 제품을 확인할 수 있을 것이다.

하지만, 프로그램이서는 상자의 중첩 수준과 세부 사항들을 미리 알고 있어야 하기 때문에 현실 세계와 같은 직접적인 접근 방식으로 총 가격을 구하기 어렵다.

어떻게 하는 게 좋을까? 😭

.

Solution

  • 복합체 패턴를 적용하면, 총 가격을 계산하는 메서드가 선언된 공통 인터페이스를 통해 제품 및 상자 클래스들과 작업해볼 수 있다.

    • 제품: 단순히 제품 가격 반환

    • 상자: 상자에 포함된 각 항목과 가격을 확인 후 상자의 총 가격 반환

  • 상자 안에 상자가 있는 객체 트리의 모든 컴포넌트들에 대해 재귀적으로 행동을 실행

    • 메서드를 호출하면 객체들은 트리 아래로 요청을 전달

.

Practice

Composite Pattern Practice

.

Apply

  • 트리와 같은 객체 구조를 구현해야 할 경우

  • 클라이언트 코드가 단순 요소, 복합 요소들을 모두 균일하게 처리하도록 하고 싶을 경우

    • 모든 요소들은 공통 인터페이스를 공유

.

pros and cons

장점.

  • 다형성과 재귀를 통해 복잡한 트리 구조를 편리하게 작업

  • 개방/폐쇄 원칙(OCP). 객체 트리와 작동하는 기존 코드를 훼손하지 않고 앱에 새로운 요소 유형들을 도입 가능

단점.

  • 기능이 너무 다른 클래스들에는 공통 인터페이스를 제공하기 어려울 수 있음

  • 어떤 경우에는 컴포넌트 인터페이스를 과도하게 일반화해야 하여 이해하기 어렵게 만들어질 수 있음

.

Decorator

Decorator

데코레이터는 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴

현재 클래스와 동일한 클래스 또는 인터페이스의 객체를 받아들이는 생성 메서드 또는 생성자에 의해 인식 가능

.

Problem

이메일로 알림을 보내는 기능을 가진 라이브러리를 만들었다. 해당 라이브러리를 사용하는 사용자들은 이메일뿐만 아니라 SMS 문자, 페이스북, 슬랙 등으로도 알림을 보내는 기능이 생기길 원한다. 그렇게 알림 클래스를 확장하게 되었는데 여러 유형의 알림을 동시에 보낼 수 있는 기능도 찾기 시작했다.

여러 알림 메서드를 합성한 자식 클래스들도 생성하게 되었으나 이 접근 방식은 라이브러리뿐만 아니라 사용자 코드도 엄청나게 불어날 것만 같다. 알림 클래스들의 수가 많아지지 않도록 알림 클래스를 구성하는 다른 방법이 필요한데..

어떻게 하는 게 좋을까? 😭

.

Solution

객체 동작 변경 시 가장 먼저 고려되는 방법은 클래스의 확장이다. 그러나 상속은 주의해야 할 사항들이 존재한다.

  • 상속은 정적이다

    • 런타임 때 기존 객체의 행동을 변경할 수 없고, 다른 객체로만 바꿀 수 있다.

  • 자식 클래스는 하나의 부모 클래스만 가질 수 있다.

상속 대신 집합 관계 또는 합성으로 이 문제를 해결할 수 있다.

집합 관계에서는 한 객체가 다른 객체에 대한 참조를 가지고 일부 작업을 위임하는 반면, 상속을 사용하면 객체 자체가 부모 클래스에서 행동을 상속한 후 해당 작업을 수행할 수 있다.

연결된 데코레이터 객체를 다른 객체로 대체하여 런타임 때 컨테이너의 행동을 변경할 수 있다. 이렇게 객체는 여러 클래스의 행동들을 사용할 수 있고, 여러 객체에 대한 참조들이 있으며 이 객체들에 모든 종류의 작업을 위임하게 된다. 집합 관계/합성은 데코레이터를 포함한 많은 디자인 패턴의 핵심 원칙이다.

래퍼는 데코레이터 패턴의 주요 아이디어를 표현하는 단어이다.

  • 일부 대상 객체와 연결할 수 있는 객체

  • 대상 객체와 같은 메서드들의 집합이 포함

  • 자신이 받는 모든 요청을 대상 객체에 위임

  • 요청을 대상에 전달하기 전/후에 무언가를 수행하여 결과를 변경 가능

Decorator Pattern Structure

.

Practice

Decorator Pattern Practice

.

Apply

  • 데코레이터 패턴은 객체를 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체들에 할당할 수 있어야 할 경우 사용

    • 비즈니스 로직을 계층으로 구성하고, 각 계층에 데코레이터를 생성하고 런타임에 이 로직의 다양한 조합들로 객체들을 구성

  • 상속을 사용하여 객체의 행동을 확장하는 것이 어색하거나 불가능할 경우 사용

    • final 클래스의 기존 행동들을 재사용할 수 있는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 방법

.

pros and cons

장점.

  • 새로운 자식 클래스를 만들지 않고도 객체의 행동을 확장 가능

  • 런타임에 객체로부터 책임을 추가하거나 제거 가능

  • 객체를 여러 데코레이터로 래핑하여 여러 행동들을 합성 가능

  • 단일 책임 원칙(SRP). 다양한 행동들의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 분리

단점.

  • 래퍼 스택에서 특정 래퍼를 제거하기 어려움

  • 데코레이터의 행동이 데코레이터 스택 내의 순서에 의존

  • 계층들의 초기 설정 코드가 이해하기 어려움

.

Facade

Facade

퍼사드 패턴은 라이브러리, 프레임워크 또는 다른 클래스의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적 디자인 패턴

단순한 인터페이스를 가진 클래스에서 인식될 수 있지만 대부분의 작업을 다른 클래스에 위임하고 일반적으로 사용하는 객체들의 전체 수명 주기를 관리

.

Problem

정교한 라이브러리나 프레임워크에 속한 많은 객체들의 집합으로 코드를 작동하게 만들어야 한다. 일반적으로, 이러한 객체들을 모두 초기화하고, 종속성 관계들을 추적하고, 올바른 순서로 메서드들을 실행하는 등의 작업을 수행해야 한다.

이렇게 되면 클래스의 비즈니스 로직이 외부 클래스의 구현 세부 사항들과 밀접하게 결합하여 코드를 이해하고 유지 관리하기가 어려워지는데..

어떻게 하는 게 좋을까? 😭

.

Solution

퍼사드는 클라이언트가 필요로 하는 기능들로 구성된 간단한 인터페이스를 제공하는 클래스다.

보통 다양한 기능이 있는 라이브러리와 통합해야 하지만 그 기능의 극히 일부만을 필요로 할 때 편리하다.

.

Practice

퍼사드 패턴은 복잡한 비디오 변환 프레임워크와의 상호작용을 단순화

Facade Pattern Practice

.

Apply

  • 퍼사드 패턴은 복잡한 하위 시스템에 대한 제한적이지만 간단한 인터페이스가 필요할 경우 사용

  • 퍼사드 패턴은 하위 시스템을 계층들로 구성하려는 경우 사용

.

pros and cons

장점.

  • 복잡한 하위 시스템에서 코드를 별도로 분리

단점.

  • 앱의 모든 클래스에 결합된 전지전능한 객체가 될 수 있음

.

Flyweight

Flyweight

플라이웨이트는 각 객체에 모든 데이터를 유지하는 대신 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 하는 구조 디자인 패턴

새로운 객체들 대신 캐싱 된 객체들을 반환하는 생성 메서드의 유무로 식별

.

Problem

많은 메모리를 차지하는 객체들이 대량으로 생성되어 충분하지 않은 RAM을 보유한 서버에서 부하를 불러오고 있다.

해당 서버에서도 코드가 돌아갈 수 있도록 하려면..

어떻게 하는 게 좋을까? 😭

.

Solution

고유한 상태의 상수 데이터를 객체로 분리하여 재사용한다면 중복되는 상태로 인한 메모리 차지는 줄어들 것이다.

이렇게 고유한 상태만 저장하는 객체를 플라이웨이트라고 한다.

공유한 상태 스토리지

  • 공유한 상태는 패턴을 적용하기 전에 객체들을 집합시키는 컨테이너 객체로 이동

플라이웨이트와 불변성

  • 플라이웨이트는 생성자 매개변수들을 통해 상태를 한 번만 초기화

  • setter, public 필드는 노출하지 않음

플라이웨이트 팩토리

  • 다양한 플라이웨이트에 편리하게 접근하기 위해 기존 플라이웨이트 객체들의 풀을 관리하는 팩토리 메서드를 생성

  • 클라이언트에서 원하는 상태를 조회하고 일치하는 플라이웨이트 객체를 찾으면 반환, 존재하지 않다면 새 플라이웨이트를 생성하여 풀에 추가

.

Practice

여러 객체 사이의 객체 상태를 공유

  • 다른 객체들이 공통으로 사용하는 데이터를 캐싱하여 RAM을 절약

Flyweight Pattern Practice

.

Apply

프로그램이 많은 수의 객체들을 필요로 해서 가용한 RAM이 부족할 경우 사용

  • 패턴의 효과는 사용 방법과 위치에 따라 크게 달라지며, 아래의 경우 가장 유용

    • 앱이 수많은 유사 객체들을 생성해야 할 경우

    • 객체들이 클라이언트가 사용할 수 있는 모든 RAM을 소모할 경우

    • 객체들에 여러 중복 상태들이 포함되어 있으며, 이 상태들이 추출된 후 객체 간에 공유 가능할 경우

.

pros and cons

장점.

  • 프로그램에 유사한 객체들이 많을 경우 많은 RAM 절약 가능

단점.

  • 플라이웨이트 메서드를 호출할 때마다 콘텍스트 데이터의 일부를 다시 계산해야 한다면 CPU 주기 대신 RAM을 절약하고 있을 가능성이 있음

  • 엔티티 상태 분리로 인핸 코드의 복잡성 증가

.

Proxy

Proxy

프록시는 객체에 대한 대체를 제공할 수 있는 구조 디자인 패턴

  • 프록시는 원래 객체에 대한 접근을 제어하므로, 당신의 요청이 원래 객체에 전달되기 전/후에 무언가를 수행할 수 있도록 지원

프록시는 모든 실제 작업을 다른 객체에 위임.

각 프록시 메서드는 프록시가 서비스 객체의 자식 클래스가 아닌 이상 최종적으로 서비스 객체를 참조

.

Problem

실제로 필요할 때만 객체를 만들어서 객체의 지연된 초기화를 구현하고 싶다.

그러면 모든 클라이언트는 지연된 초기화 코드를 실행해야 하는데, 이것은 많은 코드 중복을 불러올 것이다.

이 코드를 객체 클래스에 직접 넣고 싶지만 그게 항상 가능한 것은 아니다.(폐쇄된 타사 라이브러리 일부일 경우)

객체를 지연 초기화할 수 있는 방법이 없을까? 😭

.

Solution

프록시 패턴은 실제 객체와 동일한 인터페이스로 새 프록시 클래스를 생성

  • 클라이언트로부터 요청을 받으면 이 프록시는 실제 객체를 생성하고 모든 작업을 이 객체에 위임

.

Practice

프록시는 클라이언트가 사용하는 실제 서비스 객체를 대신하는 객체를 제공하는 구조 디자인 패턴

  • 프록시는 클라이언트 요청을 수신하고, 일부 작업​(접근 제어, 캐싱 등)​을 수행한 다음 요청을 실제 서비스 객체에 전달

  • 프록시 패턴은 비효율적인 타사 라이브러리에 대한 게으른 초기화 및 캐싱 구현을 지원

  • 프록시는 코드를 변경할 수 없는 클래스에 몇 가지 추가 행동들을 추가해야 할 경우 유용

Proxy Pattern Practice

.

Apply

  • 지연된 초기화​(가상 프록시)가 필요할 경우

    • 가끔 필요한 무거운 서비스 객체가 항상 가동되어 있어 시스템 자원들을 낭비하는 경우

    • 앱이 시작될 때 객체를 생성하는 대신, 객체 초기화를 실제로 초기화가 필요한 시점까지 지연

  • 접근 제어(보호 프록시)가 필요한 경우

    • 특정 클라이언트만 서비스 객체를 사용할 수 있도록 제한 하고 싶을 경우

    • 클라이언트의 자격 증명이 정해진 특정 기준과 일치하는 경우에만 서비스 객체에 요청을 전달

  • 원격 서비스의 로컬 실행(원격 프록시)이 필요할 경우

    • 서비스 객체가 원격 서버에 있는 경우

    • 프록시는 네트워크를 통해 클라이언트 요청을 전달하여 네트워크와의 작업의 모든 복잡한 세부 사항을 처리

  • 요청들의 로깅​(로깅 프록시)이 필요할 경우

    • 서비스 객체에 대한 요청들의 기록을 유지하려는 경우

    • 프록시는 각 요청을 서비스에 전달하기 전에 로깅​(기록) 가능

  • 요청 결과들의 캐싱​(캐싱 프록시)이 필요할 경우

    • 클라이언트 요청들의 결과들을 캐시하고 이 캐시들의 수명 주기를 관리해야 할 경우(특히 결과들이 상당히 큰 경우)

    • 프록시는 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱 구현 가능

    • 프록시는 요청들의 매개변수들을 캐시 키들로 사용 가능

  • 스마트 참조가 필요한 경우

    • 사용하는 클라이언트 없어 거대한 객체를 해제할 수 있어야 할 경우

    • 프록시는 서비스 객체 또는 그 결과에 대한 참조를 얻은 클라이언트 추적 가능

    • 때로 프록시는 클라이언트를 점검하여 클라이언트가 여전히 활성 상태인지를 확인 가능

    • 클라이언트 리스트가 비어 있으면 프록시는 해당 서비스 객체를 닫고 그에 해당하는 시스템 자원 확보

    • 클라이언트가 서비스 객체를 수정했는지도 추적도 가능(변경되지 않은 객체는 다른 클라이언트들이 재사용)

.

pros and cons

장점.

  • 클라이언트가 알지 못하는 상태에서 서비스 객체 제어

  • 클라이언트가 신경 쓰지 않을 때 서비스 객체의 수명 주기 관리

  • 프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동

  • 개방/폐쇄 원칙(OCP). 서비스나 클라이언트를 변경하지 않고도 새 프록시 도입 가능

단점.

  • 새로운 클래스를 많이 도입해야 하므로 코드 복잡도 증가

  • 서비스 응답이 늦어질 수 있음

.

Behavioral Design Patterns

행동 디자인 패턴은 알고리즘 및 객체 간의 책임 할당과 관련

Cain of Responsibility

Cain of Responsibility

책임 연쇄 패턴은 핸들러의 체인​​을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴

  • 각 핸들러는 요청을 받으면 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지 결정

패턴의 모든 객체는 공통 인터페이스를 따르며, 다른 객체들의 같은 메서드들을 간접적으로 호출하는 한 객체 그룹의 행동 메서드들이 있다.

.

Problem

인증된 사용자들만 주문을 생성할 수 있도록 시스템에 대한 접근을 제한하고 싶다. 또 관리 권한이 있는 사용자들에게는 모든 주문에 대한 전체 접근 권한을 부여하려고 한다.

하지만, 이러한 검사들은 차례대로 수행해야 한다. 사용자들의 자격 증명이 포함된 요청을 받을 때마다 시스템에 대해 사용자 인증을 시도할 수 있으나, 이러한 자격 증명이 올바르지 않아서 인증에 실패하면 다른 검사들을 진행할 필요가 없다.

검사 항목에 새로운 기능이 추가될 때마다 코드는 부풀고 복잡해지기 시작했다.

각 검사들을 독립적이면서 연쇄적으로 동작하도록 할 수 있는 방법이 없을까? 😭

.

Solution

Cain of Responsibility(책임 연쇄) 패턴은 특정 행동들을 핸들러라는 독립 실행형 객체들로 변환한다.

  • 각 검사는 검사를 수행하는 단일 메서드가 있는 자체 클래스로 추출되고, 요청은 데이터와 함께 이 메서드에 인수로 전달된다.

이 패턴은 이러한 핸들러를 체인으로 연결하도록 제안한다.

  • 연결된 각 핸들러에는 체인의 다음 핸들러에 대한 참조를 저장하기 위한 필드가 있다.

  • 요청을 처리하는 것 외에도 핸들러들은 체인을 따라 요청을 전달하며, 이 요청은 모든 핸들러가 요청을 처리할 기회를 가질 때까지 체인을 따라 이동한다.

  • 또한 핸들러가 요청을 전달하지 않고 중지할 수도 있다.

.

Practice

사용자 데이터가 포함된 요청이 인증, 권한 부여와 같은 다양한 작업을 수행하는 핸들러의 순차적 체인을 통과하는 과정

Cain of Responsibility Pattern Practice

.

Apply

  • 프로그램이 다양한 방식으로 다양한 종류의 요청들을 처리할 것으로 예상되지만 정확한 요청 유형들과 순서들을 미리 알 수 없는 경우

  • 특정 순서로 여러 핸들러를 실행해야 할 경우

  • 핸들러들의 집합과 그들의 순서가 런타임에 변경되어야 할 경우

.

pros and cons

장점.

  • 요청의 처리 순서를 제어 가능

  • 단일 책임 원칙(SRP). 작업을 호출하는 클래스와 작업을 수행하는 클래스 분리

  • 개방/폐쇄 원칙(OCP). 기존 클라이언트 코드를 손상시키지 않고 새 핸들러 도입 가능

단점.

  • 일부 요청들은 처리되지 않을 수 있음

.

Command

Command

요청에 대한 모든 정보를 포함하는 독립형 개체로 변환하는 동작 설계 패턴

  • 이 변환을 통해 요청을 메서드 인수로 전달하고, 요청의 실행을 지연하거나 대기열에 놓이며, 실행 취소 가능한 작업을 지원

특정 작업(복사, 잘라내기, 보내기, 인쇄 등)을 나타내는 관련 클래스 집합이 표시되는 경우 이는 Command 패턴일 수 있다.

  • 이러한 클래스는 동일한 인터페이스/추상 클래스를 구현 필요.

  • 명령어는 관련 작업을 자체적으로 구현하거나 작업을 개별 개체(수신기)에 위임 가능.

  • 마지막 퍼즐 조각은 호출자를 식별하는 것으로, 메서드 또는 생성자의 매개 변수에서 명령 개체를 받아들이는 클래스를 검색

.

Problem

텍스트 편집기 앱에서 작업하고 있다. 편집기의 다양한 작업을 위한 수많은 버튼이 있는 도구 모음을 만드는 작업이다. 도구 모음의 버튼뿐만 아니라 다양한 대화상자의 일반 버튼에도 사용할 수 있는 매우 깔끔한 버튼 클래스를 만들었다.

모두 다른 기능을 수행하는 비슷한 버튼들이 생겨나는데, 점점 많은 수의 자식 클래스들이 생겨나고 부모 클래스를 수정할 때마다 자식 클래스의 코드를 깨뜨릴 위험이 생겨나고 있다. 같은 기능을 구현하는 코드들도 생겨나고 구현하지 않아도 되는 기능까지 구현하게 되고 말았다.

이 문제를 효율적으로 해결할 수 있는 방법이 없을까? 😭

.

Solution

커맨드 패턴은 GUI 객체들이 요청을 직접 보내서는 안되고, 모든 요청 세부 정보들​(호출되는 객체, 메서드 이름 및 인수 리스트)​을 단일 메서드를 가진 별도의 커맨드 클래스로 추출하라고 제안

  • 커맨드 객체는 다양한 GUI 객체들과 비즈니스 논리 객체들 간의 링크 역할

  • GUI 객체는 어떤 비즈니스 논리 객체가 요청을 받을지와 이 요청이 어떻게 처리할지에 대하여 알 필요가 없음

  • GUI 객체는 커맨드를 작동시킬 뿐이며, 그렇게 작동된 커맨드는 모든 세부 사항을 처리

Solution

Structure

.

Practice

요청 또는 간단한 작업을 객체로 변환하는 행동 디자인 패턴

Command Pattern Practice

.

Apply

  • 작업들로 객체를 매개 변수화할 경우

  • 작업들의 실행을 예약하거나, 대기열에 넣거나, 원격으로 실행하려는 경우

  • 되돌릴 수 있는 작업을 구현하고 싶을 경우

.

pros and cons

장점.

  • 단일 책임 원칙(SRP). 작업을 호출하는 클래스를 작업을 수행하는 클래스들로부터 분리

  • 개방/폐쇄 원칙(OCP). 기존 클라이언트 코드를 손상시키지 않고 새로운 커맨드 도입 가능

  • 실행 취소, 다시 실행 구현 가능

  • 작업 지연 실행 구현 가능

  • 간단한 커맨드 집합을 복잡한 커맨드로 조합 가능

단점.

  • 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하므로 코드 복잡도 증가

.

Iterator

Iterator

반복자는 컬렉션 요소들의 기본 표현​(리스트, 스택, 트리 등)​을 노출하지 않고 순회할 수 있도록 하는 행동 디자인 패턴

탐색 메서드(next, previous..)​로 쉽게 인식 가능

반복자를 사용하는 클라이언트 코드는 반복자가 순회하는 컬렉션에 직접 접근할 수 없음

.

Problem

컬렉션은 요소에 접근할 수 있는 방법을 클라이언트에 제공해야 한다.

컬렉션마다 요소에 접근할 수 있는 방법이 다른데..

단순하게 접근할 수 있도록 하는 방법이 없을까? 😭

.

Solution

Iterator 패턴을 통해 컬렉션의 순회 동작을 Iterator라는 별도의 객체로 추출할 수 있다.

  • 반복자는 다양한 순회 알고리즘을 구현

  • 여러 반복자 객체들이 동시에 같은 컬렉션 순회 가능

.

Practice

Practice Pattern Practice

.

Apply

  • 컬렉션 내부에 복잡한 데이터 구조가 있지만 이 구조의 복잡성을 보안이나 편의상의 이유로 클라이언트로부터 숨기고 싶을 경우

  • 순회 코드의 중복을 줄이고 싶을 경우

  • 다른 데이터 구조를 순회하고 싶거나, 데이터 구조 유형을 미리 알 수 없을 경우

.

pros and cons

장점.

  • 단일 책임 원칙(SRP). 부피가 큰 순회 알고리즘을 별도의 클래스들로 추출하여 클라이언트 코드와 컬렉션을 분리

  • 개방/폐쇄 원칙(OCP). 새로운 유형의 컬렉션과 반복자를 구현할 수 있고, 기존의 코드 수정 없이 전달 가능

  • 같은 컬렉션을 병렬로 순회 가능. (각 반복자 객체에는 자신의 고유한 순회 상태가 포함)

    • 순회를 지연하고 필요할 때 계속할 수 있음

단점.

  • 단순한 컬렉션하고만 작동하는 경우 반복자 패턴을 적용하는 것은 과도할 수 있음

  • 반복자를 사용하는 것은 일부 특수 컬렉션들의 요소들을 직접 탐색하는 것보다 효율적이지 않을 수 있음

.

Mediator

Mediator

중재자는 객체 간의 혼란스러운 의존 관계들을 줄일 수 있는 행동 디자인 패턴

  • 객체 간의 직접 통신을 제한하고 중재자 객체를 통해서만 협력

중재자 패턴의 가장 인기 있는 사용 용도는 GUI 컴포넌트 간의 통신을 쉽게 하는 것

  • MVC 패턴의 컨트롤러 동의어는 중재자

.

Solution

중재자 패턴은 서로 독립적으로 작동해야 하는 컴포넌트 간의 모든 직접 통신을 중단한 후, 컴포넌트의 호출을 적절한 컴포넌트로 리다이렉션하는 중재자 객체로 호출하여 간접적으로 협력하도록 제안

  • 컴포넌트들은 수십 개의 동료 컴포넌트와 결합되는 대신 단일 중재자 클래스에만 의존

  • 클래스의 의존관계가 적을수록 해당 클래스의 수정, 확장, 재사용이 쉬워짐

.

Practice

많은 GUI 요소들이 중재자의 도움으로 협력하지만, 서로에 의존하지 않도록 해당 요소들을 정리하는 방법을 표현

Mediator Pattern Practice

.

Apply

  • 일부 클래스가 다른 클래스와 강하게 결합되어 변경하기 어려울 경우

  • 다른 컴포넌트에 의존하여 다른 서비스에서 컴포넌트를 재사용할 수 없는 경우

  • 몇가지 기능들을 여러 컨텍스트에서 재사용하기 위해 자식 클래스를 많이 만들고 있을 경우

.

pros and cons

장점.

  • 단일 책임 원칙(SRP). 다양한 컴포넌트 간의 통신을 한곳으로 추출하여 코드를 이해하고 유지 관리하기 쉽움

  • 개방/폐쇄 원칙(OCP). 실제 컴포넌트들을 변경하지 않고도 새로운 중재자 도입 가능

  • 프로그램의 다양한 컴포넌트 간의 결합도를 줄일 수 있음

  • 개별 컴포넌트들을 쉽게 재사용 가능

단점.

  • 전지전능한 객체로 발전할지도 모름

.

Memento

Memento

메멘토는 객체의 세부 구현을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴

직렬화를 사용하여 달성 가능.

직렬화는 객체 상태의 스냅숏을 만드는 유일한 또는 가장 효율적인 방법은 아니나, 다른 객체로부터 구조를 보호하면서 상태 백업을 저장

.

Problem

클래스의 세부 정보를 모두 공개하면 클래스가 너무 취약해 진다.

하지만 클래스의 상태에 접근하지 못하게 하면 스냅샷을 생성할 수 없게 된다.

그러면.. 실행 취소 기능은 어떻게 구현해야 할까? 😭

.

Solution

메멘토는 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 originator 객체에 위임한다.

  • 그러면 다른 객체들이 외부에서 편집기의 상태를 복사하려 시도하는 대신,

  • 자신의 상태에 대해 완전한 접근 권한을 갖는 클래스가 자체적으로 스냅샷을 생성할 수 있다.

메멘토 패턴메멘토라는 특수 객체에 객체 상태의 복사본을 저장하라고 제안한다.

  • 메멘토의 내용에는 메멘토를 생성한 객체를 제외한 다른 어떤 객체도 접근할 수 없다.

  • 다른 객체들은 메멘토와 제한된 인터페이스를 사용해 통신

  • 이러한 인터페이스는 스냅샷의 메타데이터​(생성 시간, 수행한 작업의 이름 등)​를 가져올 수 있도록 할 수 있지만, 스냅샷에 포함된 원래 객체의 상태는 가져오지 못한다.

중첩된 클래스에 기반한 구현

  • 중첩 클래스에 대한 지원에 의존

엄격한 캡슐화를 사용한 구현

  • 다른 클래스들이 오리지네이터의 상태를 메멘토를 통해 접근할 가능성을 완전히 제거하고자 할 때 유용

.

Practice

Memento Pattern Practice

.

Apply

  • 객체의 이전 상태를 복원할 수 있도록 객체의 상태의 스냅샷을 생성하려는 경우

    • 트랜잭션을 처리할 때​(오류 발생 시 작업 롤백)​도 사용 가능한 패턴

  • 객체의 필드/게터/세터를 직접 접근하는 것이 해당 객체의 캡슐화를 위반할 경우

    • 메멘토는 객체 자신이 자신의 상태를 스냅샷으로 만드는 역할

    • 다른 객체는 스냅샷을 읽을 수 없으므로 원래 객체의 상태 데이터는 안전

.

pros and cons

장점.

  • 캡슐화를 위반하지 않고, 객체 상태의 스냅샷을 생성

  • Caretacker가 Originator의 상태 기록을 유지하도록 하여 Originator의 코드 단순화

단점.

  • 클라이언트가 메멘토를 너무 자주 생성하면 많은 RAM 소모

  • Caretacker들은 더 이상 쓸모없는 메멘토들을 파괴할 수 있도록 Originator의 수명주기 추적 필요

  • PHP, Python, JavaScript 같은 대부분의 동적 프로그래밍 언어에서는 메멘토 상태가 그대로 유지된다고 보장 불가

.

Observer

Observer

옵서버 패턴은 관찰 중인 개체에 발생하는 이벤트에 대해 여러 개체에 통지하는 구독 메커니즘을 정의할 수 있는 행동 디자인 패턴

수신 개체를 목록에 저장하는 구독 방법이 나타나면 패턴을 인식 가능

해당 목록의 개체에 대해 반복되는 일종의 알림 방법을 보고 "업데이트" 방법을 호출하면 식별 확인 가능

.

Problem

매장에서는 새로운 제품이 출시될 때마다 모든 고객에게 스팸으로 간주할 수 있는 이메일을 보낼 수 있다.

이 이메일은 일부 고객들이 신제품 출시 확인을 위한 잦은 매장 방문으로부터 구해낼 수 있으나, 동시에 신제품 출시에 관심이 없는 다른 고객들을 화나게 할 것이다.

모두가 만족할 수 있는 방법은 없을까? 😭

.

Solution

옵서버 패턴은 Publisher 클래스(자신의 상태에 대한 변경에 대해 다른 객체들에 알림을 보내는 역할)에 개별 객체들이 그 Publisher로부터 오는 이벤트들의 알림들을 구독 또는 구독 취소할 수 있도록 구독 메커니즘을 추가할 것을 제안

Publisher에 중요한 이벤트가 발생할 때마다 구독자 리스트를 참조한 후 그들의 객체들에 있는 특정 알림 메서드를 호출

.

Practice

Observer Pattern Practice

.

Apply

  • 한 객체의 상태가 변경되어 다른 객체를 변경해야 할 필요성이 생겼을 경우. 그리고 실제 객체 집합을 미리 알 수 없거나 이러한 집합들이 동적으로 변경될 경우

  • 앱의 일부 객체들이 제한된 시간 동안 또는 특정 경우에만 다른 객체들을 관찰해야 할 경우

    • 구독 리스트는 동적이므로 구독자들은 필요할 때마다 리스트에 가입하거나 탈퇴할 수 있음

.

pros and cons

장점.

  • 개방/폐쇄 원칙(OCP). 출판사의 코드를 변경하지 않고도 새 구독자 클래스 도입 가능

    • 출판사 인터페이스가 있는 경우 그 반대로 구독자 클래스를 변경하지 않고 새 출판사 클래스 도입 가능

  • 런타임에 객체 간의 관계들을 형성 가능

단점.

  • 구독자들은 무작위로 알림을 받음

.

State

State

상태 패턴은 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 하는 행동 디자인 패턴

  • 객체가 행동을 변경할 때 객체가 클래스를 변경한 것처럼 보일 수 있음

개체의 상태에 따라 동작을 변경하는 방법으로 인식 가능

상태 개체 자체를 포함한 다른 개체가 이 상태를 제어하거나 대체할 수 있는 경우 식별 가능

.

Problem

상태 머신들은 일반적으로 객체의 상태에 따라 적절한 행동들을 선택하는 많은 조건문​(if 또는 switch)​으로 구현된다. 일반적으로 이 상태는 객체의 필드들의 값들의 집합일 뿐이다.

ex. 유한 상태 머신 구현 예

class Document is
    field state: string
    // ...
    method publish() is
        switch (state)
            "draft":
                state = "moderation"
                break
            "moderation":
                if (currentUser.role == "admin")
                    state = "published"
                break
            "published":
                // Do nothing.
                break
    // ...

이렇게 조건문에 기반한 상태 머신은 클래스의 상태와 상태에 의존하는 행동이 추가될 수록 조건문이 점점 거대해질 것이다.

설계 단계에서는 가능한 모든 상태와 상태의 규칙을 예상하기 어려운데.. 좋은 방법이 없을까? 😭

.

Solution

상태 패턴은 가능한 모든 상태를 클래스로 만들고 모든 상태별 행동을 해당 클래스들로 추출할 것을 제안한다.

콘텍스트 객체는 모든 행동을 자체적으로 구현하는 대신 현재 상태를 나타내는 상태 객체 중 하나에 대한 참조를 저장하고 모든 상태와 관련된 작업을 상태 객체에 위임한다.

콘텍스트를 다른 상태로 전환하려면 상태 객체를 다른 객체로 변경하기만 하면 된다.

이 구조는 전략 패턴과 비슷해 보이지만

  • 상태 패턴는 특정 상태들이 서로를 인식하고, 한 상태에서 다른 상태로 전환될 수 있지만

  • 전략 패턴은 상태들이 대부분 서로에 대해 알지 못한다.

.

Practice

상태 패턴은 같은 미디어 플레이어 컨트롤들이 현재 재생 상태에 따라 다르게 작동하도록 한다.

  • 플레이어의 기본 클래스에는 상태 객체에 대한 참조가 포함되며, 이 객체는 플레이어를 위해 대부분 작업을 수행

  • 일부 작업은 상태 객체를 다른 객체로 대처할 수 있으며, 이는 플레이어가 사용자 상호 작용에 반응하는 방식을 변경

State Pattern Practice

.

Apply

  • 상태에 따라 다르게 행동하는 객체 많고, 상태별 코드가 자주 변경될 경우

  • 클래스 필드의 값에 따라 클래스가 행동하는 방식을 변경하는 거대한 조건문으로 오염되어 있을 경우

  • 유사한 상태들에 중복 코드와 조건문 기반 상태 머신의 천이가 많을 경우

.

pros and cons

장점.

  • 단일 책임 원칙(SRP). 특정 상태와 관련된 코드를 별도 클래스로 구성

  • 개방/폐쇄 원칙(OCP). 상태 클래스, 콘텍스트를 변경하지 않고 새로운 상태 도입 가능

  • 거대한 상태 머신 조건문을 제거하여 콘텍스트의 코드를 단순화

단점.

  • 상태 머신에 몇 가지 상태만 있거나 머신이 거의 변경되지 않을 경우 상태 패턴을 적용하는 것은 과도할 수 있음

.

Strategy

Strategy

전략 패턴은 알고리즘의 전략을 정의하고, 각 전략을 별도의 클래스에 넣은 후 그들의 객체를 상호교환할 수 있도록 하는 행동 디자인 패턴

전략 패턴은 중첩된 객체가 실제 작업을 수행할 수 있는 방법뿐만 아니라 해당 객체를 다른 객체로 대체할 수 있는 setter를 통해 인식 가능

.

Problem

알고리즘을 추가할 때마다 메인 클래스의 크기가 두 배로 늘어나고, 간단한 버그를 수정하거나 기능을 변경하면 전체 클래스에 영향이 미쳐 이미 작동하는 코드에서 오류가 발생할 가능성이 높아졌습니다.

그리고 새로운 기능을 구현하려면 거대한 클래스를 변경해야 하는데 이렇게 변경한 내용들이 다른 코드와의 병합 충돌로 많이 시간이 낭비되고 있습니다.

이 문제를 효율적으로 해결할 수 있는 방법은 없을까? 😭

.

Solution

전략 패턴은 특정 작업을 다양한 방식으로 수행하는 클래스를 선택한 후 모든 알고리즘을 strategies 클래스들로 추출할 것을 제안

  • context라는 원래 클래스에는 전략 중 하나에 대한 참조를 저장하기 위한 필드가 필요

  • context는 작업을 자체적으로 실행하는 대신 연결된 전략 객체에 위임

context는 작업에 적합한 알고리즘을 선택할 책임이 없음(전략에 대해 알지 못함)

  • 대신 클라이언트가 원하는 전략을 context에 전달

  • context는 동일 인터페이스를 구현한 전략들과 함께 작동하며, 이 인터페이스는 선택된 전략 내에 캡슐화된 알고리즘을 작동시킬 단일 메서드만 노출

전략 패턴을 통해 context가 구상 전략들에 의존하지 않게 되므로 context 또는 다른 전략들의 코드를 변경하지 않고도 새 알고리즘들을 추가하거나 기존 알고리즘들을 수정할 수 있다.

.

Practice

전자 상거래 앱의 다양한 지불 메서드들을 구현하는 예시.

  • 구매할 제품을 선택한 후 고객은 페이팔 또는 신용 카드 지불 메서드를 선택

  • 구상 전략들은 실제 지불을 수행할 뿐만 아니라 체크아웃 양식의 행동을 변경하여 지불 세부 정보들을 기록하는 적절한 필드들을 제공

Strategy Pattern Practice

.

Apply

  • 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 경우.

    • 그리고 런타임 중 다른 알고리즘으로 전환하고 싶을 경우

  • 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스가 많은 경우

  • 콘텍스트에서 알고리즘 구현 세부 사항을 분리하고 싶을 경우

  • 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 클래스에 있는 경우

.

pros and cons

장점.

  • 런타임에 객체 내부에서 사용되는 알고리즘 변경 가능

  • 알고리즘을 사용하는 코드와 세부 구현의 분리

  • 상속을 합성으로 대체

  • 개방/폐쇄 원칙(OCP). 콘텍스트를 변경하지 않고도 새로운 전략 도입 가능

단점.

  • 사용되는 알고리즘이 적고 변경될 일이 없다면, 패턴과 함께 사용되는 새로운 클래스와 인터페이스로 복잡성 증가

  • 클라이언트는 적절한 전략을 선택할 수 있도록 전략 간의 차이점들을 알아야 함

  • 패턴 사용을 위해 클래스와 인터페이스를 추가하지 않고 함수형으로 처리 가능

.

Template Method

Template Method

템플릿 메서드는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드​할 수 있도록 하는 행동 디자인 패턴

템플릿 메서드는 기본 클래스에서 추상적이거나 비어 있는 다른 메서드를 호출하는 메서드가 존재

Problem

작업을 처리하는 클래스에 따라 행동을 선택하는 조건문이 많이 있다. 처리 클래스에 공통 인터페이스나 공통 클래스를 적용했더라면 클라이언트 코드에서 조건문을 제거하고 처리 객체에 메서드를 호출할 때 다형성을 사용할 수 있었을 것이다.

이 상황을 효율적으로 해결할 수 있는 방법은 없을까? 😭

.

Solution

템플릿 메소드 패턴은 알고리즘을 일련의 단계로 분해하고, 이 단계를 메소드로 변환하고, 이러한 메소드에 일련의 호출을 하나의 템플릿 메소드 안에 넣는 것을 제안합니다.

  • 알고리즘을 사용하려면 클라이언트는 자신의 하위 클래스를 제공하고, 모든 추상적인 단계를 구현하며, 필요한 경우 선택적인 단계 중 일부를 재정의

.

Practice

Template Method Pattern Practice

.

Apply

  • 클라이언트가 알고리즘의 특정 단계들만 확장할 수 있도록 하고 싶고, 전체 알고리즘이나 알고리즘 구조는 확장하지 못하도록 하고 싶을 경우

    • 부모 클래스에서 정의된 구조를 그대로 유지하면서 자식 클래스로 쉽게 확장 가능

  • 유사한 알고리즘을 포함하는 여러 클래스가 있는 경우 (결과적으로 알고리즘이 변경되면 모든 클래스를 수정해야 하는 경우)

.

pros and cons

장점.

  • 클라이언트가 대규모 알고리즘의 특정 부분만 오버라이드하도록 하여 알고리즘의 다른 부분에 발생하는 변경에 영향을 덜 받도록 할 수 있음

  • 중복 코드를 부모 클래스로 이전 가능

단점.

  • 일부 클라이언트는 알고리즘의 제공된 골격에 제한

  • 자식 클래스를 통해 디폴트 단계 구현을 억제하여 리스코프 치환 원칙 위반 가능

  • 템플릿 메서드에 단계가 많을수록 유지가 더 어려운 경향 존재

.

Visitor

Visitor

비지터 패턴은 알고리즘이 작동하는 객체에서 알고리즘을 분리할 수 있는 행동 디자인 패턴

기존 코드를 변경하지 않고 기존 클래스 계층구조에 새로운 행동들을 추가 가능

.

Problem

그래프 데이터를 XML 형식으로 내보내는 작업을 구현해야 한다. 노드 클래스에 해당 기능을 추가하려고 했지만, 노드 클래스에 XML 내보내기 기능을 넣을 수 없는 상황이다. 또한, 데이터를 XML 형식이 아닌 다른 형식으로 내보내는 기능이 추가될 가능성이 있다고 한다. 그러면 새로운 기능이 추가될 떄마다 클래스를 변경해야 하는데..

이 상황을 효율적으로 해결할 수 있는 방법은 없을까? 😭

.

Solution

비지터 패턴은 새로운 기능을 기존 클래스들에 통합하는 대신 visitor​라는 별도의 클래스에 배치할 것을 제안합니다. 기능을 수행해야 했던 원래 객체는 visitor 메서드에 인수로 전달하게 됩니다. 그러면 메서드는 원래 객체 내에 포함된 모든 필요한 데이터에 접근할 수 있습니다.

.

Practice

계층구조의 클래스의 기존 코드를 변경하지 않고 해당 계층구조에 모든 행등을 추가할 수 있도록 구성

  • 객체를 인수로 전달하여 해당 객체의 데이터에 접근

Visitor Pattern Practice

.

Apply

  • 트리와 같은 복잡한 객체 구조​의 모든 요소에 대한 작업이 필요할 경우

    • 같은 작업의 여러 변형을 구현하도록 하면서 다양한 클래스를 가진 여러 객체의 집합에 작업을 실행할 수 있도록 지원

  • 보조 기능에 대한 비즈니스 로직을 비지터 클래스의 집합으로 분리

  • 특정 기능이 클래스 계층구조의 일부 클래스에서만 의미가 있고 다른 클래스에서는 의미가 없을 경우

.

pros and cons

장점.

  • 개방/폐쇄 원칙(OCP). 다른 클래스를 변경하지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 행동 추가 가능

  • 단일 책임 원칙(SRP). 동일한 기능의 여러 버전을 같은 클래스로 이동 가능

  • 비지터 객체는 다양한 객체들과 작업하면서 유용한 정보를 축적 가능.

    • 객체 트리와 같은 복잡한 객체 구조를 순회하여 각 객체에 비지터 패턴을 적용하려는 경우 유용

단점.

  • 클래스가 계층구조에 추가/제거될 때마다 모든 비지터에 대한 업데이트 필요

  • 비지터들은 함께 작업해야 하는 요소의 비공개 필드/메서드에 접근하기 위한 권한이 부족할 수 있음

Last updated