-
[디자인패턴] 데코레이터 패턴기타/북스터디 2022. 12. 11. 13:51
데코레이터 패턴이란?
데코레이퍼 패턴이란 기존 코드를 변경하지 않고 부가기능을 추가하는 패턴으로 상속이 아닌 위임을 사용해서 보다 유연하게 부가기능을 추가할 수 있도록 해준다.
우리는 코드를 설계할 때에 보통 공통된 역할을 하는 클래스들이 보인다면 슈퍼클래스를 선언하여 상속을 받는 서브클래스들을 만드는 구조를 먼저 떠올린다.
하지만, 상속을 무분별하게 사용한다면 문제가 발생할 수 있다.
헤드퍼스트 디자인 패턴에 나온 예시를 보면, Beverage라는 슈퍼클래스가 있고 이를 상속받는 구체적인 음료들이 서브클래스들로 선언되어있다.
추후에 여러가지 조합을 통해 나오는 새로운 음료들도 출시가 된다면 우리는 계속 상속받는 서브클래스들을 생산해내야할 것이다.
코드를 보며 데코레이터 패턴이 필요한 상황과 데코레이터 패턴을 사용하면 어떻게 코드가 변하는지 알아보자.
무분별한 상속 사용예시
// client와 실행코드 public class Client { private SpeakService speakService; public Client(SpeakService speakService) { this.speakSerivce = speakService; } private void speakVoice(String voice) { speakService.addVoice(voice); } public static void main(String[] args) { Client client = new client(new speakService()); client.speakVoice("디자인패턴"); client.speakVoice("오늘은 데코레이터 패턴에 대해 알아봅시다"); } } // SpeakService 코드 public class SpeakService { public void addVoice(String voice) { System.out.println(voice); } }
예시의 상황을 보면, 어떠한 음성을 입력받아 텍스트로 출력해주는 서비스를 만들어야한다고 가정해보자.
(실제 코드 상으로는 음성을 입력받는 코드 대신에 static하게 텍스트를 입력해주는 코드로 설계했다.)
처음에는 단순히 입력받은 음성을 그대로 출력만해주는 서비스를 생각하여 위와 같이 코드를 설계할 수 있다.
하지만, 문장이 '다','까','요' 등으로 마무리되지 않은 음성이 들어온다면 '좋아요'를 마지막에 붙여서 출력해주는 기능이 필요다고 생각해보자.
그러면 우리는 아래와 같이 클래스를 선언할 수 있다.
public class AddingLikeVoiceService extends SpeakService { @Override public void addVoice(String voice) { super.addVoice(addLike(voice)); } private String addLike(String voice) { if (canAddLike) { return voice + " 좋아요"; } return voice; } private boolean canAddLike(voice) { String lastChar = voice.charAt(voice.length() - 1); return lastChar == '다' || lastChar == '요' || lastChar == '까'; } } // client와 실행코드 public class Client { ... public static void main(String[] args) { Client client = new client(new AddingLikeVoiceService()); client.speakVoice("디자인패턴"); client.speakVoice("오늘은 데코레이터 패턴에 대해 알아봅시다"); } }
다음 기획으로 전화번호가 음성으로 들어온다면 "전화번호입니다."로 변환하여 출력할 수 있는 기능이 필요하다고 생각해보자.
public class ConvertingPhoneNumberService extends SpeakService { private static final String PATTERN = "^[0-9]*$"; @Override public void addVoice(String voice) { super.addVoice(convertPhoneNumber(voice)); } private String convertPhoneNumber(voice) { return voice.replaceAll(PATTERN); } } // client와 실행코드 public class Client { ... public static void main(String[] args) { Client client = new client(new ConvertingPhoneNumberService()); client.speakVoice("디자인패턴"); client.speakVoice("오늘은 데코레이터 패턴에 대해 알아봅시다"); } }
이렇게 기능이 추가될 때마다 supper class상속받는 sub class를 계속해서 생산해야하는 단점이 발생한다.
그리고 실행코드를 보면 static하게 serivce를 주입하는 방식으로 유연하지 못한 코드가 된다.
상속의 또 하나의 단점은 단일상속만 가능하다는 점으로 위에서 '좋아요'추가기능과 핸드폰번호 변환기능을 동시에 하는 클래스를 만든다고 생각하면 뭔가 기존에 존재하는 클래스를 다중상속하여 만들고 싶지만 현실적으로 불가능하다.
데코레이터 패턴 적용
위에서 살펴본 예시를 이제 데코레이터 패턴을 적용시켜 리팩토링해보자.
먼저, client가 사용할 Component interface를 선언해주자.
public interface SpeakService { void addVoice(String voice); }
그리고 위에서 기존에 SpeakService에서 하던 일을 DefaultSpeakService라는 클래스를 만들어주어 SpeakService를 implements해주도록 만들어보자.
맨 위에 도식도를 보면 DefaultSpeakService가 ConcreteComponent역할을 한다.
public class DefaultSpeakService implements SpeakService { @Override public void addVoice(String voice) { System.out.println(voice); } }
다음으로 필요한 것은 데코레이터이다.
데코레이터는 쉽게 말하면 Component를 감싸는 일종의 wrapper이고 감싸고 있는 Component를 그대로 호출해주면 된다.
public class SpeakDecorator implements SpeakService { private SpeakService speakService; public SpeakDecorator(SpeakService speakService) { this.speakService = speakService; } @Override public void addVoice(String voice) { speakService.addVoice(voice); } }
이제 우리가 필요한 기능을 하는 데코레이터를 만들어보자.
public class AddingLikeVoiceDecorator extends SpeakDecorator { public AddingLikeVoiceDecorator(SpeakService speakService) { super(speakService); } @Override public void addVoice(String voice) { super.addVoice(addLike(voice)); } private String addLike(String voice) { if (canAddLike) { return voice + " 좋아요"; } return voice; } private boolean canAddLike(voice) { String lastChar = voice.charAt(voice.length() - 1); return lastChar == '다' || lastChar == '요' || lastChar == '까'; } }
public class ConvertingPhoneNumberDecorator extends SpeakDecorator { private static final String PATTERN = "^[0-9]*$"; public onvertingPhoneNumberDecorator(SpeakService speakService) { super(speakService); } @Override public void addVoice(String voice) { super.addVoice(convertPhoneNumber(voice)); } private String convertPhoneNumber(voice) { return voice.replaceAll(PATTERN); } }
이렇게 데코레이터 패턴을 적용하여 리팩토링한 코드는 애플리케이션단에서의 사용 시에 어떻게 바뀔까?
아래의 코드와 같이 동적으로 필요한 기능을 추가하면서 우리가 원하는 일을 할 수 있게 해준다.
public class Application { private static boolean enabledAddingLike = true; private static boolean enabledConvertingPhoneNumber = true; public static void main(Strig[] args) { SpeakService speakService = new DefaultSpeakService(); if (enabledAddingLike) { speakService = new AddingLikeVoiceDecorator(speakSerivce); } if (enabledConvertingPhoneNumber) { speakService = new ConvertingPhoneNumberDecorator(speakService); } Client client = new Client(speakService); client.speakVoice("디자인패턴"); client.speakVoice("오늘은 데코레이터 패턴에 대해"); } }
상속을 사용했을 때의 큰 차이점은 우리가 2가지 기능을 모두 하는 클래스를 새로 선언하지 않아도 데코레이터를 통해 해결할 수 있다는 점이다.
데코레이터 패턴의 장점과 단점
- 장점
1. 새로운 클래스를 만들지 않고 기존 기능을 조합하여 원하는 기능을 수행시킬 수 있다.
2. 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
- 단점
1. 데코레이터를 조합하는 코드가 복잡할 수 있다.
'기타 > 북스터디' 카테고리의 다른 글
[디자인패턴] 커맨드패턴 (0) 2022.12.25 [이펙티브 자바] 아이템2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) 2022.12.12 [디자인패턴] 전략 패턴 (0) 2022.07.10 Test Driven Development By Example 2부 정리 (1) 2022.06.04 [객체지향의 사실과 오해] 2주차 정리 (3~4장) (0) 2022.04.01