데코레이터 패턴 (Decorator pattern)
- 래퍼 객체를 이용해 모듈과 비슷한 방식으로 기존 객체에 기능을 추가할 수 있다.
- 기존 기능에 영향을 주지 않고 가능하다.
- 런타임에 객체에 '행위' 혹은 '기능'을 추가할 수 있게 해준다.
기존 객체
를'행위'를 가진 특별한 래퍼 객체 (데코레이터)
에 넣어서 객체가 그 '행위'를 할 수 있게 만든다.- 캐싱, 로깅, 검증과 같은 기능에 쓰일 수 있다.
피자 클래스 데코레이터 패턴 적용 예제
- 피자를 클래스로 매핑하여 만들어보려고 한다.
데코레이터 패턴 적용 전
- 데코레이터 패턴 적용 전의 코드를 보자
Pizza
생성
public class Pizza {
protected String pizzaName() {
return "피자";
}
}
- 일반
피자
를 만들었다.
CheesePizza
, BulgogiPizza
추가
- 치즈 피자와 불고기 피자를 만들어야 하는 요구사항이 들어왔다.
- 상속을 이용해 간단히 구현했다.
public class CheesePizza extends Pizza {
@Override
protected String pizzaName() {
return "치즈 " + super.pizzaName();
}
}
public class BulgogiPizza extends Pizza {
@Override
protected String pizzaName() {
return "불고기 " + super.pizzaName();
}
}
- 이렇게
치즈 피자
와불고기 피자
도 만들었는데, - 손님들의 반응이 좋아서
치즈 불고기 피자
를 만들고 싶다. - 기존의 상속 방식을 그대로 이용하려면,
치즈 피자
를 상속해서 만들거나불고기 피자
를 상속해서치즈 불고기 피자
클래스를 만들 수는 있다. - 갑자기, '나중에
씬도우
,치즈 크러스트
등의 피자가 추가되면,치즈크러스트 치즈 불고기 피자
,씬도우 치즈크러스트 치즈 불고기 피자
,씬도우 불고기 피자
,씬도우 치즈 피자
등 수 없이 많은 새로운 클래스를 만들어야될까?' 라는 생각이 들었다. - 이 문제를 해결하기 위해 데코레이터 패턴을 적용하기로 했다.
데코레이터 패턴 적용 후
- 아래는 데코레이터 패턴을 적용한 코드이다.
PizzaService
인터페이스 생성
public interface PizzaService {
String pizzaName();
}
- 모든 피자에서 수행할 서비스를 정의하는 인터페이스이다.
DefaultPizza
클래스 생성
public class DefaultPizza implements PizzaService{
@Override
public String pizzaName() {
return "피자";
}
}
- 기본 피자이다.
PizzaDecorator
추상 클래스 생성
public abstract class PizzaDecorator implements PizzaService {
PizzaService pizzaService;
public PizzaDecorator(PizzaService pizzaService) {
this.pizzaService = pizzaService;
}
@Override
public String pizzaName() {
return pizzaService.pizzaName();
}
}
- 피자를 꾸며주는
PizzaDecorator
추상 클래스이다. - 추상 클래스인만큼 자체적으로 인스턴스가 될 수는 없다.
CheeseDecorator
클래스 생성
public class CheeseDecorator extends PizzaDecorator{
public CheeseDecorator(PizzaService pizzaService) {
super(pizzaService);
}
@Override
public String pizzaName() {
return "치즈 " + super.pizzaName();
}
}
- 치즈를 추가할 때 사용한다.
BulgogiDecorator
클래스 생성
public class BulgogiDecorator extends PizzaDecorator{
public BulgogiDecorator(PizzaService pizzaService) {
super(pizzaService);
}
@Override
public String pizzaName() {
return "불고기 " + super.pizzaName();
}
}
- 불고기를 추가할 때 사용한다.
Client
코드 생성
public class Client {
private static boolean enabledBulgogi = true;
private static boolean enabledCheese = true;
public static void main(String[] args) {
PizzaService pizza = new DefaultPizza();
if (enabledBulgogi) {
pizza = new BulgogiDecorator(pizza);
}
if(enabledCheese) {
pizza = new CheeseDecorator(pizza);
}
System.out.println(pizza.pizzaName());
}
}
- 불고기가 활성화 되어있으면,
PizzaService
타입에 데코레이터 클래스를 랩핑해서 새로운PizzaService
를 반환받는 식으로 데코레이터 패턴을 구현했다. - 이제 런타임에 마음껏 피자 토핑을 추가할 수 있다.
- 데코레이터가 데코레이터를 감싸는 게 어찌보면 유연한 느낌의 상속처럼 바라볼 수도 있다.
다이어그램으로 살펴보기
Component
가 이전의PizzaService
에 해당한다.Concrete Component
는DefaultPizza
이다.Decorator
는PizzaDecorator
Concrete Decorator
가 각각CheeseDecorator
,BulgogiDecorator
이다.
현실 예제
- 알람 서비스와 관련된 현실 예제를 살펴보자.
초기 상황
- 이메일 알람 서비스를 제공하는 일을 하는 회사에서 근무 중이다.
Notifier
라는 기존 클래스가 존재한다.- 등록된 회원의 이메일 주소에 일괄적으로 이메일을 보내주는 역할을 한다.
- 클라이언트는 초기 세팅 후
Notifier
클래스의send()
메서드를 이메일을 마음껏 보낼 수 있었다.
Notifier
의 현재 소스코드
public class Notifier {
private final ArrayList<String> emails = new ArrayList<>();
public void addEmail(String email) {
emails.add(email);
System.out.println("이메일 \"" + email + "\" 가 성공적으로 수신 이메일 목록에 추가되었습니다.");
}
public void send(String message) {
for (String email : emails) {
sendEmail(email, message);
}
}
private void sendEmail(String email, String message) {
System.out.println("이메일 주소: \"" + email + "\" 로 내용: \"" + message + "\" 을 보냈습니다.");
}
}
Client
의 현재 소스코드
public class Client {
public static void main(String[] args) {
Notifier notifier = new Notifier();
notifier.addEmail("n00nietzsche@gmail.com");
notifier.addEmail("billgates@microsoft.com");
notifier.send("하이.");
}
}
/*
출력 결과:
이메일 "n00nietzsche@gmail.com" 가 성공적으로 수신 이메일 목록에 추가되었습니다.
이메일 "billgates@microsoft.com" 가 성공적으로 수신 이메일 목록에 추가되었습니다.
이메일 주소: "n00nietzsche@gmail.com" 로 내용: "안녕하세요." 을 보냈습니다.
이메일 주소: "billgates@microsoft.com" 로 내용: "안녕하세요." 을 보냈습니다.
*/
추가 요구사항
- 시대가 변하면서 알람 시스템에도 새로운 요구사항이 들어왔다.
- 많은 알람 채널을 지원해야될 필요가 있었다.
- 문자 메세지, 슬랙, 페이스북과 같은 메세지도 보낼 수 있도록 해달라는 요구사항이었다.
- 새로운 메세징 시스템이 지속적으로 추가되고 있어 이에 대비할 필요도 생겼다.
- ex) 디스코드, 트위터, 마스토돈 등...
상속을 사용할까?
- 처음에는 상속을 통해 구현하는 것을 고려할 수 있다.
Notifier
를 상속하는SmsNotifier
,FacebookNotifier
,SlackNotifier
와 같은 서브 클래스를 생성하여 구현할 수 있다.- 상속이 갖는 치명적인 단점들이 꽤 있기 때문에 되도록 1차원적 상속을 자제하고 최소한 합성을 사용하려 한다.
- 그리고 프로그램을 실행 중인 런타임에 알람 채널을 바꿀 수 있으면 좋겠다는 생각이 들어서 1차원적 상속은 확실히 제외하기로 했다.
- 그리고 사용자들에게는 적은 이질감을 느끼게 만들기 위해 최소한의 클라이언트 코드만 바꾸고 싶다.
데코레이터 패턴 구현하기
- 데코레이터 패턴을 구현하여 위의 요구사항을 충족해보자
NotifierInterface
생성
public interface NotifierInterface {
void send(String message);
}
- 현재
Notifier
구현에서Notifier
의 종류에 따라 유일하게 달라질 수 있는 부분은send()
메서드이다.
NotifierBaseDecorator
추상 클래스 생성
public abstract class NotifierBaseDecorator implements NotifierInterface {
public final Notifier notifier;
public NotifierBaseDecorator(Notifier notifier) {
this.notifier = notifier;
}
}
- 베이스 데코레이터의 역할을 할 추상 클래스이다.
- 주입된
Notifier
가 가지는 모든 메서드나 필드를 '사용'할 수 있다.- 상속 관계와 비교되는 합성 관계를 사용한 것이다.
- 생성자를 통해 기존의
Notifier
를 감싸면,Notifier
클래스를 멤버로 넣고 이에 변화를 줄 수 있는 방식으로 구현했다.- 이 추상 클래스를 상속하여 구현한 클래스가 말 그대로
행위를 가진 특별한 래퍼 객체
가 될 것이다.
- 이 추상 클래스를 상속하여 구현한 클래스가 말 그대로
Notifier
내부에ArrayList
형태로notifiers
라는 멤버를 만들어 거기에 데코레이터들을 보관할 것이다.
EmailNotifierDecorator
생성
public class EmailNotifierDecorator extends NotifierBaseDecorator {
public EmailNotifierDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
for (String email : this.notifier.getEmails()) {
System.out.println("[이메일 발신]" + email + "\" 로 내용: \"" + message + "\" 을 보냈습니다.");
}
}
}
- 이메일을 보내는데 사용될 데코레이터이다.
SlackNotifierDecorator
생성
public class SlackNotifierDecorator extends NotifierBaseDecorator {
public SlackNotifierDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
for (String email : this.notifier.getEmails()) {
System.out.println("[슬랙 메세지 발신]" + email + "\" 로 내용: \"" + message + "\" 을 보냈습니다.");
}
}
}
- 슬랙 메세지를 보내는데 사용될 데코레이터이다.
FacebookNotifierDecorator
생성
public class FacebookNotifierDecorator extends NotifierBaseDecorator {
public FacebookNotifierDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
for (String email : this.notifier.getEmails()) {
System.out.println("[페이스북 메세지 발신]" + email + "\" 로 내용: \"" + message + "\" 을 보냈습니다.");
}
}
}
- 페이스북 메세지를 보내는데 사용될 데코레이터이다.
Notifier
코드 수정
public class Notifier implements NotifierInterface {
private final ArrayList<NotifierInterface> notifiers = new ArrayList<>();
private final ArrayList<String> emails = new ArrayList<>();
// 데코레이터 이용 방법 1
public void decorate(Class<? extends NotifierBaseDecorator> decorator) {
if(containNotifier(decorator)) {
System.out.println("이미 추가된 발신 타입입니다.");
return;
}
Constructor<? extends NotifierBaseDecorator> constructor = null;
try {
constructor = decorator.getConstructor(Notifier.class);
addNotifier(constructor.newInstance(this));
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
// 데코레이터 이용 방법 2
public void facebookEnabled(boolean bool) {
if (bool) {
if(containNotifier(FacebookNotifierDecorator.class)) {
System.out.println("이미 활성화된 상태입니다.");
return;
}
addNotifier(new FacebookNotifierDecorator(this));
return;
}
removeNotifier(FacebookNotifierDecorator.class);
}
// 기타 나머지 코드들...
}
- 데코레이터 패턴을 적용하기 위해
decorate
메서드와facebookEnabled
메서드를 시범삼아 구현해보았다. decorate
메서드는 리플렉트를 통한 일반화 방식이고,NotifierBaseDecorator
를 상속한 어떤 클래스든 넣을 수 있다.facebookEnabled
는 유저 친화적인 방식으로 메서드를 구현한 것이다.
Notifier
의 전체 코드
public class Notifier implements NotifierInterface {
private final ArrayList<NotifierInterface> notifiers = new ArrayList<>();
private final ArrayList<String> emails = new ArrayList<>();
public ArrayList<String> getEmails() {
return emails;
}
private void addNotifier(NotifierInterface notifier) {
notifiers.add(notifier);
}
private void removeNotifier(Class<?> notifierClass) {
NotifierInterface foundNotifier = findNotifier(notifierClass);
if(foundNotifier != null) {
notifiers.remove(foundNotifier);
}
}
private NotifierInterface findNotifier(Class<?> notifierClass) {
for (NotifierInterface notifier : notifiers) {
if(notifier.getClass().equals(notifierClass)) {
return notifier;
}
}
return null;
}
private boolean containNotifier(Class<?> notifierClass) {
for (NotifierInterface notifier : notifiers) {
if(notifier.getClass().equals(notifierClass)) {
return true;
}
}
return false;
}
public void decorate(Class<? extends NotifierBaseDecorator> decorator) {
if(containNotifier(decorator)) {
System.out.println("이미 추가된 발신 타입입니다.");
return;
}
Constructor<? extends NotifierBaseDecorator> constructor = null;
try {
constructor = decorator.getConstructor(Notifier.class);
addNotifier(constructor.newInstance(this));
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void facebookEnabled(boolean bool) {
if (bool) {
if(containNotifier(FacebookNotifierDecorator.class)) {
System.out.println("이미 활성화된 상태입니다.");
return;
}
addNotifier(new FacebookNotifierDecorator(this));
return;
}
removeNotifier(FacebookNotifierDecorator.class);
}
public final void addEmail(String email) {
emails.add(email);
System.out.println("이메일 \"" + email + "\" 가 성공적으로 수신 목록에 추가되었습니다.");
}
@Override
public void send(String message) {
if(notifiers.isEmpty()) {
System.out.println("등록된 NotifierInterface 가 없습니다.");
return;
}
if(emails.isEmpty()) {
System.out.println("등록된 수신자가 없습니다.");
return;
}
for (NotifierInterface notifier : notifiers) {
notifier.send(message);
}
}
}
Client
코드에서 결과 살펴보기
public class Client {
public static void main(String[] args) {
Notifier notifier = new Notifier();
notifier.addEmail("n00nietzsche@gmail.com");
notifier.addEmail("billgates@microsoft.com");
// 사용자 친화적인 방법
notifier.facebookEnabled(true);
// 공통화를 쉽게 할 수 있는 방법
notifier.decorate(EmailNotifierDecorator.class);
notifier.decorate(SlackNotifierDecorator.class);
notifier.send("하이.");
}
}
/*
출력 결과:
이메일 "n00nietzsche@gmail.com" 가 성공적으로 수신 목록에 추가되었습니다.
이메일 "billgates@microsoft.com" 가 성공적으로 수신 목록에 추가되었습니다.
[페이스북 메세지 발신]n00nietzsche@gmail.com" 로 내용: "하이." 을 보냈습니다.
[페이스북 메세지 발신]billgates@microsoft.com" 로 내용: "하이." 을 보냈습니다.
[이메일 발신]n00nietzsche@gmail.com" 로 내용: "하이." 을 보냈습니다.
[이메일 발신]billgates@microsoft.com" 로 내용: "하이." 을 보냈습니다.
[슬랙 메세지 발신]n00nietzsche@gmail.com" 로 내용: "하이." 을 보냈습니다.
[슬랙 메세지 발신]billgates@microsoft.com" 로 내용: "하이." 을 보냈습니다.
*/
- 출력 결과 주석을 보았을 때, 정상적으로 작동하는 것을 볼 수 있다.
- 지금은 단순히 메일 전송을 위한
send()
메서드 하나 뿐이라 그냥 한 클래스 안에서 전부 구현해도 크게 복잡하지 않다.- 나중에 '푸시알람 보내기', '그룹에 초대하기' 등의 여러가지 기능이 더 생기고 디스코드, 마스토돈, 트위터 등 많은 SNS 가 부가적으로 생겨서 인터페이스에 메서드가 많이 추가된다면,
Notifier
하나로는 매우 복잡했을 구현을 손쉽게 가져갈 수 있다.
- 나중에 '푸시알람 보내기', '그룹에 초대하기' 등의 여러가지 기능이 더 생기고 디스코드, 마스토돈, 트위터 등 많은 SNS 가 부가적으로 생겨서 인터페이스에 메서드가 많이 추가된다면,
- 코드의 복잡도 뿐만 아니라, 일반적인 1차원적 상속과 달리 런타임에 보내는 SNS 목록등을 쉽게 편집할 수 있는 것도 큰 장점이다.
다이어그램 살펴보기
- 코드는
Pizza
보다 복잡했지만, 다이어그램을 보면Pizza
예제와 거의 흡사하다.
데코레이터 패턴의 장점과 단점
- 데코레이터 패턴의 장점과 단점을 알아보자.
장점
- 데코레이터 (래퍼 클래스) 를 이용해 기능을 조합할 수 있다.
- 상속을 이용하는 경우엔 '조합'은 불가능하여 유연하지 못했다.
- 조합의 순서도 상관없이 조합이 가능하다.
- 컴파일 타임에 기능의 내용이 확정되는 것이 아니라 런타임에 기능의 내용을 변경할 수 있다.
- 상속의 경우에는 이미 컴파일 타임에 관계가 확정되어 있다.
단점
- 데코레이터를 조합하는 코드가 조금 복잡할 수 있다.
- 작은 객체들이 많이 늘어난다.
- 사실 상속에 비하면 객체들이 상속보다 많이 늘어나진 않아서 큰 단점은 아니다.
스프링에서 사용되는 데코레이터 패턴
- 스프링에서의
HandlerAdapter
를 데코레이터 패턴의 예로 볼 수 있다. - 데코레이터 패턴의 특징은 래퍼 객체를 이용해 기존 객체에 영향을 미치지 않고 새로운 기능을 추가하는 것이라 볼 수 있다.
HandlerAdapter
는 스프링에서 특정한 타입의 컨트롤러를 디스패처 서블릿에 사용할 수 있게 해준다.SimpleControllerHandlerAdapter
는Controller
인터페이스를 구현한 컨트롤러를 사용할 수 있게 해준다.RequestMappingHandlerAdapter
는@RequestMapping
주석이 달린 컨트롤러를 사용할 수 있게 해준다.
- 각
HandlerAdapter
들은 기존의 동작을 해치지 않고, 새로운 종류의 컨트롤러가 동작할 수 있도록 도와준다.
/** List of HandlerAdapters used by this servlet. */
@Nullable
private List<HandlerAdapter> handlerAdapters;
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
HandlerAdapter
를 리스트 형태로 보관하여, 알맞은handlerAdapter
를 사용할 수 있게 해준다.supports
메서드는 초기 다이어그램에서Component
인터페이스가 제공하는 부분에 해당한다.HandlerAdapter
인터페이스 자체가Component
인터페이스 격이다.
SimpleControllerHandlerAdapter
혹은RequestMappingHandlerAdapter
가Concrete Decorator
로 볼 수 있다.
데코레이터 패턴 vs 어댑터 패턴
HandlerAdapter
는 어댑터 패턴에서도 예제로 등장했던 코드이다.- 코드를 보는 시선에 따라 어댑터 패턴과 데코레이터 패턴 둘 다 될 수 있다.
HandlerAdapter
의 의도를서로 다른 코드와의 호환
이라고 본다면, 어댑터 패턴이 된다.HandlerAdapter
에 어댑터를 추가하여기존 객체에 영향을 미치지 않고 새로운 기능을 추가하는 것
이라고 본다면, 데코레이터 패턴이 된다.
자바에서 사용되는 데코레이터 패턴
Collections
의 다양한 정적 메서드들
아래의 Collections
메서드는 Collection
객체 안에 있는 내용이나 기존 기능을 전혀 변경하지 않고도 새로운 기능을 추가할 수 있다.
synchronizedList()
:Collection
을 thread-safe 하게 만든다.checkedList()
:Collection
에 특정 타입만 들어올 수 있게 만든다.unmodifiableList()
:Collection
을 더이상 수정될 수 없는 불변으로 만든다.
데코레이터 패턴의 예제인만큼 한 번에 한 가지만 적용할 수 있는게 아니라, 여러가지를 한 Collection
에 적용하는 것도 가능하다.
HttpServletRequestWrapper
혹은 HttpServletResponseWrapper
- 이름부터 래퍼 객체임을 명확히 알리고 있다.
- 기본 자바 서블릿 API 인
HttpServletRequest
에서 넘어온 정보를HttpServletRequestWrapper
래퍼 객체로 감싸 애플리케이션 단에서 입맛에 맞게 수정하기 위한 기능들을 추가해준다. - 초기 서블릿 요청에 대한 내용을 편집할 수 있다.
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
플라이웨이트 패턴 (Flyweight Pattern) 이란? (0) | 2023.04.22 |
---|---|
퍼사드 패턴 (Facade Pattern) 이란? (0) | 2023.04.18 |
컴포지트 패턴 (Composite Pattern, 컴포짓 패턴) 이란? (0) | 2023.02.20 |
브릿지 패턴 (Bridge Pattern) 이란? (0) | 2023.02.19 |
자바 디자인 패턴, 객체 생성 관련 패턴 (Object Creational Patterns) 이란? (0) | 2023.02.17 |