Skip to main content

Decorator Pattern

Overview​

πŸ“ λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ€ λ™μ μœΌλ‘œ λŸ°νƒ€μž„μ— λΆ€κ°€κΈ°λŠ₯을 μΆ”κ°€ν•  수 μžˆκ²Œν•˜λŠ” λ””μžμΈ νŒ¨ν„΄μ΄λ‹€.

When to use​

νŠΉμ • μ‚¬μš©μž (Client) κ°€ λŒ“κΈ€μ„ λ‹€λŠ” μ„œλΉ„μŠ€κ°€ μžˆλ‹€κ³  ν•˜μž.
이 λ•Œ, λŒ“κΈ€ 끝에 뢙은 ".." 같은 λ¬Έμžμ—΄μ€ μžλ™μœΌλ‘œ trim μ²˜λ¦¬κ°€ ν•„μš”ν•˜λ‹€λŠ” μš”κ΅¬μ‚¬ν•­μ΄ μžˆμ–΄ TrimmingCommentService 클래슀λ₯Ό 상속받아 κ΅¬μ„±ν–ˆλ‹€.

Client.java​

@Component
@Getter
public class Client {
private CommentService commentService;

@Autowired
public Client(CommentService commentService) {
this.commentService = commentService;
}

public void writeComment(String comment) {
commentService.addComment(comment);
}
}

CommentService.java​

@Component
public class CommentService {
private List<String> comments = new ArrayList();

public void addComment(String comment) {
this.comments.add(comment);
}
}

TrimmingCommentService.java​

public class TrimmingCommentService extends CommentService {

@Override
public void addComment(String comment) {
super.addComment(getTrimmedComment(comment));
}

// λΆ„λͺ… ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œ λ³€κ²½ 없이 μƒˆλ‘œμš΄ κΈ°λŠ₯ ν™•μž₯을 도λͺ¨ν–ˆμ§€λ§Œ
// Compile time 에 이미 이 κΈ°λŠ₯을 μ™„λ²½νžˆ fix ν•΄μ•Όλ§Œ ν•œλ‹€λŠ”κ²Œ 단점이닀.
// μœ μ—°ν•˜μ§€ μ•Šλ‹€. Runtime μ‹œμ— λ°”κΏ”μ•Όν•œλ‹€λ©΄?
private String getTrimmedComment(String comment) {
return comment.replace("..","");
}
}

ν˜„μž¬κΉŒμ§€μ˜ κ΅¬ν˜„ 내역을 클래슀 λ‹€μ΄μ–΄κ·Έλž¨μœΌλ‘œ ν‘œν˜„ν•˜λ©΄ λ‹€μŒκ³Ό κ°™λ‹€. Comment Service Diagram

ν΄λΌμ΄μ–ΈνŠΈμ—μ„  λ‹€μŒκ³Ό 같이 Trimming 된 λŒ“κΈ€μ„ μž‘μ„±ν•˜λŠ” μ„œλΉ„μŠ€λ₯Ό μ‚¬μš©ν•  수 μžˆμ„ 것이닀.

ClientCommentTest.java​

import m.falcon.designpattern.domain.comment.service.TrimmingCommentService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

class ClientCommentTest {

@DisplayName("λŒ“κΈ€ 달기 .. 제거 ν…ŒμŠ€νŠΈ")
@Test
void writeTrimCommentTest() {
// μƒμ„±μž DI
// λ§Œμ•½, spam filtering 이 ν•„μš”ν•˜λ‹€λ©΄?
Client client = new Client(new TrimmingCommentService());
client.writeComment("..μΉ΄λ₯΄λ§ˆ..");

String trimmedComment = "μΉ΄λ₯΄λ§ˆ";
assertThat(client.getCommentService().getComments().get(0)).isEqualTo(trimmedComment);
}
}

λ¬Έμ œμ β€‹

λ§Œμ•½ "Http" κ°€ ν¬ν•¨λœ λŒ“κΈ€ μž‘μ„±μ„ λ°©μ§€ν•˜λŠ” 필터링이 ν•„μš”ν•˜λ‹€λ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ?
상속을 톡해 SpamFilteringCommentService λ₯Ό κ΅¬ν˜„ν•˜λ©΄ 될 것이닀.

HttpFilterCommentService.java​

public class HttpFilterCommentService extends CommentService {
@Override
public void addComment(String comment) {
if (isSpamComment(comment)) {
return;
}
super.addComment(comment);
}

private Boolean isSpamComment(String comment) {
return comment.contains("http://");
}
}

