안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 구조 패턴(Structural Patterns)의 한 종류로, 기존 코드를 수정하지 않고도 객체에 새로운 기능을 덧붙일 수 있게 해주는 데코레이터(Decorator) 패턴에 대해 알아보겠습니다.


정의

데코레이터 패턴객체에 동적으로 새로운 책임(기능)을 추가할 수 있게 해주는 패턴입니다. 기능을 추가하기 위해 서브클래싱(상속)을 사용하는 것보다 더 유연한 대안이 될 수 있습니다. 이름 그대로 객체를 장식(Decorate)하는 것처럼 기능을 덧씌우는 방식입니다.

 

가장 쉬운 예는 커피 주문입니다. 기본 '아메리카노'가 있습니다. 여기에 '우유 추가', '모카 시럽 추가', '휘핑 크림 추가'와 같은 옵션들을 원하는 대로 선택하여 추가할 수 있습니다. 각 옵션(데코레이터)은 기존 커피에 새로운 맛과 가격을 더합니다. '우유를 추가한 아메리카노'에 다시 '모카 시럽'을 추가할 수도 있죠. 이처럼 객체를 여러 겹으로 감싸면서 점진적으로 기능을 확장하는 것이 데코레이터 패턴의 핵심입니다.

 

데코레이터 패턴은 다음과 같이 이루어져 있습니다.

  • Component (컴포넌트): 장식될 기본 객체와 데코레이터들이 공통으로 구현할 인터페이스입니다. (예: Coffee)
  • ConcreteComponent (구체적인 컴포넌트): 장식의 대상이 되는 핵심 기능을 가진 기본 객체입니다. (예: Americano)
  • Decorator (데코레이터): Component 인터페이스를 구현하면서, 내부에 또 다른 Component 객체에 대한 참조를 가집니다. 이 참조가 바로 자신이 장식할 객체입니다.
  • ConcreteDecorator (구체적인 데코레이터): Decorator의 서브클래스로, 실제로 새로운 책임(기능)을 추가하는 역할을 합니다. (예: WithMilk, WithMocha)

⌨️ 예제 코드 (Example)

앞서 설명한 커피 주문 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 컴포넌트 (Component) 인터페이스 정의

모든 커피 메뉴가 공통으로 가질 기능(가격 계산, 설명)의 인터페이스를 정의합니다.

// Component 인터페이스
public interface Coffee {
    double getCost();
    String getDescription();
}

2. 구체적인 컴포넌트 (ConcreteComponent) 클래스 구현

기본 커피인 Americano 클래스를 구현합니다.

// ConcreteComponent 클래스
public class Americano implements Coffee {
    @Override
    public double getCost() {
        return 4.0; // 기본 가격 4.0
    }

    @Override
    public String getDescription() {
        return "아메리카노";
    }
}

3. 데코레이터 (Decorator) 추상 클래스 구현

모든 데코레이터의 기반이 될 추상 클래스를 정의합니다.

// Decorator 추상 클래스
public abstract class CoffeeDecorator implements Coffee {
    // 자신이 장식할 대상(Component)
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    // 기본적으로는 장식 대상에게 위임
    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

4. 구체적인 데코레이터 (ConcreteDecorator) 클래스 구현

실제 추가 옵션인 WithMilkWithMocha를 구현합니다.

// ConcreteDecorator A: 우유 추가
public class WithMilk extends CoffeeDecorator {
    public WithMilk(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        // 기본 가격에 우유 가격 추가
        return super.getCost() + 0.5;
    }

    @Override
    public String getDescription() {
        // 기본 설명에 "우유 추가" 덧붙이기
        return super.getDescription() + ", 우유 추가";
    }
}

// ConcreteDecorator B: 모카 시럽 추가
public class WithMocha extends CoffeeDecorator {
    public WithMocha(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        // 기본 가격에 모카 시럽 가격 추가
        return super.getCost() + 0.7;
    }

