디자인 패턴 - 구조 패턴(Structural Patterns)

송현진·2025년 7월 14일
0

CS공부

목록 보기
14/17

구조(Structural) 디자인 패턴

구조 패턴은 클래스나 객체를 조합해 더 큰 구조를 설계하는 데 집중하는 패턴이다. 즉, 여러 객체를 어떻게 연결하고 어떻게 유연하게 확장하거나 대체할 수 있을지 고민한 결과물이다.

대표적인 구조 패턴에는 아래 7가지가 있다.

패턴명핵심 목적대표 예시
Adapter인터페이스 호환HandlerAdapter, Arrays.asList()
Decorator기능 덧붙이기BufferedInputStream, Spring 필터 체인
Proxy접근 제어, 지연 로딩Spring AOP, java.lang.reflect.Proxy
Composite계층 구조 일관 처리파일 시스템, UI 컴포넌트
Facade복잡도 감추기JdbcTemplate, RestTemplate
Bridge구현과 추상화 분리View-Renderer, DB 전략 분리
Flyweight객체 공유로 메모리 절약Integer.valueOf(), String.intern()

Adaptor 패턴 (어댑터)

서로 다른 인터페이스를 가진 클래스들을 호환시켜주는 패턴이다. 중간에 어댑터 역할의 객체를 두어 기존 클래스의 인터페이스를 클라이언트가 기대하는 인터페이스로 변환한다. 주로 레거시 코드와의 호환에 사용된다.

// 기존 레거시 클래스
class LegacyPrinter {
    void oldPrint(String msg) {
        System.out.println("OLD: " + msg);
    }
}

// 클라이언트가 기대하는 인터페이스
interface Printer {
    void print(String msg);
}

// 어댑터 클래스
class PrinterAdapter implements Printer {
    private final LegacyPrinter legacyPrinter = new LegacyPrinter();

    @Override
    public void print(String msg) {
        legacyPrinter.oldPrint(msg);
    }
}

예를 들어 구형 충전기를 USB-C 포트에 연결하려면 중간에 변환 어댑터가 필요하다. 소프트웨어에서도 마찬가지로 오래된 인터페이스를 새 코드와 연결하고 싶을 때 Adapter 패턴을 적용한다.
Java에서는 java.util.Arrays#asList() 또는 Spring의 HandlerAdapter가 대표적인 어댑터 사용 예시다.

Decorator Pattern (데코레이터)

기존 객체의 기능을 확장할 수 있도록 설계된 패턴이다. 상속이 아닌 합성(composition) 을 통해 객체에 새로운 책임을 동적으로 부여할 수 있다. 객체를 감싸서 기능을 추가하므로 여러 기능을 조합해 유연하게 사용할 수 있다.

interface Coffee {
    String getDescription();
    int cost();
}

class BasicCoffee implements Coffee {
    public String getDescription() {
        return "Basic Coffee";
    }

    public int cost() {
        return 3000;
    }
}

// 데코레이터 추상 클래스
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

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

// 기능 추가: 우유
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Milk";
    }

    public int cost() {
        return decoratedCoffee.cost() + 500;
    }
}

스타벅스에서 아메리카노에 시럽, 우유, 휘핑을 추가하는 것처럼 기능을 계속 덧붙이는 상황에 적합하다. Java IO의 BufferedInputStream, DataInputStream, LineNumberInputStream 등이 데코레이터 패턴으로 구현되어 있다. Spring Security에서 필터 체인을 데코레이터처럼 구성하는 것도 유사한 원리다.

Proxy Pattern (프록시)

실제 객체에 접근하기 전 대리 객체를 통해 제어하는 패턴이다. 접근 제어, 로깅, 지연 로딩 등 다양한 목적을 위해 사용된다. 클라이언트는 실제 객체가 아니라 프록시를 통해 작업을 요청하며 프록시는 내부에서 실제 객체를 사용할지 말지를 결정한다.

interface Image {
    void display();
}

// 실제 객체
class RealImage implements Image {
    private final String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

// 프록시 객체
class ProxyImage implements Image {
    private final String filename;
    private RealImage realImage;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename); // 지연 로딩
        }
        realImage.display();
    }
}

Proxy는 보안, 캐싱, 접근 제한, 지연 초기화(lazy loading) 에 자주 사용된다.
Java의 java.lang.reflect.Proxy 클래스, MyBatis의 Mapper 프록시, Spring AOP 프록시 등이 대표적인 사례다. 특히 DB나 네트워크와 연결되는 무거운 객체의 생성을 나중으로 미루고 싶을 때 Proxy 패턴이 유용하다.

Composite Pattern (컴포지트)

트리 구조처럼 객체를 계층적으로 구성하고 개별 객체와 복합 객체를 동일하게 다루는 패턴이다. 클라이언트는 단일 객체와 복합 객체의 구분 없이 동일한 방식으로 처리할 수 있어 일관성 있는 코드를 작성할 수 있다.

Decorator 패턴과 유사하지만 Decorator기능을 동적으로 덧붙이기 위한 패턴이고 Composite전체-부분 관계를 계층적으로 표현하는 패턴이다.

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

