데코레이터 패턴이란, 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있는 패턴을 말합니다.
용어가 살짝 어려운 것 같네요. 풀어보면
A라는 기능을 가진 프로그램에 A를 기반으로한 A-1 기능, A-2 기능, A-3기능, ... 등 "파생 기능을 만들고, 파생 기능 중에 원하는 기능들을 사용할 수 있게 하는 것입니다.
그리고 그 파생 기능들을 묶어주는 역할을 하는 것이 바로 "데코레이터"입니다.
여러분들이 애플의 개발자가 되어 '시리'를 개발한다고 가정해 봅시다.
시리를 한 번 호출했을 때, 여러 가지의 동작을 동시에 실행할 수 있게 만들고 싶습니다.
처음에는 먼저 시리를 만들고, 각종 명령을 받을 준비를 합시다.
interface Siri { // 시리가 받을 명령들(하위 클래스에서 구현) void command(); }
시리를 만들었으니 이제 시리 호출 트리거를 만들어야겠죠?
이 기능이 위에서 말한 기반이 되는 기능이 되겠습니다.
// 시리야~ class Siriya implements Siri { @Override public void command() { System.out.println("네, 부르셨나요."); } }
시리를 호출했으니 이제 여러 가지 동작을 만들어 보겠습니다.
하지만 그 전에, 동작들에 '시리야'를 상속시켜 구현한다면 객체 간 의존성이 높아지겠죠. 이를 묶어 줄 녀석이 필요합니다.
데코레이터입니다.
abstract class SiriDecorator implements Siri { // 명령을 받으려면 시리가 있어야겠죠. // 시리를 선언해 줍니다. private Siri siri; // 시리가 어떠한 명령을 받습니다. SiriDecorator(Siri siri) { this.siri = siri; } // 그 명령을 실행합니다. @Override public void command() { siri.command(); } }
구체적인 명령 클래스들입니다.
"음악 틀어줘"와 "오늘 날씨 어때"입니다.
// "음악 틀어줘" class PlayMusic extends SiriDecorator { //super는 부모 클래스를 뜻합니다. //super() 는 부모 클래스의 생성자를 호출합니다. PlayMusic(Siri siri) { super(siri); } @Override public void command() { super.command(); playMusic(); } private void playMusic() { System.out.println("네, 음악을 재생할게요."); } } // "오늘 날씨 어때" class TodayWeather extends SiriDecorator { TodayWeather(Siri siri) { super(siri); } @Override public void command() { super.command(); answerWeather(); } private void answerWeather() { System.out.println("오늘 날씨는 맑음입니다."); } }
메인 함수입니다.
public static void main(String[] args) { Siri siri = new Siriya(); siri.command(); // "네, 부르셨나요." siri = new PlayMusic(new Siriya()); siri.command(); // "네, 부르셨나요." // "네, 음악을 재생할게요." siri = new TodayWeather(new PlayMusic(new Siriya())); siri.command(); // "네, 부르셨나요." // "네, 음악을 재생할게요." // "오늘 날씨는 맑음입니다." }
생성자가 여러 개 있는 것이 보이실텐데, 각 내부의 인자가 super(...) 를 통해
내부 인자를 먼저 실행되게 함으로써, 결과적으로 괄호 가장 안쪽부터 실행됩니다.
마지막 명령으로 설명하면
TodayWeather의 super.siri = new PlayMusic(new Siriya())
PlayMusic의 super.siri = new Siriya()
↓
각 super.command()가 실행되면서
Siriya.command() - PlayMusic.playMusic() - TodayWeather.answerWeather() 순으로 실행됩니다.
결과적으로, "시리야"의 파생 기능들을 사용할 수도 있고, 사용하지 않을 수도 있고, 동시에 사용할 수도 있습니다.
이제 애플의 수석 개발자가 될 일만 남았네요.
사실 자바에 입문하면 데코레이터 패턴과 금방 마주하게 됩니다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedReader와 InputStreamReader는 Reader라는 클래스를 상속받고, super()를 통해 인자를 Reader의 생성자로 보내고, 이 Reader의 값을 받아 문자열을 얻어 버퍼에 저장합니다.
데코레이터를 통해 괄호 안 쪽부터 순차적으로 실행되는 것입니다.
자세한 내용은 링크를 첨부합니다.
BufferedReader에 관해 설명하면서 Scanner가 왜 느린지도 설명하고 있으니 읽어보시면 좋겠습니다.
https://st-lab.tistory.com/41
- 기존 코드를 수정하지 않고도 데코레이터 패턴을 통해 행동을 확장시킬 수 있습니다.
- 구성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있습니다.
(구성 : 새 클래스에 기존 클래스의 객체를 생성하는 것, 형식이나 개념이 아닌 코드의 기능을 재사용하는 것임.)
(위임 : 상속과 구성의 중간 개념. 어떤 행위를 위임 관계에 있는 객체에게 넘겨서 처리하는 것을 의미.)
- 의미없는 객체들이 너무 많이 추가될 수 있습니다.
- 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해질 수 있습니다.
- 상속 : 클래스와 인터페이스에서 사용되며 is-a 관계를 나타냅니다.
(is-a 관계 : 태생부터 무엇인가 타고난 기질 → I am human / class I extends Human)
- 자바에서 클래스 다중 상속은 불가능합니다.
(class I extends Human, Animal) (X)
(class Run implements Exercise, Move) (O)
- 클래스 내부의 코드를 공유합니다. (멤버 변수, 메소드)
- 구현 : 인터페이스에서 사용되며 can-do 관계를 나타냅니다.
(can-do 관계 : 배워서 할 수 있는 행동 → I can swim / class I implements Swimming)
- 인터페이스는 다중 상속, 구현이 가능합니다.
(interface Run extends Exercise, Move) (O)
(interface Run implements Exercise, Move) (O)
- 인터페이스 내부의 멤버 변수는 공유하지만 메소드는 직접 구현해야 합니다.
https://gmlwjd9405.github.io/2018/07/09/decorator-pattern.html