    @Override
    public String getDescription() {
        // 기본 설명에 "모카 시럽 추가" 덧붙이기
        return super.getDescription() + ", 모카 시럽 추가";
    }
}

5. 클라이언트 (Client) 코드

클라이언트는 기본 커피에 원하는 데코레이터를 여러 겹으로 감싸서 사용합니다.

public class CoffeeShop {
    public static void main(String[] args) {
        // 1. 기본 아메리카노 주문
        Coffee coffee = new Americano();
        System.out.println("주문: " + coffee.getDescription() + " / 가격: " + coffee.getCost());

        // 2. 아메리카노에 우유 추가
        coffee = new WithMilk(coffee);
        System.out.println("주문: " + coffee.getDescription() + " / 가격: " + coffee.getCost());
        
        // 3. 우유 추가된 커피에 모카 시럽도 추가
        coffee = new WithMocha(coffee);
        System.out.println("주문: " + coffee.getDescription() + " / 가격: " + coffee.getCost());

        // 실행 결과:
        // 주문: 아메리카노 / 가격: 4.0
        // 주문: 아메리카노, 우유 추가 / 가격: 4.5
        // 주문: 아메리카노, 우유 추가, 모카 시럽 추가 / 가격: 5.2
    }
}


 

결론

  • 상속을 사용하지 않고, 객체에 동적으로 새로운 기능을 추가하거나 제거하고 싶을 때
  • 많은 수의 독립적인 확장 기능이 있고, 이들의 조합이 매우 다양할 때: 상속으로 모든 조합을 만들면 클래스가 폭발적으로 증가하지만, 데코레이터는 필요한 만큼만 조합하여 사용할 수 있습니다.
  • 기존 코드를 수정하는 것이 불가능할 때(OCP 준수): 원본 클래스를 변경하지 않고 래퍼(Wrapper) 클래스를 사용하여 기능을 확장할 수 있습니다.

중첩적이고 Wrapper를 이용해 높은 독립성을 유지할 수 있지만 다음의 사항을 고려해야합닝다

  • 많은 수의 작은 객체들: 데코레이터를 많이 사용할수록 관리해야 할 작은 클래스들이 많아져 시스템이 복잡해 보일 수 있습니다.
  • 디버깅의 어려움: 객체가 여러 데코레이터에 감싸여 있을 때, 특정 데코레이터의 동작을 추적하고 디버깅하기가 까다로울 수 있습니다.
  • 특정 컴포넌트의 타입을 확인하기 어려움: 장식된 객체는 데코레이터에 의해 감싸여 있어, 원래의 ConcreteComponent가 무엇이었는지 알기 어렵습니다.

데코레이터 패턴은 객체의 핵심 기능은 그대로 둔 채, 필요한 기능들을 마치 옷을 겹쳐 입듯이 유연하게 추가할 수 있게 해주는 강력한 패턴입니다. Java의 I/O 클래스(FileInputStream, BufferedInputStream 등)가 이 패턴의 대표적인 활용 사례입니다. 다음 시간에는 Flyweight 패턴에 대해서 알아보겠습니다.

안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 구조 패턴(Structural Patterns)의 한 종류로, 개별 객체와 객체의 집합을 동일한 방식으로 다룰 수 있게 해주는 컴포지트(Composite) 패턴에 대해 알아보겠습니다.


정의

컴포지트 패턴객체들을 트리 구조로 구성하여, '부분-전체' 계층을 표현하는 패턴입니다. 이 패턴을 사용하면 클라이언트는 개별 객체(Leaf)와 복합 객체(Composite)를 동일하게 취급할 수 있습니다.

가장 대표적인 예는 컴퓨터의 파일 시스템입니다.

  • 파일(File) 은 개별 객체입니다.
  • 폴더(Folder)는 다른 파일이나 또 다른 폴더들을 담을 수 있는 복합 객체입니다.

우리는 파일의 크기를 확인하거나 폴더의 전체 크기를 확인할 수 있습니다. 이때 폴더의 크기는 그 안에 포함된 모든 파일과 하위 폴더들의 크기를 합한 값이죠. 컴포지트 패턴을 사용하면, 클라이언트는 '파일'이든 '폴더'든 구분하지 않고 "크기를 알려줘"라는 동일한 요청을 보낼 수 있습니다. 이처럼 단일 객체와 객체 집합에 대해 동일한 인터페이스를 사용하게 하여 클라이언트 코드를 단순화하는 것이 이 패턴의 핵심입니다.

  • Component (컴포넌트): LeafComposite를 아우르는 공통 인터페이스입니다. 트리 내의 모든 객체들이 구현해야 할 메서드(예: operation())를 선언합니다. 또한, 자식을 관리하는 메서드( add(), remove() 등)를 선언할 수도 있습니다.)
  • Leaf (리프): 트리의 가장 말단에 있는 개별 객체입니다. 자식을 가지지 않으며, Component 인터페이스의 메서드를 구현합니다. (예: File)
  • Composite (컴포지트): 자식(Component)들을 가지는 복합 객체입니다. Component 인터페이스의 메서드를 구현하며, 보통 자신의 자식들에게 요청을 재귀적으로 전달하여 처리합니다. (예: Folder)
  • Client (클라이언트): Component 인터페이스만을 사용하여 트리 구조의 객체들을 다룹니다.

