백엔드 개발자

[디자인 패턴] 구조 패턴 본문

카테고리 없음

[디자인 패턴] 구조 패턴

임잠탱 2025. 2. 13. 19:52

구조 패턴(Structural Patterns)은 객체나 클래스를 조합하여 더 큰 구조를 만드는 데 초점을 둔다.

그래서 유지보수성과 확장성을 고려하여 코드의 결합도를 낮추고 재사용성을 높이는 역할을 한다.

1. 어댑터 패턴(Adapter Pattern)

어댑터 패턴은 서로 다른 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 중간에 변환기를 두는 패턴이다.
즉, 기존 코드의 변경 없이 새로운 기능을 추가할 때 유용하다.

구현 방법

  • 기존 인터페이스와 호환되지 않는 클래스를 새 인터페이스에 맞게 변환하는 역할을 한다.
  • 인터페이스 기반의 어댑터 또는 객체 기반의 어댑터를 사용할 수 있다.

특징

  • 코드 변경 없이 다른 클래스와 연동 가능.
  • 기존 시스템을 수정하지 않고 확장 가능.
  • 클래스 어댑터(상속 사용)와 객체 어댑터(위임 사용) 두 가지 방식이 있다.

사용 예시 (Java)

// 기존 인터페이스
interface OldSystem {
    void oldMethod();
}

// 새로운 인터페이스
interface NewSystem {
    void newMethod();
}

// 기존 시스템 구현
class OldSystemImpl implements OldSystem {
    @Override
    public void oldMethod() {
        System.out.println("Old system method.");
    }
}

// 어댑터 클래스 (NewSystem을 구현하면서, OldSystem을 필드로 가지고 있다.)
class Adapter implements NewSystem {
    private OldSystem oldSystem;

    public Adapter(OldSystem oldSystem) {
        this.oldSystem = oldSystem;
    }

    @Override
    public void newMethod() {
        oldSystem.oldMethod(); // 기존 메서드를 변환하여 호출
    }
}

public class AdapterPatternExample {
    public static void main(String[] args) {
        OldSystem oldSystem = new OldSystemImpl();
        NewSystem newSystem = new Adapter(oldSystem);
        newSystem.newMethod();
    }
}

실제 사용 예시

  • Java의 InputStreamReader: InputStream(바이트 기반)을 Reader(문자 기반)로 변환할 때 사용됨.
  • Spring의 Resource 인터페이스: 다양한 리소스(File, URL)를 통합하여 처리하는 데 사용됨.

2. 브리지 패턴(Bridge Pattern)

브리지 패턴은 추상적인 부분과 구현을 분리하여 독립적으로 확장할 수 있도록 하는 패턴이다.
즉, 인터페이스와 구현을 분리하여 유지보수성을 높이고 결합도를 낮춘다.

구현 방법

  • 추상 클래스는 기능을 정의하고, 구현체는 별도의 클래스로 분리한다.
  • 인터페이스를 활용하여 다형성을 유지하며, 여러 구현체와 조합할 수 있도록 한다.

특징

  • 추상화와 구현을 독립적으로 확장 가능.
  • 계층 구조가 복잡한 경우 유용.
  • 유지보수가 쉬워지고 결합도가 낮아짐.

사용 예시 (Java)

// 구현체 인터페이스
interface Color {
    String fill();
}

// 구체적인 구현체
class RedColor implements Color {
    @Override
    public String fill() {
        return "Color: Red";
    }
}

class BlueColor implements Color {
    @Override
    public String fill() {
        return "Color: Blue";
    }
}

// 추상 클래스
abstract class Shape {
    protected Color color;

    public Shape(Color color) {
        this.color = color;
    }

    abstract void draw();
}

// 구체적인 추상 클래스
class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }

    @Override
    public void draw() {
        System.out.println("Drawing Circle with " + color.fill());
    }
}

// 사용 예시
public class BridgePatternExample {
    public static void main(String[] args) {
        Shape redCircle = new Circle(new RedColor());
        Shape blueCircle = new Circle(new BlueColor());

        redCircle.draw(); // Drawing Circle with Color: Red
        blueCircle.draw(); // Drawing Circle with Color: Blue
    }
}

실제 사용 예시

  • JDBC 드라이버: Connection 인터페이스와 각 데이터베이스 벤더의 구현 분리.
  • GUI 라이브러리: OS별 UI 렌더링을 독립적으로 구현.

