개요
Visitor(방문자)패턴은 방문자와 방문 공간을 분리하여 방문 공간이 방문자를 맞이할 때, 이후에 대한 행동을 방문자에게 위임하는 패턴이다. 클래스 다이어그램은 아래 이미지와 같다.
출처: https://brownbears.tistory.com/575
예제
아래 예제는 문서 변환 시스템에 적용한 예제이다.
DocumentElement 인터페이스의 구체 클래스들은 실제 방문 공간이고 DocumentVisitor(MarkdownVisitor, HtmlExportVisitor, 방문자)에게 방문 공간을 맞이할때 수행할 책임들을 위임해주고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// 기본 문서 요소 인터페이스
public interface DocumentElement {
void accept(DocumentVisitor visitor);
}
// 구체적인 문서 요소들
@Getter
@AllArgsConstructor
public class TextElement implements DocumentElement {
private String content;
private String fontStyle;
private int fontSize;
@Override
public void accept(DocumentVisitor visitor) {
visitor.visitText(this);
}
}
@Getter
@AllArgsConstructor
public class ImageElement implements DocumentElement {
private String source;
private int width;
private int height;
private String altText;
@Override
public void accept(DocumentVisitor visitor) {
visitor.visitImage(this);
}
}
@Getter
@AllArgsConstructor
public class TableElement implements DocumentElement {
private List<List<String>> data;
private List<String> headers;
private String borderStyle;
@Override
public void accept(DocumentVisitor visitor) {
visitor.visitTable(this);
}
}
// 방문자 인터페이스
public interface DocumentVisitor {
void visitText(TextElement text);
void visitImage(ImageElement image);
void visitTable(TableElement table);
}
// HTML 변환 방문자
public class HtmlExportVisitor implements DocumentVisitor {
private StringBuilder html = new StringBuilder();
@Override
public void visitText(TextElement text) {
html.append(String.format(
"<p style='font-family: %s; font-size: %dpx;'>%s</p>",
text.getFontStyle(),
text.getFontSize(),
text.getContent()
));
}
@Override
public void visitImage(ImageElement image) {
html.append(String.format(
"<img src='%s' width='%d' height='%d' alt='%s' />",
image.getSource(),
image.getWidth(),
image.getHeight(),
image.getAltText()
));
}
@Override
public void visitTable(TableElement table) {
html.append("<table border='1'>\n");
// 헤더 추가
if (!table.getHeaders().isEmpty()) {
html.append(" <tr>\n");
for (String header : table.getHeaders()) {
html.append(" <th>").append(header).append("</th>\n");
}
html.append(" </tr>\n");
}
// 데이터 행 추가
for (List<String> row : table.getData()) {
html.append(" <tr>\n");
for (String cell : row) {
html.append(" <td>").append(cell).append("</td>\n");
}
html.append(" </tr>\n");
}
html.append("</table>");
}
public String getHtml() {
return html.toString();
}
}
// Markdown 변환 방문자
public class MarkdownExportVisitor implements DocumentVisitor {
private StringBuilder markdown = new StringBuilder();
@Override
public void visitText(TextElement text) {
markdown.append(text.getContent()).append("\n\n");
}
@Override
public void visitImage(ImageElement image) {
markdown.append(String.format(
"![%s](%s)\n",
image.getAltText(),
image.getSource()
));
}
@Override
public void visitTable(TableElement table) {
// 헤더 추가
if (!table.getHeaders().isEmpty()) {
markdown.append("|");
for (String header : table.getHeaders()) {
markdown.append(header).append("|");
}
markdown.append("\n|");
// 구분선 추가
for (int i = 0; i < table.getHeaders().size(); i++) {
markdown.append("---|");
}
markdown.append("\n");
}
// 데이터 행 추가
for (List<String> row : table.getData()) {
markdown.append("|");
for (String cell : row) {
markdown.append(cell).append("|");
}
markdown.append("\n");
}
markdown.append("\n");
}
public String getMarkdown() {
return markdown.toString();
}
}
public class DocumentConverter {
public static void main(String[] args) {
// 문서 요소 생성
List<DocumentElement> document = new ArrayList<>();
document.add(new TextElement(
"안녕하세요, 방문자 패턴 예제입니다.",
"Arial",
16
));
document.add(new ImageElement(
"/images/example.jpg",
800,
600,
"예제 이미지"
));
List<String> headers = Arrays.asList("이름", "나이", "직업");
List<List<String>> data = Arrays.asList(
Arrays.asList("홍길동", "30", "개발자"),
Arrays.asList("김철수", "25", "디자이너")
);
document.add(new TableElement(data, headers, "solid"));
// HTML 변환
HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
for (DocumentElement element : document) {
element.accept(htmlVisitor);
}
System.out.println("HTML 출력:");
System.out.println(htmlVisitor.getHtml());
// Markdown 변환
MarkdownExportVisitor markdownVisitor = new MarkdownExportVisitor();
for (DocumentElement element : document) {
element.accept(markdownVisitor);
}
System.out.println("\nMarkdown 출력:");
System.out.println(markdownVisitor.getMarkdown());
}
}
- Client 코드를 보면 방문자(HtmlExportVisitor, MarkdownExportVisitor)가 방문 공간(DocumentElement)에 순차적으로 들어가고(accept 함수)있고 들어갈때의 책임은 방문자(HtmlExportVisitor, MarkdownExportVisitor)에서 구현된걸 확인할 수 있다.
유용한 상황
방문자 패턴이 유용한 케이스는 아래와 같다.
1) 다양한 포맷 지원이 필요할 때
- 문서 변환 (HTML, PDF, Markdown 등)
- 데이터 직렬화 (JSON, XML, YAML 등)
2) 복잡한 객체 구조에 대한 다양한 처리가 필요할 때
- 파일 시스템 탐색
- AST(Abstract Syntax Tree) 처리
3) 처리 로직이 자주 변경되거나 확장되는 경우
- 새로운 보고서 형식 추가
- 새로운 계산 방식 도입
4) 객체의 데이터와 처리를 분리하고 싶을 때
- 데이터 구조는 안정적이지만 처리 방식이 다양한 경우
- 처리 로직을 모듈화하고 싶은 경우
단, 간단한 객체 구조에서는 오버엔지니어링이 될 수 있습니다.