사용예시

앞서 설명한 파일 시스템 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 컴포넌트 (Component) 인터페이스 정의

파일과 디렉토리(폴더)가 공통으로 가질 기능의 인터페이스를 정의합니다.

// Component 인터페이스
public interface FileSystemItem {
    String getName();
    int getSize(); // 파일 크기 또는 디렉토리 총 크기
    void print(); // 계층 구조 출력
}

2. 리프 (Leaf) 클래스 구현

개별 객체인 File 클래스를 구현합니다.

// Leaf 클래스
public class File implements FileSystemItem {
    private String name;
    private int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() { return name; }

    @Override
    public int getSize() { return size; }

    @Override
    public void print() { System.out.println("- " + getName() + " (size: " + getSize() + "KB)"); }
}

 

3. 컴포지트 (Composite) 클래스 구현

다른 FileSystemItem들을 자식으로 가질 수 있는 Directory 클래스를 구현합니다.

import java.util.ArrayList;
import java.util.List;

// Composite 클래스
public class Directory implements FileSystemItem {
    private String name;
    private List<FileSystemItem> children = new ArrayList<>();

    public Directory(String name) { this.name = name; }

    public void add(FileSystemItem item) { children.add(item); }

    public void remove(FileSystemItem item) { children.remove(item); }

    @Override
    public String getName() { return name; }

    @Override
    public int getSize() {
        // 재귀적으로 모든 자식들의 크기를 합산하여 반환
        int totalSize = 0;
        for (FileSystemItem item : children) {
            totalSize += item.getSize();
        }
        return totalSize;
    }

    @Override
    public void print() {
        System.out.println("D " + getName() + " (total size: " + getSize() + "KB)");
        // 자식들을 순회하며 print() 호출
        for (FileSystemItem item : children) {
            item.print();
        }
    }
}

4. 클라이언트 (Client) 코드

클라이언트는 FileSystemItem 인터페이스만으로 파일과 디렉토리를 동일하게 다룹니다.

public class Client {
    public static void main(String[] args) {
        // 루트 디렉토리 생성
        Directory root = new Directory("root");

        // 파일 생성
        File file1 = new File("file1.txt", 10);
        File file2 = new File("file2.txt", 20);
        
        // 하위 디렉토리 및 파일 생성
        Directory subDir = new Directory("sub-dir");
        File file3 = new File("file3.txt", 30);
        subDir.add(file3);

        // 루트에 추가
        root.add(file1);
        root.add(file2);
        root.add(subDir);

        // 전체 구조 출력
        root.print();
        
        // 실행 결과:
        // D root (total size: 60KB)
        // - file1.txt (size: 10KB)
        // - file2.txt (size: 20KB)
        // D sub-dir (total size: 30KB)
        // - file3.txt (size: 30KB)
    }
}

결론

  • 객체들이 트리 구조를 형성하고, 이 구조를 일관된 방식으로 다루고 싶을 때 : UI 컴포넌트(패널 안에 버튼, 다른 패널 등이 포함되는 구조), 조직도, 파일 시스템 등
  • 클라이언트 코드를 단순화하고 싶을 때 : 클라이언트는 개별 객체인지 복합 객체인지 신경 쓸 필요 없이 공통 인터페이스만 사용하면 되므로 코드가 간결해집니다.
  • 새로운 종류의 Component를 쉽게 추가할 때 : 개방-폐쇄 원칙(OCP)을 잘 따르는 구조로, 새로운 LeafComposite 클래스를 추가해도 기존 코드는 거의 영향을 받지 않습니다.

직관적이고 이해가 쉬운 구조를 가지고 있지만 다음과 같은 조건을 고려해야합니다

  • 설계의 일반화 : 때로는 시스템을 너무 일반화시켜 인터페이스에 특정 클래스에만 적용되는 새로운 기능을 추가하기 어려울 수 있습니다.
  • 자식 관리 메서드의 위치: Component 인터페이스에 자식을 추가/삭제하는 add(), remove() 메서드를 넣으면 Leaf 클래스는 이 메서드들을 불필요하게 구현해야 합니다. (예제처럼 예외를 던지거나 아무것도 하지 않도록 구현). 반대로 Composite 클래스에만 넣으면 클라이언트가 CompositeLeaf를 구분해서 사용해야 하므로 투명성이 깨집니다. 이는 설계 시 트레이드오프 관계에 있습니다.

