[Design Patterm] 2. 구조(Structural) 패턴(1) - Decorator, Proxy

김민경·2025년 6월 15일

Design Pattern

목록 보기
3/9
post-thumbnail

구조(Structural) 패턴

클래스나 객체를 조합하여 더 큰 구조를 만드는 패턴

  • 서로 다른 인터페이스를 갖는 2개의 객체를 하나로 묶어 단일 인터페이스 제공
  • 객체들을 서로 묶어 새로운 기능 제공

즉, 객체 간의 관계를 실현할 수 있는 간단한 방법으로써 설계를 용이하게 하는 패턴

구조 클래스 패턴은 상속을 통해 클래스나 인터페이스를 합성하고, 구조 객체 패턴은 객체를 합성하는 방법을 정의


데코레이터 (Decorator) 패턴

객체를 동적으로 새로운 책임을 추가할 수 있게 하는 패턴

서브 클래스를 생성하는 것보다 더 유연한 방법 제공
객체를 데코레이터 클래스의 객체로 감싸 런타임에 객체의 기능을 확장하거나 변경하는 데 사용

예제

기본 Coffee에 대한 인터페이스와 기본 구현체(BasicCoffee) 생성

public interface Coffee {
    String getDescription();
    int getCost();
}

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

    @Override
    public int getCost() {
        return 3000;
    }
}

추상 클래스 CoffeeDecorator와 구체화하는 MilkDecorator와 WhipDecorator 생성

public abstract class CoffeeDecorator implements Coffee{
    protected Coffee coffee;
    //구성 요소의 레퍼런스를 포함한 인스턴스 변수가 존재

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

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

    @Override
    public int getCost() {
        return coffee.getCost();
    }
}

public class MilkDecorator extends CoffeeDecorator{
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

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

    @Override
    public int getCost() {
        return coffee.getCost() + 500;
    }
}

public class WhipDecorator extends CoffeeDecorator{
    public WhipDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Whip";
    }

    @Override
    public int getCost() {
        return coffee.getCost() + 700;
    }
    
}

런타임에 BasicCoffee 객체에서 Milk,Whip 데코레이터를 통해 객체를 감싸며 동적으로 기능을 확장

public class CoffeeMain {
    public static void main(String[] args) {
        Coffee coffee = new BasicCoffee();
        System.out.println(coffee.getDescription() + " : " + coffee.getCost());

        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.getDescription() + " : " + coffee.getCost());

        coffee = new WhipDecorator(coffee);
        System.out.println(coffee.getDescription() + " : " + coffee.getCost());
    }
}

/*
Basic Coffee : 3000
Basic Coffee, Milk : 3500
Basic Coffee, Milk, Whip : 4200
*/

특징

  1. 런타임에 기능 추가 가능
    상속이 아닌 합성을 사용하여, 객체를 감싸며 기능을 점진적 추가

  2. OCP 준수
    기존 코드를 변경하지 않고, 새로운 클래스를 통해 기능 확장

  3. 상속보다 유연
    상속은 컴파일 타임에 결정되지만, Decorator는 런타임에 조합 가능

  4. 재귀적 구조
    Decorator는 같은 타입을 구현하므로, 자기 자신을 계속 감쌀 수 있는 구조를 가짐
    new WhipDecorator(new MilkDecorator(new BasicCoffee)))

  1. 클라이언트는 Decorator와 원본 구분 불가능

  2. 복잡도 증가 가능
    Decorator를 여러 개 중첩하는 경우 구조가 복잡해지고 디버깅이 어려워질 수 있음

Decorator가 새로운 메서드를 추가할 수도 있지만, 일반적으로 감싸는 객체에 있던 메소드를 별도의 작업으로 처리해서 새로운 기능을 추가


프록시 (Proxy) 패턴

실제 기능을 수행하는 객체 대신 가상의 객체를 만들어 사용하는 패턴
어떤 객체를 사용하고자 할 때, 해당 객체를 직접 참조하는 것이 아니라, 그 객체를 대신(Proxy)하는 객체를 통해 접근하는 방식

해당 객체가 메모리상에 존재하지 않더라도 기본적으로 정보를 참조하거나 설정할 수 있고, 실제 객체의 기능이 반드시 필요한 시점까지 객체 생성을 미룰 수 있는 장점 존재

예제

File 인터페이스와 구현체 NormalFile 생성

public interface File {
    void read(String fileName);
}

public class NormalFile implements File{
    @Override
    public void read(String fileName) {
        System.out.println("Reading.. " + fileName);
    }
}

직접 NormalFile를 참조하는 대신 가상의 객체(NormalFileProxy)를 통해 해당 파일에 접근이 가능한지를 확인

public class NormalFileProxy implements File{

    private NormalFile normalFile = new NormalFile();
    private String password = "";

    public NormalFileProxy(String password){
        this.password = password;
    }

    @Override
    public void read(String fileName) {
        if (password.equals("security"))
            normalFile.read(fileName);
        else
            System.out.println("Security File");
    }
}

NormalFileProxy를 통해 접근 권한 판단 후 만약 접근 가능하다면 간접적으로 NormalFile에 참조

public class FileMain {
    public static void main(String[] args) {
        File securedFile = new NormalFileProxy("security");
        securedFile.read("readme.md");

        File normalFile = new NormalFileProxy("");
        normalFile.read("readme.md");
    }
}

/*
Security File
Reading.. readme.md
*/

특징

  1. 접근 제어
    Proxy를 통해 제한적 접근 구현
  1. 실제 객체와 동일한 인터페이스 사용
    클라이언트는 Proxy인지 실제 객체인지 알 필요 없이 동일하게 사용
  1. 지연 초기화 (Lazy Initialization)
    실제 사용될 때까지 객체 생성을 지연
  1. 성능 향상 기능
    결과를 캐싱하거나, 실제 객체의 사용을 줄임으로써 전체 시스템 성능 향상 기능

출처

https://jslee-tech.tistory.com/26
https://www.hanbit.co.kr/channel/view.html?cmscode=CMS8616098823

profile
뭐든 기록할 수 있도록

0개의 댓글