Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

데코레이터 패턴 정리 #102

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions 구조/5주차-데코레이터/summary/example-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 데코레이터 패턴 적용 예제

데코레이터 패턴의 개념은 이전 글에서 다루었기 때문에 생략했다.

- 이전 글 : [https://dev-youngjun.tistory.com/213](https://dev-youngjun.tistory.com/213)

이번 글은 실무에서는 어떤식으로 데코레이터 패턴이 적용되는지 작성했다.

## 1. 실무 요구사항

채팅 서비스를 도입하는데, 광고 메시지는 보내지 않았으면 좋겠고 욕설은 필터링이 되었으면 좋겠다.

이를 데코레이터 패턴으로 구현해보자.

## 2. Decorator 패턴 적용하기


![decorator.png](https://user-images.githubusercontent.com/42997924/159971013-8f0009d2-b9f5-46e6-b97f-158e16e0b3a5.png)

클라이언트(채팅 입력 주체)는 sendMessage를 하는 목적이 중요하고, 코드는 변경되지 않아야 한다.

```java
// 채팅 입력 클라이언트(변경x)
public class Client {
private final ChatService chatService;
public Client(ChatService chatService) {
this.chatService = chatService;
}
// 채팅 메시지를 발송
public void sendMessage(String message) {
chatService.sendMessage(message);
}
}
```

기본 컴포넌트(ChatService)와 기본 컴포넌트 구현체(DefaultChatService)는 다음과 같다.

```java
// Component 인터페이스
public interface ChatService {
void sendMessage(String message);
}

// ConcreteComponent : 기본 메시지 발송
public class DefaultChatService implements ChatService {
// 단 하나의 컴포넌트(ChatService)를 포함
@Override
public void sendMessage(String message) {
System.out.println("send message : " + message);
}
}
```

여기에 자주 추가/삭제될 수 있는 필터링 기능을 Decorator로 구현하면 된다.

```java
// Decorator : 기능 추가 후 Component 호출
public class ChatDecorator implements ChatService {
private final ChatService chatService;
public ChatDecorator(ChatService chatService) {
this.chatService = chatService;
}
@Override
public void sendMessage(String message) {
chatService.sendMessage(message);
}
}

// 광고 필터링 Decorator
public class AdFilterChatDecorator extends ChatDecorator {
public AdFilterChatDecorator(ChatService chatService) {
super(chatService);
}

@Override
public void sendMessage(String message) {
// 광고 필터링 기능 추가
if (isNotAd(message) ) {
super.sendMessage(message);
}
}

private boolean isNotAd(String message) {
return !message.contains("광고");
}
}

// 욕설 필터링 Decorator
public class AbuseFilterChatDecorator extends ChatDecorator {
public AbuseFilterChatDecorator(ChatService chatService) {
super(chatService);
}

@Override
public void sendMessage(String message) {
// 욕설 필터링 기능 추가
super.sendMessage(abuseFilter(message));
}

private String abuseFilter(String message) {
return message.replace("똥개야","이쁜진돗개순종아" );
}
}
```

이제 채팅App에서는 런타임 시 동적으로 플래그 값에 따라 필터를 추가할 수 있다!

```java
// 채팅 App
public class ChatApp {
private static final boolean enabledAdFilter = true;
private static final boolean enabledAbuseFilter = true;

public static void main(String[] args) {
// 기본 채팅 서비스 생성
ChatService chatService = new DefaultChatService();
// 런타임 시 플래그 값에 따라 필터를 추가
if (enabledAdFilter) { // 광고 제거 필터
chatService = new AdFilterChatDecorator(chatService);
}
if (enabledAbuseFilter) { // 욕설 치환 필터
chatService = new AbuseFilterChatDecorator(chatService);
}

Client client = new Client(chatService);
client.sendMessage("안녕하세요!");
client.sendMessage("(광고) 돈이 복사가 된다고?");
client.sendMessage("똥개야 답장해줘");
}
}

/* 실행 결과
send message = 안녕하세요!
send message = 이쁜진돗개순종아 답장해줘
*/
```
190 changes: 190 additions & 0 deletions 구조/5주차-데코레이터/summary/데코레이터 패턴.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# 데코레이터 패턴

> 기존 코드를 변경하지 않고 부가 기능을 동적으로(유연하게) 추가하는 패턴

![](https://refactoring.guru/images/patterns/diagrams/decorator/structure.png)

**Component**: wrapper들과 wrap된 객체들을 위한 공통 인터페이스

**Base Decorator**: Wrapped객체를 참조를 위해서 필드를 가지고 있는 클래스. 베이스 데코레이터는 모든 작업을 Wrapped된 객체에 위임해주는 역할을 한다.

**Concrete Decorators**: 컴포넌트에 동적으로 추가할 수 있는 추가적인 동작을 정의해두면 .. 이 데코레이터들은 기본 데코레이터의 메서드를 제정의하고 상위 메소드를 부르거나 부르기 전에 이 동작을 실행 시킬 수 있다.

상속이 아닌 위임을 사용해서 보다 유연하게(**런타임에**) 부가 기능을 추가하는 것도 가능하다. 기능 확장이 필요할 때 상속대신 사용할 수 있는 패턴.

## 데코레이터 언제 사용되는가?

- 클래스의 요소들을 계속 수정해서 사용하는 구조

- 상황에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우

- 객체의 결합을 통해 기능이 생성될 수 있는 경우

## 데코레이터 패턴 예제 코드

## 1. 실무 요구사항

채팅 서비스를 도입하는데, 광고 메시지는 보내지 않았으면 좋겠고 욕설은 필터링이 되었으면 좋겠다.

이를 데코레이터 패턴으로 구현해보자.

## 2. Decorator 패턴 적용하기

![decoratorpng](https://user-images.githubusercontent.com/42997924/159971013-8f0009d2-b9f5-46e6-b97f-158e16e0b3a5.png)

클라이언트(채팅 입력 주체)는 sendMessage를 하는 목적이 중요하고, 코드는 변경되지 않아야 한다.

```java
// 채팅 입력 클라이언트(변경x)
public class Client {
private final ChatService chatService;
public Client(ChatService chatService) {
this.chatService = chatService;
}
// 채팅 메시지를 발송
public void sendMessage(String message) {
chatService.sendMessage(message);
}
}
```

기본 컴포넌트(ChatService)와 기본 컴포넌트 구현체(DefaultChatService)는 다음과 같다.

```java
// Component 인터페이스
public interface ChatService {
void sendMessage(String message);
}

// ConcreteComponent : 기본 메시지 발송
public class DefaultChatService implements ChatService {
// 단 하나의 컴포넌트(ChatService)를 포함
@Override
public void sendMessage(String message) {
System.out.println("send message : " + message);
}
}
```

여기에 자주 추가/삭제될 수 있는 필터링 기능을 Decorator로 구현하면 된다.

```java
// Decorator : 기능 추가 후 Component 호출
public class ChatDecorator implements ChatService {
private final ChatService chatService;
public ChatDecorator(ChatService chatService) {
this.chatService = chatService;
}
@Override
public void sendMessage(String message) {
chatService.sendMessage(message);
}
}

// 광고 필터링 Decorator
public class AdFilterChatDecorator extends ChatDecorator {
public AdFilterChatDecorator(ChatService chatService) {
super(chatService);
}

@Override
public void sendMessage(String message) {
// 광고 필터링 기능 추가
if (isNotAd(message) ) {
super.sendMessage(message);
}
}

private boolean isNotAd(String message) {
return !message.contains("광고");
}
}

// 욕설 필터링 Decorator
public class AbuseFilterChatDecorator extends ChatDecorator {
public AbuseFilterChatDecorator(ChatService chatService) {
super(chatService);
}

@Override
public void sendMessage(String message) {
// 욕설 필터링 기능 추가
super.sendMessage(abuseFilter(message));
}

private String abuseFilter(String message) {
return message.replace("똥개야","이쁜진돗개순종아" );
}
}
```

이제 채팅App에서는 런타임 시 동적으로 플래그 값에 따라 필터를 추가할 수 있다!

```java
// 채팅 App
public class ChatApp {
private static final boolean enabledAdFilter = true;
private static final boolean enabledAbuseFilter = true;

public static void main(String[] args) {
// 기본 채팅 서비스 생성
ChatService chatService = new DefaultChatService();
// 런타임 시 플래그 값에 따라 필터를 추가
if (enabledAdFilter) { // 광고 제거 필터
chatService = new AdFilterChatDecorator(chatService);
}
if (enabledAbuseFilter) { // 욕설 치환 필터
chatService = new AbuseFilterChatDecorator(chatService);
}

Client client = new Client(chatService);
client.sendMessage("안녕하세요!");
client.sendMessage("(광고) 돈이 복사가 된다고?");
client.sendMessage("똥개야 답장해줘");
}
}

/* 실행 결과
send message = 안녕하세요!
send message = 이쁜진돗개순종아 답장해줘
*/
```

## 패턴의 장/단점

✅ 장점:

- OCP(개방/폐쇄원칙) 새로운 구독자 클래스를 발행자의 코드 변경 없이 추가 가능. 그리고 그 반대로 발행자 인터페이스가 있다는 가정하에 역도 성립한다.
- 새로운 서브클래스 생성없이, 객체의 행동을 확장 할 수 있다.
- 런타임 도중에 객체로부터 책임을 추가하거나, 제거할 수 있다.
- 여러개의 데코레이터로 갑싸진 객체에 대한 여러개의 동작을 합칠 수 있다.
- SRP(단일 책임 원칙) 여러가지 가능한 변형 동작을 구현한 모놀로식 클래스에 대해서 여러개의 작은 클래스로 나눌 수 있다.

🚨 단점:

- Wrapper들의 모음에서 특정 wrapper만 제거 하는건 어렵다.
- 데코레이터들의 모음에서 순서대로 동작하게 구현하는 건 데코레이터로 구현하기 어렵다.
- 이러한 계층을 만들기 위한 초기 환경 코드들이 보기에 난잡해보일 수 있다.

## 비슷한 패턴

- **어뎁터**패턴은 존재하는 객체에 대해서 인터페이스가 변경되지만, **데코레이터**는 인터페이스의 변경없이 객체를 확장시킨다.
추가적으로 데코레이터는 재귀적 구성을 제공하고, 어뎁터는 그렇게 구성은 불가능하다.

- **어뎁터**는 Wrapped 객체에 대해 다른 인터페이스를 제공하지만, **프록시**는 그것과 동일한 인터페이스를, **데코레이터**는 확장된 인터페이스를 제공한다.

- **책임 연쇄 패턴**과 **데코레이터**는 클래스 구조상 비슷한 형식을 가지고 있다 . 두 패턴은 모두 한개의 객체로 실행시키기 위해 재귀적 합성방식을 이용한다. 그러나 몇몇 부분은 확연하게 차이점이 있다.

- **책임 연쇄 핸들러**는 각각 서로가 독립적으로 임의의 조작을 실행시킬 수 있다. 또한 언제든지 이런 요청들을 멈출 수도 있다.

- **데코레이터**들은 기본 인터페이스을 기반으로 일관성을 유지하면서 개체의 동작을 확장할 수 있다. 또한 데코레이터는 요청의 흐름을 끊을 수 없다.

- 컴포짓과 데코레이터는 비슷한 구조 다이어 그램을 가지고 있다. 둘다 재귀적인 구성을 의존해서 무한정 객체를 구성할 수 있다.

- 데코레이터는 컴포짓과 닮아있지만, 하나의 자식 컴포넌트만 가질 수 있다.

- 컴포짓 그리고 데코레이터를 많이 사용하는 상황에서는 프로토 타입을 사용하는게 더 이점이 있다. 이런 패턴을 적용하게 되면, 복잡한 구조자체를 그냥 한번에 복제해서 구성하면 되기때문에 편리하다.

- **데코레이터**는 객체에 스킨을 변경하는 것과 같고, **전략 패턴**은 내장을 바꾸는 역할을 한다.

- **데코레이터**와 **프록시** 역시 비슷한 구조를 가지고 있지만, 매우 다른 의도를 가지고 있다. 두 패턴들은 모두 한 객체가 다른 객체에 작업의 일부를 위임하도록 되어 있는 구성으로 기반으로 하고 있다. 프록시는 서비스 객체의 수명 주기를 스스로 관리하지만, 데코레이터는 이런 생명주기를 client에 의해 제어한다.