컴포지트 패턴은 복잡한 트리 구조를 단순하고 우아하게 처리할 수 있는 강력한 도구입니다. 부분과 전체를 동일하게 다루는 아이디어를 잘 기억해두시면 다양한 상황에서 유용하게 활용할 수 있습니다. 기술사공부의 키워드는 부분-전체 / 트리구조 / Lead-Composite 가 있겠네요 예제의 코드가 제일 이해가 쉬울 거 같습니다. 다음으로는 Decorator 패턴에 대해서 알아보겠습니다

  • 작가 : 에이드리엔 허버트 / 고원(역)
  • 쪽수 : 260쪽
  • 가격 : 15,000원
  • 출판사 : 위즈덤하우스
  • 출판일 : 2022년 1월 14일
  • 독서일 : 2025년 6월 26일

 

필자가 느낀 점

 

최근 들어 생산성에 관련된 책을 읽는 일이 많아지는 것 같습니다. 어쩔 수 없는 것이, 요즘 제가 동시에 처리해야 하는 일이 너무 많기 때문입니다. 일정과 시간 관리를 위해 촌각(寸刻)을 다투며 살다 보니, 자연스레 이런 종류의 책에 눈길이 가는 것 같습니다. 독서와는 별개로, 돌이켜보면 오히려 가장 바빴을 때 저의 생산성이 극대화되었던 기억이 많습니다. 그래서 지금의 이 시기가 훗날 저를 발전시킨 원동력이 되지 않을까 하는 기대를 품으며 책을 선정했습니다.

 

이 책 [파워 아워]는 지난번에 읽었던 [싱글 태스킹]처럼, 단위 시간당 품질과 생산성을 높이는 법을 다루는 자기계발서입니다. '매일 1%의 작은 성취로 만들어내는 최고의 변화'라는 부제를 통해, 이 책이 습관에 관해 이야기할 것임을 본능적으로 짐작할 수 있었습니다.

 

이 책은 총 7장으로 구성되어 있습니다. 각각의 장은 '~한 시간'이라는 일관된 형식으로 되어 있으며, 세부 내용은 다음과 같습니다.

  1. 행복한 움직임을 찾는 시간: 운동과 움직임을 통해 긍정적인 에너지와 생각을 채우는 법을 다룹니다.
  2. 마인드셋이 전환되는 시간: 월등한 사고방식과 높은 시간 밀도를 갖기 위해 필요한 마음가짐과 질문에 대해 이야기합니다.
  3. 강력한 습관을 만드는 시간: 훌륭한 습관을 만들고 유지하기 위해 끊어내야 하는 나쁜 행동들을 설명합니다.
  4. 수면의 힘을 되찾는 시간: 좋은 수면 습관으로 양질의 휴식을 취하고, 그 에너지를 통해 다음 날을 훌륭하게 살아내는 방법을 제시합니다.
  5. 사람과 나를 연결하는 시간: 주변 사람들을 분석하고 그들의 네트워크를 통해 지식과 에너지를 얻는 법을 이야기합니다.
  6. 목적에 열정을 더하는 시간: 때때로 마주하게 되는 번아웃 증상에 대처하고, 지금까지 만든 습관의 힘을 더욱 강화하는 법을 다룹니다.
  7. 파워 아워를 만드는 시간: 1~6장을 토대로 강력한 집중력을 유지하고, 이를 통해 자신만의 '파워 아워'를 만드는 법으로 마무리합니다.

이 책을 통해 특히 마음에 들었던 내용은 '배움의 7가지 유형'이라는 부분이었습니다(p.59). 그 7가지 내용은 다음과 같습니다.

  • 청각적 학습: 소리, 목소리, 음악을 통해 가장 잘 배우는 유형
  • 시각적 학습: 이미지, 그림, 공간적 이해를 통해 가장 잘 배우는 유형
  • 구두 학습: 말, 읽기, 쓰기를 통해 가장 잘 배우는 유형
  • 신체적 학습: 움직임, 따라 하기, 감각을 통해 가장 잘 배우는 유형
  • 논리적 학습: 문제 해결, 시스템, 추리를 통해 가장 잘 배우는 유형
  • 사회적 학습: 타인과 함께하거나 팀의 일원일 때 가장 잘 배우는 유형
  • 독자적 학습: 혼자서 배우고 일하는 것을 선호하는 유형