// Leaf: 개별 객체
class File implements Component {
    private final String name;

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

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

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

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

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

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

파일 시스템처럼 디렉토리(폴더) 안에 파일과 또 다른 폴더가 들어가는 계층적 구조에 적합하다.UI 컴포넌트 트리(React, Swing 등), JSON/XML 파서 등에서 자주 사용된다.

Facade Pattern (퍼사드)

복잡한 서브시스템을 단순한 인터페이스로 감싸서 사용을 쉽게 만드는 패턴이다. 여러 개의 객체나 클래스를 조합해 내부 로직을 캡슐화하고 외부에는 간단한 인터페이스만 제공한다.

class CPU {
    void start() {
        System.out.println("CPU 시작");
    }
}

class Memory {
    void load() {
        System.out.println("메모리 로드");
    }
}

class HardDrive {
    void read() {
        System.out.println("하드디스크 읽기");
    }
}

// 퍼사드 클래스
class ComputerFacade {
    private final CPU cpu = new CPU();
    private final Memory memory = new Memory();
    private final HardDrive hd = new HardDrive();

    public void startComputer() {
        cpu.start();
        memory.load();
        hd.read();
    }
}

Spring의 JdbcTemplate, RestTemplate, SecurityContextHolder 등은 내부 로직을 감추고 단순한 API를 제공하는 대표적인 퍼사드이다. 외부 시스템이나 라이브러리의 복잡도를 숨기고 클라이언트 코드의 의존성을 줄이는 데 효과적이다.

Bridge Pattern (브리지)

추상화와 구현을 분리하여 독립적으로 확장 가능하게 하는 패턴이다. 상속보다 구성을 통해 유연하게 계층 구조를 나눌 수 있으며 변화에 쉽게 대응할 수 있다.

// 구현부 인터페이스
interface Device {
    void turnOn();
    void turnOff();
}

// 실제 구현체
class TV implements Device {
    public void turnOn() {
        System.out.println("TV 켜짐");
    }

    public void turnOff() {
        System.out.println("TV 꺼짐");
    }
}

class Radio implements Device {
    public void turnOn() {
        System.out.println("라디오 켜짐");
    }

    public void turnOff() {
        System.out.println("라디오 꺼짐");
    }
}

// 추상화
abstract class RemoteControl {
    protected Device device;

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

    abstract void togglePower();
}

// 확장된 추상화
class BasicRemote extends RemoteControl {
    private boolean isOn = false;

    public BasicRemote(Device device) {
        super(device);
    }

    public void togglePower() {
        if (isOn) {
            device.turnOff();
        } else {
            device.turnOn();
        }
        isOn = !isOn;
    }
}

UI 프레임워크에서 뷰(View)와 플랫폼별 렌더러(Renderer) 를 분리하거나 Spring의 JdbcTemplate에서 DB 연결과 SQL 실행 전략을 분리하는 것도 유사한 개념이다.

Flyweight Pattern (플라이웨이트)

공통된 데이터를 공유해서 메모리 사용을 최소화하는 패턴이다. 대량의 객체를 생성해야 할 때 변하지 않는 부분은 공유하고 변하는 부분만 외부에서 주입하여 성능과 메모리 최적화를 꾀한다.

// 공유 객체
class Font {
    private final String name;

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

    public String getName() {
        return name;
    }
}

// 팩토리
class FontFactory {
    private static final Map<String, Font> fontPool = new HashMap<>();

    public static Font getFont(String name) {
        return fontPool.computeIfAbsent(name, Font::new);
    }
}

// 사용 예
class Character {
    private final char c;
    private final Font font;

    public Character(char c, String fontName) {
        this.c = c;
        this.font = FontFactory.getFont(fontName); // 공유
    }

    public void print() {
        System.out.println("Char: " + c + ", Font: " + font.getName());
    }
}

텍스트 편집기에서 문자의 글꼴, 게임에서 수천 개의 총알 객체 등은 Flyweight 패턴을 통해 공유하여 성능을 개선할 수 있다. Java의 Integer.valueOf(), String.intern(), 캐시 풀(Pool) 등이 이 패턴을 기반으로 한다.

String.intern()
동일한 문자열은 JVM의 string pool에 한 번만 저장

Integer.valueOf(), Boolean.TRUE/FALSE
자주 쓰이는 값은 캐싱해서 재사용

📝 배운점

처음에는 구조 패턴들이 이름만 어렵고 실제로는 비슷비슷하다고 생각했는데 이번에 하나하나 예제 코드로 정리해보면서 각 패턴이 어떤 상황에 쓰이고 어떤 장점이 있는지 더 잘 이해하게 됐다. 특히 Adapter, Decorator, Facade 같은 패턴은 내가 실제 프로젝트에서 자주 봤던 코드들이랑 연결돼서 더 쉽게 와닿았다. 반면에 Bridge, Flyweight 같은 패턴은 평소에 잘 보이지는 않지만 프레임워크 내부나 대규모 시스템에서는 성능이나 구조 설계 때문에 꼭 필요한 개념이라는 것도 알게 되었다. 앞으로는 꼭 디자인 패턴 이름을 떠올리지 않더라도 비슷한 상황이 오면 “아 이럴 땐 이렇게 구조를 나눌 수 있겠구나” 하고 자연스럽게 떠올릴 수 있도록 많이 연습해야겠다고 느꼈다.


참고

profile
개발자가 되고 싶은 취준생

0개의 댓글