κ·Έλ ‡λ‹€λ©΄ ν΄λΌμ΄μ–ΈνŠΈλŠ” μƒμ„±μž μ£Όμž…μ„ λ‹€μŒκ³Ό 같이 λ³€κ²½ν•˜μ—¬ μ‚¬μš©ν•΄μ•Όν•œλ‹€.

Client client = new Client(new HttpFilterCommentService());
client.writeComment("http://karma.com");

더 λ‚˜μ•„κ°€μ„œ, Http 필터링과 Trimming 정책을 λ™μ‹œμ— μ μš©ν•˜κ³ μ‹Άλ‹€.
κ²½μš°μ— λ”°λΌμ„œλŠ” λ‘˜μ€‘ ν•œ μ •μ±…λ§Œ μ μš©ν•˜κ³  μ‹Άλ‹€. μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ?

1. μžμ‹ 클래슀 좔가​

상속과 μƒμ„±μž μ£Όμž…μ„ 톡해 문제λ₯Ό ν•΄κ²°ν•˜λŠ” 방법이닀.
클래슀 λ‹€μ΄μ–΄κ·Έλž¨μ„ ν‘œν˜„ν•΄λ³΄λ©΄ λ‹€μŒκ³Ό 같이 ν•„μš”ν•œ 정책이 λŠ˜μ–΄λ‚  λ•Œλ§ˆλ‹€ 클래슀 μΆ”κ°€κ°€ λΆˆκ°€ν”Όν•˜λ‹€. Comments Service Diagrams

μžλ°”μ—μ„œ 상속은 단일 μƒμ†λ§Œ κ°€λŠ₯ν•˜κ³ , 컴파일 νƒ€μž„μ— μ •μ±… 섀정이 μ™„λ£Œλ˜μ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— μœ μ—°μ„±μ΄ 떨어진닀.
πŸ’‘ Decorator νŒ¨ν„΄μ€ μ΄λŸ¬ν•œ μƒν™©μ˜ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μ‘΄μž¬ν•œλ‹€.

πŸ“ λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ€ λŸ°νƒ€μž„μ— 정책을 λ³€κ²½ν•˜μ—¬ λ‹€λ₯΄κ²Œ λ™μž‘ν•˜λŠ” μœ μ—°ν•œ μ½”λ“œ μž‘μ„±μ„ λ•λŠ” νŒ¨ν„΄μ΄λ‹€.

Why to use​

λŸ°νƒ€μž„μ— λ™μ μœΌλ‘œ λ‹€λ₯Έ μ„œλΉ„μŠ€ (κΈ°λŠ₯)을 μ‚¬μš©ν•  수 μžˆλ„λ‘ ν•˜κ²Œ ν•˜κΈ°μœ„ν•΄.

How to use​

νŠΉμ • Component λ₯Ό ν’ˆλŠ” Decorator λ₯Ό Interface λ˜λŠ” abstract 클래슀둜 λ§Œλ“€κ³ 
ꡬ체 Decorator 클래슀λ₯Ό ν•˜λ‚˜μ”© μΆ”κ°€ν•œλ‹€.

이 μ˜ˆμ œμ—μ„œλŠ” CommentService κ°€ Component λ‹€.

Decorator Class diagram

CommentService.java​

public interface CommentService {
void addComment(String comment);
void printAllComments();
}

DefaultCommentService.java​

@Component
@Getter
public class DefaultCommentService implements CommentService {
private List<String> comments = new ArrayList();

@Override
public void addComment(String comment) {
this.comments.add(comment);
}

@Override
public void printAllComments() {
for (var comment : comments) {
System.out.println(comment);
}
}
}

CommentDecorator.java​

@Component
@RequiredArgsConstructor
public abstract class CommentDecorator implements CommentService {
// κ΅¬ν˜„μ²΄κ°€ μ•„λ‹Œ Interface(μ—­ν• ) μ—λ§Œ 의쑴 => DIP 원칙 μ€€μˆ˜
private final CommentService commentService;

@Override
public void addComment(String comment) {
this.commentService.addComment(comment);
}

@Override
public void printAllComments() {
this.commentService.printAllComments();
}
}

HttpFilteringCommentDecorator.java​

public class HttpFilteringCommentDecorator extends CommentDecorator {
public HttpFilteringCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
if (isSpamComment(comment)) {
return;
}
super.addComment(comment);
}

private Boolean isSpamComment(String comment) {
return comment.contains("http://");
}
}

TrimmingCommentDecorator.java​