이것은 일종의 학습 유형에 대한 설명서였지만, 저는 조금 다르게 접근하여 '학습의 양식(Modality)'에 대한 분류라고 생각했습니다. 즉, 단순히 정보를 눈으로만 읽는 것이 아니라, 소리 내어 읽거나(청각/구두) 해당 정보를 머릿속으로 상상해보는(시각) 등 여러 채널을 동시에 활용하여 학습한다면 더 높은 학습 효율을 얻을 수 있지 않을까 하는 생각이었습니다.

 

최근 학습 효율에 대해 고민할 때가 많았는데, 이 7가지 유형을 다중 감각을 활용하는 학습법으로 간주하고 저의 학습 시간을 진정한 '파워 아워'로 만들고 싶다는 다짐으로 독후감을 마칩니다.

안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 구조 패턴(Structural Patterns)의 두번째, 상속 대신 합성을 사용하여 더욱 유연한 구조를 만드는 브릿지(Bridge) 패턴에 대해 알아보겠습니다.


정의

브릿지 패턴'기능의 계층'과 '구현의 계층'을 분리하여, 각각을 독립적으로 확장할 수 있도록 만드는 패턴입니다. 이름 그대로 두 계층 사이를 '다리(Bridge)'로 연결하는 개념입니다. 리모컨과 TV를 예로 들어보겠습니다. '기본 리모컨'이 있고, '넷플릭스 버튼'이 추가된 '고급 리모컨'이 있습니다. 이것이 기능의 계층(Abstraction)입니다. 한편, TV는 '삼성 TV', 'LG TV'가 있습니다. 이것이 구현의 계층(Implementation)입니다.

 

만약 상속으로 이 모든 조합을 만든다면 '기본 리모컨-삼성 TV', '기본 리모컨-LG TV', '고급 리모컨-삼성 TV', '고급 리모컨-LG TV' ... 처럼 기능과 구현의 조합만큼 클래스 수가 폭발적으로 증가하게 됩니다. 브릿지 패턴은 이 문제를 해결합니다. 리모컨(기능)은 어떤 TV(구현)와 연결될지 참조(Reference)만 하고, 실제 전원을 켜거나 채널을 바꾸는 동작은 연결된 TV 객체에게 위임합니다. 이렇게 하면 새로운 리모컨이나 새로운 TV가 추가되어도 서로에게 영향을 주지 않고 독립적으로 확장할 수 있습니다.

 

브릿지 패턴은 추상화된 기능을 중심으로 구현부에서 Core한 기능을 별도 구현하게 됩니다

  • Abstraction (추상화된 기능): 기능 계층의 최상위 클래스입니다. Implementor에 대한 참조를 유지합니다. (예: RemoteControl)
  • RefinedAbstraction (구체화된 기능): Abstraction을 상속받아 기능을 확장합니다. (예: AdvancedRemoteControl)
  • Implementor (구현부 인터페이스): 구현 계층의 최상위 인터페이스입니다. Abstraction이 호출할 메서드를 정의합니다. (예: Device 인터페이스)
  • ConcreteImplementor (구체적인 구현부): Implementor 인터페이스를 실제로 구현합니다. (예: Tv, Radio)

사용예시

앞서 설명한 리모컨과 디바이스 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 구현부 (Implementor & ConcreteImplementor) 정의

먼저 디바이스(구현)의 인터페이스와 구체적인 클래스들을 정의합니다.

// Implementor: 구현부 인터페이스
public interface Device {
    boolean isEnabled();
    void enable();
    void disable();
    int getVolume();
    void setVolume(int percent);
    int getChannel();
    void setChannel(int channel);
}

// ConcreteImplementor A: TV
class Tv implements Device {
    private boolean on = false;
    private int volume = 30;
    private int channel = 1;

    @Override
    public boolean isEnabled() { return on; }
    @Override
    public void enable() { on = true; System.out.println("TV is ON"); }
    @Override
    public void disable() { on = false; System.out.println("TV is OFF"); }
    // ... volume, channel getter/setter 구현 ...
    @Override
    public int getVolume() { return volume; }
    @Override
    public void setVolume(int volume) { this.volume = volume; }
    @Override
    public int getChannel() { return channel; }
    @Override
    public void setChannel(int channel) { this.channel = channel; }
}

