-
[Software Engineering] Design Pattern: Structural PatternsCS/Software Engineering 2026. 4. 9. 12:46
Creational Pattern이 객체를 어떻게 만들지를 다뤘다면, Structural Pattern은 만들어진 객체들을 어떻게 조합할지를 다룬다. 기능이 변해도 구조를 유연하게 유지하는 것이 핵심이다.
이 글에서는 다룬 세 가지 Structural Pattern을 다룬다
- Composite
- Facade
- Decorator
1. Composite Pattern
문제
PowerPoint나 Keynote에서 도형 그룹 기능을 써본 적 있을 것이다. 원, 사각형, 삼각형을 따로따로 선택해서 이동시킬 수도 있고, 세 개를 묶어 그룹으로 만든 뒤 한 번에 이동시킬 수도 있다.
코드로 구현한다면 어떻게 할까? 단순하게 생각하면 이렇다.
void move(Object obj) { if (obj instanceof Circle) { ((Circle) obj).move(); } else if (obj instanceof ShapeGroup) { for (Shape s : ((ShapeGroup) obj).getChildren()) { move(s); // 재귀 } } }객체가 단일 도형인지 그룹인지 매번 타입을 확인해야 한다. 새로운 도형이 추가될 때마다 이 조건문을 수정해야 하고, 코드가 점점 복잡해진다.
문제의 핵심은 단일 객체와 복합 객체를 다르게 취급하고 있다는 점이다.
해결
Composite Pattern의 아이디어는 단순하다. 단일 객체와 복합 객체가 같은 인터페이스를 구현하게 만드는 것이다.
interface Shape { void resize(double scale); } class Circle implements Shape { public void resize(double scale) { System.out.println("Resize circle by " + scale); } } class Rectangle implements Shape { public void resize(double scale) { System.out.println("Resize rectangle by " + scale); } } class ShapeGroup implements Shape { private List<Shape> children = new ArrayList<>(); public void add(Shape s) { children.add(s); } public void resize(double scale) { for (Shape child : children) { child.resize(scale); // 자식에게 위임 } } }클라이언트 입장에서는 Shape 인터페이스만 알면 된다. 단일 도형인지 그룹인지 신경 쓸 필요가 없다.
Shape rect1 = new Rectangle(); Shape circle1 = new Circle(); ShapeGroup g1 = new ShapeGroup(); g1.add(rect1); g1.add(circle1); ShapeGroup g2 = new ShapeGroup(); g2.add(g1); // 그룹 안에 그룹 g2.add(new Rectangle()); // 클라이언트는 그냥 Shape로 다룬다 List<Shape> shapes = List.of(rect1, g1, g2); for (Shape s : shapes) { s.resize(1.5); // 단일이든 그룹이든 동일하게 호출 }g2.resize(1.5)를 호출하면 g2가 자신의 자식들에게 resize를 위임하고, g1도 자신의 자식들에게 위임한다. 자연스럽게 트리 구조를 재귀적으로 순회하게 된다.
주의할 점
Composite Pattern은 시스템이 트리 구조로 표현 가능할 때만 의미가 있다. 트리가 아닌 구조에 억지로 적용하면 오히려 복잡해진다. 파일 시스템(파일과 폴더), UI 컴포넌트(버튼과 패널), 조직도(직원과 팀) 같은 구조가 전형적인 적용 사례다.
2. Facade Pattern
문제
카페 주문 시스템을 만든다고 가정한다. 주문 하나를 처리하려면 재고 확인, 결제, 영수증 발송이 필요하다. 클라이언트가 이걸 직접 처리하면 이렇게 된다.
Inventory inventory = new Inventory(); Payment payment = new Payment(); Notification notification = new Notification(); if (inventory.check(order)) { payment.charge(order); notification.sendReceipt(order); }지금은 세 개지만 시스템이 커지면 클라이언트가 더 많은 내부 모듈을 알아야 한다.
문제는 두 가지다.
- 이해하기 어려워진다. 주문 하나 처리하는 데 필요한 맥락을 클라이언트가 전부 파악해야 한다.
- 변경이 어려워진다. 나중에 결제 라이브러리를 바꾸거나 알림 방식을 변경하면, 이 코드를 직접 쓴 모든 클라이언트를 수정해야 한다. 결론적으로 결합도가 높아진 것이다.
해결
Facade는 건물의 정면(외관)이라는 뜻이다. 복잡한 내부 구조를 숨기고 단순한 인터페이스 하나만 바깥에 내놓는다.
class OrderService { // Facade private Inventory inventory = new Inventory(); private Payment payment = new Payment(); private Notification notification = new Notification(); public void placeOrder(Order order) { if (inventory.check(order)) { payment.charge(order); notification.sendReceipt(order); } } } // 클라이언트 OrderService orderService = new OrderService(); orderService.placeOrder(order);클라이언트는 placeOrder() 하나만 알면 된다. 내부에서 재고 확인, 결제, 알림이 어떤 순서로 어떻게 처리되는지 몰라도 된다.
나중에 결제 모듈을 바꾸더라도 OrderService 내부만 수정하면 되고, 클라이언트 코드는 손댈 필요가 없다.
Facade는 은닉이지 제한이 아니다
중요한 점은 Facade가 내부 모듈에 대한 접근을 막는 게 아니라는 것이다. 정말 내부 기능에 직접 접근해야 하는 경우라면 여전히 Inventory나 Payment를 직접 써도 된다. Facade는 편의를 위한 단순화된 인터페이스를 제공하는 것이지, 내부를 잠가버리는 게 아니다.
또한 복잡한 시스템이라면 Facade를 하나가 아닌 여럿으로 분리할 수도 있다. OrderFacade, InventoryFacade처럼 각 도메인별로 나누면 각 Facade가 관련 있는 모듈들만 조합하게 된다.
3. Decorator Pattern
문제

알림 시스템을 만든다고 하자. Email, SMS, Slack, Twitter 등 다양한 채널이 있고, 사용자마다 여러 채널 조합을 선택할 수 있다.
상속으로 해결하려 하면 이렇게 된다.
채널이 4개만 돼도 조합이 수십 개다. 채널 하나가 추가될 때마다 기존 조합 수만큼 새 클래스가 필요하다. 이건 유지보수가 불가능한 구조다.
왜 이렇게 됐을까? 상속은 컴파일 시점에 클래스 관계가 고정된다. "Email + SMS"를 나타내려면 그 조합을 위한 클래스를 미리 만들어 놓아야 한다. 상속은 변형의 조합을 표현하는 데 약하다.
해결
Decorator Pattern은 상속 대신 객체를 감싸는(wrapping) 방식으로 기능을 추가한다.
각 추가 기능을 별도의 Decorator 클래스로 만들고, 원본 객체를 감싸게 한다. Decorator는 원본 객체와 같은 인터페이스를 구현하고, 자신의 기능을 수행한 뒤 원본 객체의 메서드를 호출(위임)한다.
커피 가격 계산 예시로 보면 더 명확하다.
interface Coffee { String getDescription(); double cost(); } // 기본 음료 class Americano implements Coffee { public String getDescription() { return "Americano"; } public double cost() { return 2.0; } } // Decorator class MilkDecorator implements Coffee { private Coffee base; public MilkDecorator(Coffee base) { this.base = base; } public String getDescription() { return base.getDescription() + ", Milk"; // 위임 후 추가 } public double cost() { return base.cost() + 0.5; // 위임 후 추가 } } class WhippedCreamDecorator implements Coffee { private Coffee base; public WhippedCreamDecorator(Coffee base) { this.base = base; } public String getDescription() { return base.getDescription() + ", Whipped Cream"; } public double cost() { return base.cost() + 0.7; } }클라이언트에서 조합을 만드는 방법이 핵심이다.
Coffee c = new Americano(); c = new MilkDecorator(c); // 아메리카노를 Milk로 감쌈 c = new WhippedCreamDecorator(c); // 다시 Whipped Cream으로 감쌈 System.out.println(c.getDescription()); // Americano, Milk, Whipped Cream System.out.println(c.cost()); // 2.0 + 0.5 + 0.7 = 3.2c.cost()를 호출하면 WhippedCreamDecorator가 MilkDecorator에게 위임하고, MilkDecorator가 Americano에게 위임한다. 각 단계에서 자신의 가격을 더해서 결과가 쌓인다. 러시아 인형처럼 겹겹이 감싸는 구조다.
알림 시스템에도 같은 방식을 적용하면, Email + SMS + Slack 조합은 클래스를 새로 만드는 게 아니라 런타임에 Decorator를 세 번 감싸는 것으로 표현된다.
상속 대신 조합
상속은 기능을 추가하는 방법이지만, 조합 수가 늘어날수록 클래스 폭발(class explosion)이 일어난다. Decorator는 기능 추가를 객체 조합으로 표현해서 이 문제를 피한다.
단, Decorator가 많이 중첩될수록 코드를 따라가기 어려워진다는 단점이 있다. 어떤 Decorator가 몇 겹 감겨 있는지 추적하기가 까다롭기 때문에, 디버깅할 때 불편함이 생길 수 있다.
정리
세 패턴 모두 "유연한 구조"를 만들기 위한 것이지만, 각각 해결하는 문제가 다르다.
Composite Facade Decorator 핵심 문제 단일/복합 객체를 다르게 다뤄야 하는 번거로움 복잡한 내부 구조에 대한 높은 결합도 기능 조합의 수가 늘어나는 상속 구조 해결 방식 단일/복합에 같은 인터페이스 부여 복잡한 내부를 단순한 인터페이스로 은닉 객체를 감싸는 방식으로 기능을 런타임에 추가 구조적 특징 트리 구조 + 재귀 위임 여러 모듈을 하나의 진입점으로 묶음 같은 인터페이스를 구현한 객체가 다른 객체를 감쌈 Structural Pattern을 관통하는 공통 원칙 하나를 꼽자면, 인터페이스로 복잡성을 숨기고 위임으로 동작을 연결하라는 것이다. 구체적인 구현에 직접 의존하는 대신, 추상화 뒤에서 구조를 유연하게 조합한다.
출처: 경북대학교 손정주 교수님, "소프트웨어공학" 강의 자료
'CS > Software Engineering' 카테고리의 다른 글
[Software Engineering] 버전 관리 시스템(VCS): Git 핵심 (1) 2026.04.11 [Software Engineering] Design Pattern: Behavioral Patterns (1) 2026.04.10 [Software Engineering] Design Pattern: Creational Patterns (1) 2026.04.08 [Software Engineering] SW Architecture (0) 2026.04.08 [Software Engineering] Behavioral Modeling: 시스템의 내부 동작과 객체 협력 이해하기 (0) 2025.10.27