3. 프록시 패턴(Proxy Pattern)

프록시 패턴은 특정 객체에 대한 접근을 제어하기 위해 대리자(Proxy)를 사용하는 패턴이다.

구현 방법

  • 실제 객체 대신 중간에 Proxy 클래스를 두고 요청을 위임한다.
  • 접근 제어(보안, 로깅, 캐싱 등)를 적용할 수 있다.

특징

  • 객체 생성 비용을 줄일 수 있음.
  • 접근 제어 및 로깅 기능을 쉽게 추가 가능.
  • 원본 객체를 보호할 수 있음.

사용 예시 (Java)

// 원본 인터페이스
interface Service {
    void operation();
}

// 실제 서비스 클래스
class RealService implements Service {
    @Override
    public void operation() {
        System.out.println("Real Service is performing an operation.");
    }
}

// 프록시 클래스 (RealService 접근 제어)
class ProxyService implements Service {
    private RealService realService;

    @Override
    public void operation() {
        if (realService == null) {
            realService = new RealService(); // 지연 초기화
        }
        System.out.println("Proxy: Logging before operation.");
        realService.operation();
    }
}

// 사용 예시
public class ProxyPatternExample {
    public static void main(String[] args) {
        Service proxy = new ProxyService();
        proxy.operation();
    }
}

실제 사용 예시

  • Spring AOP: 프록시를 사용하여 트랜잭션, 로깅, 보안 기능 추가.
  • 가상 프록시: 이미지 로딩 최적화(예: 지연 로딩).

=> 연예인 매니지먼트를 통해 섭외, 협찬, 선물을 전달하는 것처럼 연예인한테 다이렉트로 요청을 전하는 것이 아닌 프록시(대리인)를 통해 접근 제어를 할 수 있다.

4. 데코레이터 패턴(Decorator Pattern)

데코레이터 패턴은 기존 객체에 새로운 기능을 추가하는 패턴이다.
상속이 아닌 객체 조합(composition) 을 사용하여 기능을 확장한다.

구현 방법

  • 기본 클래스를 상속받은 데코레이터 클래스를 만들고, 해당 클래스에서 기존 기능을 감싸서 확장한다.
  • 실행 중에 동적으로 객체의 기능을 변경할 수 있다.

특징

  • 기존 코드를 수정하지 않고 기능을 추가 가능.
  • 중첩을 통해 여러 기능을 조합 가능.
  • OCP(개방-폐쇄 원칙)를 만족.

사용 예시 (Java)

// 기본 인터페이스
interface Coffee {
    String make();
}

// 기본 클래스
class SimpleCoffee implements Coffee {
    @Override
    public String make() {
        return "Simple Coffee";
    }
}

// 데코레이터 클래스
class MilkDecorator implements Coffee {
    private Coffee coffee;

    public MilkDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String make() {
        return coffee.make() + ", with Milk";
    }
}

// 사용 예시
public class DecoratorPatternExample {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        Coffee milkCoffee = new MilkDecorator(coffee);

        System.out.println(coffee.make());      // Simple Coffee
        System.out.println(milkCoffee.make()); // Simple Coffee, with Milk
    }
}

클래스 다이어그램

이미지 출처 : https://www.youtube.com/watch?v=UTmY_oB4V8I&list=PLe6NQuuFBu7FhPfxkjDd2cWnTy2y_w_jZ&index=15

실제 사용 예시

  • Java I/O (BufferedReader, InputStreamReader)
  • Spring의 BeanPostProcessor

5. 컴포지트 패턴(Composite Pattern)

컴포지트 패턴은 객체를 트리 구조로 구성하여 계층적인 표현을 가능하게 한다.
즉, 단일 객체와 복합 객체를 동일하게 다룰 수 있도록 설계한다.

구현 방법

  • 공통 인터페이스를 만들어 개별 객체와 복합 객체를 동일하게 다룰 수 있도록 한다.
  • 재귀적인 구조를 가질 수 있도록 설계한다.

특징

  • 클라이언트가 단일 객체와 복합 객체를 동일하게 다룰 수 있음.
  • 트리 구조를 쉽게 관리 가능.
  • 계층적인 데이터 표현에 적합.

사용 예시 (Java)

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