// ConcreteImplementor B: Radio
class Radio implements Device {
    // TV와 동일한 방식으로 구현
    private boolean on = false;
    private int volume = 10;
    private int channel = 99;
    
    @Override
    public boolean isEnabled() { return on; }
    @Override
    public void enable() { on = true; System.out.println("Radio is ON"); }
    @Override
    public void disable() { on = false; System.out.println("Radio is OFF"); }
    @Override
    public int getVolume() { return volume; }
    @Override
    public void setVolume(int volume) { this.volume = volume; }
    @Override
    public int getChannel() { return channel; }
    @Override
    public void setChannel(int channel) { this.channel = channel; }
}

2. 기능부 (Abstraction & RefinedAbstraction) 정의

다음으로 리모컨(기능)의 기본 클래스와 확장 클래스를 정의합니다.

// Abstraction: 기능부의 최상위 클래스
public class RemoteControl {
    // 구현부(Implementor)에 대한 참조(Bridge)
    protected Device device;

    public RemoteControl(Device device) {
        this.device = device;
    }

    public void togglePower() {
        if (device.isEnabled()) {
            device.disable();
        } else {
            device.enable();
        }
    }

    public void volumeUp() {
        device.setVolume(device.getVolume() + 10);
    }
    
    // ... volumeDown, channelUp, channelDown 등 구현 ...
}

// RefinedAbstraction: 확장된 기능부
public class AdvancedRemoteControl extends RemoteControl {
    public AdvancedRemoteControl(Device device) {
        super(device);
    }

    public void mute() {
        System.out.println("Muting device");
        device.setVolume(0);
    }
}

3. 클라이언트 (Client) 코드

클라이언트는 원하는 기능(리모컨)과 구현(디바이스)을 자유롭게 조합하여 사용합니다.

public class Client {
    public static void main(String[] args) {
        // TV와 기본 리모컨 조합
        Device tv = new Tv();
        RemoteControl remote = new RemoteControl(tv);
        remote.togglePower(); // TV is ON

        System.out.println("--------------------");

        // 라디오와 고급 리모컨 조합
        Device radio = new Radio();
        AdvancedRemoteControl advancedRemote = new AdvancedRemoteControl(radio);
        advancedRemote.togglePower(); // Radio is ON
        advancedRemote.mute();      // Muting device
    }
}

결론

그럼 브릿지 패턴은 언제 사용하는 걸까요?

  • 기능과 구현을 독립적으로 확장해야 할 때: 새로운 리모컨(기능)을 추가해도 TV, 라디오(구현) 코드는 수정할 필요가 없습니다. 반대의 경우도 마찬가지입니다.
  • 상속으로 인해 클래스가 너무 많아지는 것을 피하고 싶을 때: 브릿지 패턴은 상속 대신 합성을 사용하여 클래스 폭발 문제를 해결합니다.
  • 런타임에 구현을 바꾸고 싶을 때: 리모컨 객체가 참조하는 디바이스 객체를 동적으로 교체할 수 있습니다.

어댑터 패턴과의 차이점

브릿지 패턴은 어댑터 패턴과 구조가 비슷해 혼동하기 쉽지만, 의도가 완전히 다릅니다.

  • 어댑터 패턴: 호환되지 않는 기존의 두 인터페이스를 연결하기 위해 사용됩니다. 주로 시스템을 통합하거나 레거시 코드를 재사용할 때, 즉 사후에 문제를 해결하기 위해 쓰입니다.
  • 브릿지 패턴: 처음부터 기능과 구현을 분리하여 독립적으로 확장할 수 있는 유연한 구조를 만들기 위해 사용됩니다. 즉, 설계 단계에서부터 고려되는 패턴입니다.

브릿지 패턴은 '상속보다는 합성을 사용하라(Composition over inheritance)'는 객체 지향의 특성을 잘 보여주는 대표적인 예입니다. 복잡한 시스템을 유연하고 확장 가능하게 설계하고 싶을 때 반드시 고려해야 할 강력한 패턴입니다. 다음 시간에는 복합패턴(Composite)에 대해서 알아보겠습니다

안녕하세요! GoF 디자인 패턴 연재, 이번 시간부터는 구조 패턴(Structural Patterns)에 대해 알아봅니다. 구조 패턴은 클래스나 객체들을 조합하여 더 큰 구조를 만드는 것과 관련된 패턴들입니다. 그 첫 번째 주자는 바로 어댑터(Adapter) 패턴입니다!


