Java Decorator 패턴 사례, 실제 코드로 배우는 구조 패턴 활용법
☕ 실무에서 바로 써먹는 객체지향 설계의 숨은 보석
코드를 작성하다 보면 새로운 기능을 추가해야 하는 순간이 자주 찾아옵니다.
그때마다 기존 클래스를 수정하거나 상속 계층을 늘리는 방식으로 해결하면, 코드 구조가 점점 복잡해지고 유지보수가 어려워집니다.
이런 문제를 우아하게 해결할 수 있는 방법이 바로 Decorator 패턴입니다.
객체를 감싸는 방식으로 기능을 동적으로 확장할 수 있어, 상속의 단점을 피하면서도 유연한 설계가 가능합니다.
이번 글에서는 Java에서의 Decorator 패턴 사례를 구체적인 코드와 함께 소개합니다.
커피 주문 시스템부터 Java I/O 스트림까지, 다양한 예제를 통해 이 패턴이 어떻게 실무에서 쓰이는지 살펴볼 예정입니다.
초보자도 이해할 수 있도록 차근차근 설명하니, 디자인 패턴 학습 중이거나 유지보수성이 높은 코드를 만들고 싶은 분들에게 유용할 것입니다.
📋 목차
🔗 Decorator 패턴의 기본 개념 복습
Decorator 패턴은 기존 객체를 수정하지 않고도 새로운 기능을 동적으로 추가할 수 있는 구조 패턴(Structural Pattern)입니다.
기존 클래스의 기능을 확장하기 위해 상속을 사용하는 대신, 객체를 감싸는 방법을 선택하는 것이 특징입니다.
이를 통해 코드 변경 없이 새로운 동작을 손쉽게 추가할 수 있으며, 클래스 폭발 문제를 방지할 수 있습니다.
이 패턴의 핵심 구성 요소는 다음과 같습니다.
먼저 Component 인터페이스 또는 추상 클래스가 있습니다.
그 다음 ConcreteComponent는 실제 기능을 수행하는 클래스이며,
마지막으로 Decorator는 Component를 구현하고 내부에 Component 인스턴스를 보관하여 새로운 기능을 추가합니다.
🧩 동작 방식
Decorator 패턴은 실행 중에 객체를 감싸며, 원본 메서드를 호출한 전후에 추가 동작을 수행합니다.
여러 개의 Decorator를 체인처럼 연결할 수 있어, 기능 확장이 매우 유연합니다.
예를 들어, 커피 객체에 우유, 시럽, 휘핑크림을 순서대로 감싸는 방식으로 다양한 메뉴를 만들 수 있습니다.
// Component
public interface Coffee {
String getDescription();
double cost();
}
// ConcreteComponent
public class BasicCoffee implements Coffee {
public String getDescription() { return "Basic Coffee"; }
public double cost() { return 2.0; }
}
// Decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
이 기본 구조를 이해하면, 이후 실제 사례에서 Decorator 패턴이 어떻게 동작하는지 훨씬 쉽게 파악할 수 있습니다.
다음 섹션에서는 Java 표준 라이브러리 속 Decorator 패턴의 대표적인 예시를 살펴보겠습니다.
🛠️ Java I/O 스트림 속 Decorator 패턴
Java 표준 라이브러리에는 Decorator 패턴이 이미 널리 활용되고 있습니다.
대표적인 예가 java.io 패키지의 스트림 클래스입니다.
이 구조에서는 기본 스트림이 데이터의 읽기·쓰기 기능을 제공하고, 다양한 보조 스트림이 기능을 확장합니다.
예를 들어, BufferedInputStream은 InputStream을 감싸서 버퍼링 기능을 추가합니다.
📂 BufferedInputStream 예제
아래 코드는 파일을 읽을 때 버퍼링 기능을 추가하는 예시입니다.
BufferedInputStream이 FileInputStream을 감싸며, 읽기 성능이 향상됩니다.
try (InputStream input =
new BufferedInputStream(
new FileInputStream("data.txt"))) {
int data;
while ((data = input.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
이 예제에서 FileInputStream은 기본 파일 읽기 기능만 제공합니다.
하지만 BufferedInputStream이 이를 감싸면, 데이터 읽기를 효율적으로 처리할 수 있게 됩니다.
이처럼 Decorator 패턴은 기본 기능을 변경하지 않고도 부가 기능을 추가하는 데 최적화되어 있습니다.
💎 핵심 포인트:
Java의 스트림 구조는 Decorator 패턴의 전형적인 사례이며, 기능 확장을 위해 상속 대신 객체 조합을 활용한 대표적인 예시입니다.
⚙️ 커피 주문 시스템 예제
Decorator 패턴을 가장 직관적으로 이해할 수 있는 예시 중 하나가 바로 커피 주문 시스템입니다.
기본 커피에 우유, 시럽, 휘핑크림 등의 옵션을 자유롭게 추가하는 구조는 Decorator 패턴의 핵심 개념과 완벽히 일치합니다.
각 옵션은 독립된 데코레이터 클래스로 구현되어, 필요에 따라 원하는 만큼 조합할 수 있습니다.
☕ 커피 예제 코드
// Component
public interface Coffee {
String getDescription();
double cost();
}
// Concrete Component
public class BasicCoffee implements Coffee {
public String getDescription() { return "Basic Coffee"; }
public double cost() { return 2.0; }
}
// Base Decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
public String getDescription() { return coffee.getDescription(); }
public double cost() { return coffee.cost(); }
}
// Milk Decorator
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public String getDescription() { return super.getDescription() + ", Milk"; }
public double cost() { return super.cost() + 0.5; }
}
// Syrup Decorator
public class SyrupDecorator extends CoffeeDecorator {
public SyrupDecorator(Coffee coffee) { super(coffee); }
public String getDescription() { return super.getDescription() + ", Syrup"; }
public double cost() { return super.cost() + 0.3; }
}
// Usage
Coffee order = new SyrupDecorator(new MilkDecorator(new BasicCoffee()));
System.out.println(order.getDescription() + " $" + order.cost());
이 구조에서는 새로운 옵션이 필요할 때마다 Decorator 클래스를 하나씩 추가하면 됩니다.
기존 코드를 수정하지 않고도 무한한 조합이 가능하며, 이로 인해 OCP(개방-폐쇄 원칙)을 완벽하게 지킬 수 있습니다.
💡 TIP: 이 방식은 단순한 예제지만, 실무에서 기능 확장이 잦은 서비스 구조에 그대로 적용할 수 있습니다.
🔌 웹 애플리케이션에서의 Decorator 활용
웹 애플리케이션 개발에서도 Decorator 패턴은 매우 유용하게 쓰입니다.
특히 필터링, 로깅, 권한 검증, 데이터 포맷 변환 등 공통 기능을 추가할 때 Decorator 방식이 코드 품질을 높입니다.
Spring Framework나 Servlet 필터 구조에서도 이 개념이 자주 등장합니다.
🌐 Servlet 필터 예시
Java Servlet의 Filter는 요청과 응답 객체를 감싸서 처리하는 구조로, Decorator 패턴의 아이디어를 활용합니다.
아래 예시는 요청 처리 전에 로깅을 수행하고, 이후 체인을 따라 다음 필터나 서블릿으로 전달하는 구조입니다.
@WebFilter("/*")
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Request received at " + new Date());
// 다음 필터 또는 서블릿 호출
chain.doFilter(request, response);
}
}
이 구조는 요청과 응답을 변경하지 않고, 원하는 기능을 런타임에 동적으로 추가할 수 있습니다.
필요한 기능별로 필터를 나누면, 코드가 깔끔해지고 재사용성이 높아집니다.
⚙️ Spring AOP와의 유사성
Spring의 AOP(관점 지향 프로그래밍)도 Decorator 패턴과 유사한 원리를 활용합니다.
핵심 기능 코드에 직접 손대지 않고, 부가 기능을 메서드 실행 전후에 주입하는 방식이기 때문에 설계 철학이 동일합니다.
💎 핵심 포인트:
웹 개발에서 Decorator 패턴은 유지보수성과 모듈성을 높이는 데 핵심적인 역할을 하며, 필터나 AOP 구조에서 자연스럽게 적용됩니다.
💡 Decorator 패턴 적용 시 유의사항
Decorator 패턴은 강력한 유연성을 제공하지만, 무분별하게 적용하면 코드 복잡도가 높아질 수 있습니다.
따라서 설계 단계에서 적용 범위와 방식에 대해 신중하게 판단하는 것이 중요합니다.
📏 중첩 구조의 복잡성
Decorator는 여러 개를 체인처럼 연결할 수 있지만, 너무 많은 계층을 쌓으면 디버깅이 어려워집니다.
특히 런타임에 어떤 Decorator가 적용되었는지 파악하기 어려워질 수 있으므로, 필요한 경우에만 사용해야 합니다.
🔍 공통 기능 관리
여러 Decorator에서 동일한 로직이 반복된다면, 이를 상위 Decorator 클래스나 별도의 유틸리티 메서드로 추출해 관리하는 것이 좋습니다.
이렇게 하면 유지보수성과 코드 재사용성이 함께 향상됩니다.
⚠️ 성능 영향
Decorator 패턴은 객체를 여러 겹 감싸는 구조이므로, 호출 경로가 길어질 수 있습니다.
실시간 성능이 중요한 시스템에서는 Decorator 계층 수를 제한하는 것이 바람직합니다.
💎 핵심 포인트:
Decorator 패턴은 적재적소에 사용하면 유지보수성과 확장성을 높일 수 있지만, 남용 시 코드 복잡성과 성능 저하를 유발할 수 있습니다.
💡 TIP: 기능 변경 가능성이 높은 부분에만 선택적으로 적용하면 Decorator의 장점을 극대화할 수 있습니다.
❓ 자주 묻는 질문 (FAQ)
Decorator 패턴은 언제 사용하는 것이 좋나요?
Java에서 Decorator 패턴을 구현한 대표적인 예는 무엇인가요?
Decorator 패턴과 상속의 가장 큰 차이점은 무엇인가요?
Decorator를 여러 개 연결해서 사용해도 되나요?
웹 애플리케이션에서도 Decorator를 활용할 수 있나요?
Decorator 패턴을 적용하면 테스트가 쉬워지나요?
Decorator 패턴의 단점은 무엇인가요?
Decorator 패턴과 Proxy 패턴은 어떻게 다르나요?
🚀 실제 코드로 확인한 Decorator 패턴의 가치
Decorator 패턴은 단순한 이론이 아니라, Java의 표준 라이브러리와 실무 프로젝트에서 활발히 쓰이는 강력한 설계 도구입니다.
이번 글에서 살펴본 Java I/O 스트림과 커피 주문 예제, 그리고 웹 애플리케이션 필터 구조까지 모두 Decorator의 개념을 실감나게 보여줍니다.
이 패턴은 상속의 한계를 극복하고, 기능 변경과 확장을 안전하게 지원합니다.
특히 유지보수성과 유연성을 동시에 확보할 수 있다는 점에서, 변화가 많은 서비스 구조에 적합합니다.
다만, 계층 중첩에 따른 복잡도와 성능 저하 가능성을 항상 고려해야 합니다.
필요한 기능에만 적절히 적용하면 Decorator 패턴은 장기적으로 프로젝트 품질을 높여주는 든든한 도구가 될 것입니다.
🏷️ 관련 태그 : Java디자인패턴, 구조패턴, Decorator패턴, Java예제, 객체지향설계, 코드유연성, 유지보수성, OCP원칙, 디자인패턴사례, 개발팁