// 공통 인터페이스
interface Component {
    void showDetails();
}

// 개별 객체 (Leaf)
class File implements Component {
    private String name;

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

    @Override
    public void showDetails() {
        System.out.println("File: " + name);
    }
}

// 복합 객체 (Composite)
class Folder implements Component {
    private String name;
    private List<Component> components = new ArrayList<>();

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

    public void addComponent(Component component) {
        components.add(component);
    }

    @Override
    public void showDetails() {
        System.out.println("Folder: " + name);
        for (Component component : components) {
            component.showDetails();
        }
    }
}

// 사용 예시
public class CompositePatternExample {
    public static void main(String[] args) {
        Folder root = new Folder("Root");
        Folder subFolder = new Folder("SubFolder");

        File file1 = new File("file1.txt");
        File file2 = new File("file2.txt");

        root.addComponent(subFolder);
        root.addComponent(file1);
        subFolder.addComponent(file2);

        root.showDetails();
    }
}

실제 사용 예시

  • 파일 시스템 (디렉토리 - 파일 구조)
  • HTML 요소 구조

6. 퍼사드 패턴(Facade Pattern)

퍼사드 패턴은 복잡한 서브시스템을 단순한 인터페이스로 감싸는 패턴이다.
즉, 클라이언트가 복잡한 내부 구현을 몰라도 쉽게 사용할 수 있도록 한다.

구현 방법

  • 서브시스템을 감싸는 단일 클래스를 만든다.
  • 클라이언트는 이 단일 클래스를 통해서만 기능을 사용하도록 한다.

특징

  • 사용자가 내부 구현을 몰라도 쉽게 사용 가능.
  • 서브시스템 간의 결합도를 낮출 수 있음.
  • 코드의 가독성과 유지보수성이 향상됨.

사용 예시 (Java)

// 복잡한 서브 시스템
class CPU {
    void start() { System.out.println("CPU started."); }
}

class Memory {
    void load() { System.out.println("Memory loaded."); }
}

class HardDrive {
    void read() { System.out.println("Hard Drive reading."); }
}

// 퍼사드 클래스
class ComputerFacade {
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;

    public ComputerFacade() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    public void startComputer() {
        cpu.start();
        memory.load();
        hardDrive.read();
        System.out.println("Computer started successfully.");
    }
}

// 사용 예시
public class FacadePatternExample {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();
        computer.startComputer();
    }
}

실제 사용 예시

  • Spring의 JdbcTemplate (복잡한 DB 작업을 단순화)
  • SLF4J 로깅 프레임워크

=> 너무 당연하게 느껴지지만 우리가 라이브러리나 프레임워크를 사용하는 것도 이런 퍼사드 패턴 기반으로 발전해왔기 때문에 그렇게 느껴질 수 있다.


7. 플라이웨이트 패턴(Flyweight Pattern)

플라이웨이트 패턴은 객체를 공유하여 메모리 사용량을 최적화하는 패턴이다.

구현 방법

  • 공유 가능한 상태를 가진 객체를 캐싱하여 재사용한다.
  • 다수의 객체 생성 비용을 줄인다.

특징

  • 객체 재사용을 통해 성능 최적화 가능.
  • 상태를 내부(공유)와 외부(비공유)로 나눔.
  • 메모리 사용량 감소.

사용 예시 (Java)

import java.util.HashMap;
import java.util.Map;

// 공유 객체
class Character {
    private final char symbol;

    public Character(char symbol) {
        this.symbol = symbol;
    }

    public void display() {
        System.out.println("Character: " + symbol);
    }
}

// 플라이웨이트 팩토리
class CharacterFactory {
    private static final Map<Character, Character> cache = new HashMap<>();

    public static Character getCharacter(char symbol) {
        cache.putIfAbsent(symbol, new Character(symbol));
        return cache.get(symbol);
    }
}

// 사용 예시
public class FlyweightPatternExample {
    public static void main(String[] args) {
        Character a1 = CharacterFactory.getCharacter('A');
        Character a2 = CharacterFactory.getCharacter('A');
        a1.display();
        a2.display();

        System.out.println("Same object: " + (a1 == a2)); // true
    }
}

실제 사용 예시

  • Java String Pool (String interning)
  • 그래픽 엔진에서 객체 재사용 (예: 문자 렌더링)
Comments