정의

어댑터 패턴호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록 변환해주는 역할을 하는 패턴입니다. 이름 그대로 '어댑터' 역할을 합니다. 가장 쉽게 떠올릴 수 있는 예는 바로 '돼지코'라고 불리는 여행용 전원 어댑터입니다. 한국의 220V 콘센트와 미국의 110V 플러그는 모양과 전압이 달라 직접 연결할 수 없습니다. 이때 둘 사이에 어댑터를 끼우면, 미국 110V 전자제품을 한국 220V 콘센트에서 사용할 수 있게 되죠.

 

이처럼 어댑터 패턴은 서로 다른 인터페이스 때문에 함께 사용할 수 없는 클래스들을, 중간에서 '어댑터' 클래스를 통해 연결하여 클라이언트가 기대하는 인터페이스로 변환해주는 역할을 합니다.

 

어댑터 패턴은 구현 방식에 따라 '객체 어댑터''클래스 어댑터' 두 가지로 나뉩니다. 여기서는 더 널리 쓰이고 유연한 객체 어댑터 패턴을 중심으로 설명하겠습니다.

  • Target (타겟 인터페이스): 클라이언트가 사용하려는 목표 인터페이스입니다. (예: Korean220V 인터페이스)
  • Client (클라이언트): Target 인터페이스를 사용하여 작업을 요청하는 객체입니다.
  • Adaptee (어댑티): 기존에 존재하지만 인터페이스가 호환되지 않는 클래스입니다. (예: American110VAppliance 클래스)
  • Adapter (어댑터): Target 인터페이스를 구현하고, 내부에 Adaptee 인스턴스를 가집니다. 클라이언트의 요청이 들어오면, Adaptee의 메서드를 호출하여 실제 작업을 위임합니다.

사용예시

앞서 비유로 들었던 110V 가전제품을 220V 콘센트에 연결하는 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 타겟 (Target) 인터페이스 정의

클라이언트가 사용하고자 하는 220V 콘센트 인터페이스입니다.

// Target 인터페이스
public interface Korean220V { void powerOn(); }

2. 어댑티 (Adaptee) 클래스 정의

기존에 존재하던, 인터페이스가 호환되지 않는 110V 가전제품 클래스입니다.

// Adaptee 클래스
public class American110VAppliance {
    public void run() { System.out.println("110V 가전제품이 작동합니다."); }
}

public class VoltageAdapter implements Korean220V {

    // 내부에 Adaptee 객체를 가짐 (Composition)
    private American110VAppliance appliance;

    public VoltageAdapter(American110VAppliance appliance) { this.appliance = appliance; }

    // Target 인터페이스의 메서드를 구현
    @Override
    public void powerOn() { appliance.run(); }
}

클라이언트가 powerOn()을 호출하면, 어댑터는 내부적으로 appliance의 run() 메서드를 호출하여 요청을 변환해줍니다.

3. 클라이언트 (Client) 코드

클라이언트는 Korean220V 인터페이스만 알고 있으며, 어댑터를 통해 110V 제품을 사용할 수 있습니다.

public class Client {
    public static void main(String[] args) {
        // 1. 호환되지 않는 110V 가전제품 생성
        American110VAppliance tv = new American110VAppliance();

        // 2. 어댑터에 110V 가전제품을 연결
        Korean220V adapter = new VoltageAdapter(tv);

        // 3. 클라이언트는 220V 인터페이스를 통해 110V 제품을 사용
        connectTo220VOutlet(adapter);
        
        // 실행 결과:
        // 220V 콘센트에 연결되었습니다.
        // 110V 가전제품이 작동합니다.
    }

    // 클라이언트는 Korean220V 인터페이스에만 의존한다.
    public static void connectTo220VOutlet(Korean220V appliance) {
        System.out.println("220V 콘센트에 연결되었습니다.");
        appliance.powerOn();
    }
}

결론