public class TrimmingCommentDecorator extends CommentDecorator {
public TrimmingCommentDecorator(CommentService commentService) {
super(commentService);
}

@Override
public void addComment(String comment) {
super.addComment(trimComment(comment));
}

private String trimComment(String comment) {
return comment.replace("..", "");
}
}

ClientTest.java​

class ClientCommentTest {
private CommentService commentService = new DefaultCommentService();
private static boolean enabledHttpFilter = true;
private static boolean enabledTrimFilter = true;

@DisplayName("Http 및 Trim ν•„ν„° λ™μ‹œ 적용")
@Test
void dynamicCommentPolicyApplyTest() {
// πŸ’‘Decorator λ₯Ό ν†΅ν•œ μƒμ„±μž μ£Όμž…μœΌλ‘œ Http, Trimming λ™μ‹œ ν•„ν„° 적용
if (enabledHttpFilter) {
commentService = new HttpFilteringCommentDecorator(commentService);
}
if (enabledTrimFilter) {
commentService = new TrimmingCommentDecorator(commentService);
}

Client client = new Client(commentService);
client.writeComment("μΉ΄λ₯΄λ§ˆ..");
client.writeComment("http://karma.com");
client.writeComment("https://karma.com");
client.getCommentService().printAllComments();
}
}

좜λ ₯ 결과​

Http, Trimming ν•„ν„° λͺ¨λ‘ 적용된 것을 확인할 수 μžˆλ‹€.

μΉ΄λ₯΄λ§ˆ
https://karma.com

이처럼 λŸ°νƒ€μž„ 내에 λ™μ μœΌλ‘œ 필터링 정책을 μ μš©ν• μˆ˜λ„, μ μš©ν•˜μ§€ μ•Šμ„ μˆ˜λ„ μžˆλ‹€.

λ‚΄λΆ€ λ™μž‘ μ„€λͺ… μ˜μƒβ€‹

μ–΄λ–»κ²Œ ν•˜λ‚˜μ˜ κ°μ²΄λ‘œλΆ€ν„° ν•œ 번의 addComment λ©”μ†Œλ“œ 호좜둜 μ—¬λŸ¬ 정책을 λͺ¨λ‘ μ μš©ν•  수 μžˆλŠ” κ²ƒμΌκΉŒ?
μ˜μƒκ³Ό ν•¨κ»˜ νŒŒν—€μ³λ³΄μž

Pros and Cons​

μž₯점​

  • μƒˆλ‘œμš΄ 클래슀 생성 없이 κΈ°μ‘΄ κΈ°λŠ₯ μ‘°ν•©
    ex) HttpFilterDecorator + TrimmingFilterDecorator μ‘°ν•©
    쑰합이 λΆˆκ°€λŠ₯ν•˜λ‹€λ©΄ ν•œ 클래슀 내에 2가지 μ΄μƒμ˜ ν•„ν„°λ₯Ό 같이 걸어야함.
  • λŸ°νƒ€μž„μ— λ™μ μœΌλ‘œ κΈ°λŠ₯ ꡐ체

단점​

  • λ°μ½”λ ˆμ΄ν„° μ‘°ν•© μ½”λ“œ λ³΅μž‘μ„± 증가

    λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ„ μ‚¬μš©ν•˜μ§€ μ•Šμ„ λ•Œ μ„œλΈŒ 클래슀 μˆ˜κ°€ O(2N)O(2^N) 으둜 λŠ˜μ–΄λ‚  수 있기 λ•Œλ¬Έμ— 단점이라 보기에 민망함.

SRP μœ„λ°˜ μ½”λ“œβ€‹

if (enabledHttpFilter && enabledTrimFilter) {
// μ •μ μœΌλ‘œ λͺ¨λ“  쑰합에 λŒ€ν•œ 상속 ν΄λž˜μŠ€κ°€ μ‘΄μž¬ν•΄μ•Όν•¨ => μ„œλΈŒ 클래슀 수 κΈ‰κ²©ν•˜κ²Œ 증가 μœ„ν—˜
// μ—¬λŸ¬ μ±…μž„μ„ κ°–λŠ” μ„œλΈŒ 클래슀 μƒμ„±μ‹œ Single Responsibility Principal (SRP) μœ„λ°°
commentService = new HttpFilterAndTrimmingComment(); // 뢄리될 수 μžˆλŠ” Filter 둜직 2개λ₯Ό ν•˜λ‚˜μ˜ ν΄λž˜μŠ€κ°€ 묢음 ν˜•νƒœλ‘œ κ°€μ§€κ³ μžˆλ‹€.
}

πŸ”— Reference​

GoF λ””μžμΈ νŒ¨ν„΄ - λ°±κΈ°μ„