그럼 어댑터 패턴은 언제 사용하는 걸까요?

  • 기존 코드를 변경하지 않고, 호환되지 않는 인터페이스를 가진 클래스를 사용하고 싶을 때 : 라이브러리나 프레임워크의 클래스를 수정할 수 없을 때 매우 유용합니다.
  • 여러 서브클래스들을 재사용하고 싶지만, 각각의 인터페이스가 목표 인터페이스와 다를 때 : 각 서브클래스마다 어댑터를 만들어 재사용성을 높일 수 있습니다.
  • 기존 클래스와 클라이언트 사이에 결합도를 낮추고 싶을 때 : 클라이언트는 Target 인터페이스에만 의존하므로, 실제 구현(Adaptee)이 변경되어도 클라이언트 코드는 영향을 받지 않습니다.

결합도를 감소시킬 수 있는 고 유지보수성의 패턴이지만 다음과 같은 조건을 고려해야합니다

  • 불필요한 클래스 증가: 간단한 로직임에도 불구하고 어댑터 클래스를 추가로 만들어야 하므로, 시스템의 복잡도가 약간 증가할 수 있습니다.

어댑터 패턴은 기존 시스템에 새로운 구성 요소를 통합하거나, 레거시 코드를 재사용할 때 빛을 발하는 매우 실용적인 패턴입니다. 서로 다른 두 세계를 연결하는 '다리' 역할을 한다고 기억해두시면 좋습니다. 다음 시간에는 어댑터 패턴과 유사하지만 차이가 명확하게 존재하는 브릿지 패턴을 알아보겠습니다

  • 작가 : 데보라 잭 / 이혜리(역)
  • 쪽수 : 221쪽
  • 가격 : 13,000원
  • 출판사 : 인사이트앤뷰
  • 출판일 : 2015년 9월 1일
  • 독서일 : 2025년 6월 25일

 

필자가 느낀 점

 

멀티태스킹(Multi-tasking)은 본래 컴퓨터 과학 분야에서 사용되는 용어로, 하나의 프로세서(처리기)가 여러 작업을 빠르게 오가며 수행해 마치 두 가지 이상의 일을 동시에 처리하는 것처럼 보이게 하는 기법입니다. 컴퓨터처럼 엄청난 처리 속도를 가진 존재라면 모를까, 과연 인간에게도 멀티태스킹이 가능할까요?

 

이 물음은 생산성을 높이고 싶은 사람들에게 '가능해야만 한다'는 당위성과 함께 일종의 환상처럼 여겨져 왔습니다. 사람들은 멀티태스킹을 하는 이들을 초인처럼 여기고, 그들의 테크닉을 배우기 위해 책을 읽는 등 많은 연구가 이루어진 것으로 압니다. 하지만 안타깝게도 인간의 뇌는 멀티태스킹에 적합하게 설계되지 않았습니다. 인간의 집중력은 쉽게 분산되는 구조를 가지고 있어, 두 가지 일을 동시에 50분간 처리하는 것보다 각각 25분씩 순차적으로 처리하는 것이 오히려 더 높은 결과물의 품질을 보장합니다.

 

이 책 [싱글 태스킹]은 이제 우리 인류가 비효율적인 멀티태스킹의 신화에서 벗어나 '싱글태스킹'을 선택해야 한다고 강력하게 주장합니다. 총 7개의 장으로 구성된 이 책은 '하나에 집중하지 않으면 하나도 이룰 수 없다'는 부제처럼, 한 번에 한 가지 일에만 집중하는 것이 얼마나 우리의 삶에 도움이 되는지, 그리고 얼마나 큰 집중력의 이점을 가져다주는지 설명합니다.

 

책에서 특히 마음에 들었던 점은 '클러스터 태스킹(Cluster-tasking)'이라는 기법입니다. 이는 간단하고 한데 묶을 수 있는 일들을 하루에 2~3번, 정해진 시간에 몰아서 처리하는 방식으로, 여러 작업 사이를 정신없이 오가는 멀티태스킹과는 근본적으로 다릅니다. 메일 확인, 결제 처리 등과 같이 간단히 해결할 수 있는 일들을 쉬는 시간이나 자투리 시간에 한 번에 처리함으로써, 오히려 다른 중요한 작업의 속도와 집중도를 높이는 기법으로 소개합니다.

 

저 또한 업무 중에 여러 일을 동시에 하려 하거나 다른 일에 집중력을 빼앗길 때가 많습니다. 이 책을 통해 배운 '클러스터 태스킹'을 적용하여 제 생활과 업무 속에서 묶어서 처리할 수 있는 일이 무엇인지 잘 분석해보고 싶습니다. 이를 통해 저의 생산성을 한 단계 높일 수 있기를 기대하며 독후감을 마칩니다.

 

+ Recent posts