"객체 구조와 알고리즘을 분리하자"
// 문제 1: 새 기능 추가 시 모든 클래스 수정
public interface Shape {
void draw();
void calculateArea();
// 새 기능 추가? 모든 구현체 수정!
}
public class Circle implements Shape {
public void draw() { }
public void calculateArea() { }
// 새 메서드 추가 시 여기도 수정
}
public class Rectangle implements Shape {
public void draw() { }
public void calculateArea() { }
// 여기도 수정
}
// export() 기능 추가하려면?
// → 모든 Shape 구현체 수정!
// 문제 2: 타입별 다른 처리
public class DocumentProcessor {
public void process(List<Document> docs) {
for (Document doc : docs) {
if (doc instanceof PDFDocument) {
processPDF((PDFDocument) doc);
} else if (doc instanceof WordDocument) {
processWord((WordDocument) doc);
} else if (doc instanceof ExcelDocument) {
processExcel((ExcelDocument) doc);
}
// instanceof 지옥!
}
}
}
// 문제 3: 여러 연산이 흩어짐
public class Employee {
public void calculateSalary() { }
public void generateReport() { }
public void exportToXML() { }
public void sendEmail() { }
// 직원 클래스가 너무 많은 책임!
// SRP 위반!
}
// 문제 4: 객체 구조 순회
public class FileSystem {
public void calculateSize(FileSystemNode node) {
if (node instanceof File) {
return ((File) node).getSize();
} else if (node instanceof Directory) {
Directory dir = (Directory) node;
int total = 0;
for (FileSystemNode child : dir.getChildren()) {
total += calculateSize(child); // 재귀
}
return total;
}
}
// 새 연산 추가마다 비슷한 코드 반복!
}- OCP 위반: 새 연산 추가 시 모든 클래스 수정
- SRP 위반: 클래스가 너무 많은 책임
- 타입 체크: instanceof 남발
- 연산 분산: 관련 연산이 여러 클래스에 흩어짐
객체 구조를 변경하지 않고 새로운 연산을 추가할 수 있게 하는 패턴. 연산을 수행할 객체(Visitor)를 별도로 만들어 객체 구조의 각 요소를 방문하며 연산을 수행한다.
- 연산 분리: 알고리즘을 객체 구조에서 분리
- OCP 준수: 새 연산 추가 시 기존 코드 불변
- SRP 준수: 각 연산을 별도 클래스로
- 타입 안전: instanceof 대신 메서드 오버로딩
// Before: 객체에 연산 추가
public class Shape {
void draw() { }
void export() { } // 새 연산 추가!
}
// After: Visitor로 연산 분리
public interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
public class ExportVisitor implements ShapeVisitor {
void visit(Circle c) { /* Circle 내보내기 */ }
void visit(Rectangle r) { /* Rectangle 내보내기 */ }
}┌──────────────┐
│ Visitor │ ← 방문자 인터페이스
├──────────────┤
│ + visit(A) │
│ + visit(B) │
└──────────────┘
△
│ implements
┌───────────────┐
│ConcreteVisitor│
├───────────────┤
│ + visit(A) │
│ + visit(B) │
└───────────────┘
┌──────────────┐
│ Element │ ← 요소 인터페이스
├──────────────┤
│ + accept(v) │
└──────────────┘
△
│ implements
│
┌──────────────┐
│ElementA │
├──────────────┤
│ + accept(v) │────┐
└──────────────┘ │
│ visitor.visit(this)
┌──────────────┐ │
│ElementB │ │
├──────────────┤ │
│ + accept(v) │────┘
└──────────────┘
| 요소 | 역할 | 예시 |
|---|---|---|
| Visitor | 방문자 인터페이스 | ShapeVisitor |
| ConcreteVisitor | 구체적 방문자 | ExportVisitor |
| Element | 요소 인터페이스 | Shape |
| ConcreteElement | 구체적 요소 | Circle |
/**
* Visitor: 도형 방문자
*/
public interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
void visit(Triangle triangle);
}
/**
* Element: 도형
*/
public interface Shape {
void accept(ShapeVisitor visitor);
}
/**
* ConcreteElement 1: 원
*/
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this); // Double Dispatch!
}
}
/**
* ConcreteElement 2: 사각형
*/
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
/**
* ConcreteElement 3: 삼각형
*/
public class Triangle implements Shape {
private double base;
private double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
public double getBase() {
return base;
}
public double getHeight() {
return height;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
/**
* ConcreteVisitor 1: 넓이 계산
*/
public class AreaCalculator implements ShapeVisitor {
private double totalArea = 0;
@Override
public void visit(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
System.out.println("⭕ 원 넓이: " + String.format("%.2f", area));
totalArea += area;
}
@Override
public void visit(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
System.out.println("⬜ 사각형 넓이: " + String.format("%.2f", area));
totalArea += area;
}
@Override
public void visit(Triangle triangle) {
double area = 0.5 * triangle.getBase() * triangle.getHeight();
System.out.println("🔺 삼각형 넓이: " + String.format("%.2f", area));
totalArea += area;
}
public double getTotalArea() {
return totalArea;
}
}
/**
* ConcreteVisitor 2: XML 내보내기
*/
public class XMLExporter implements ShapeVisitor {
private StringBuilder xml;
public XMLExporter() {
this.xml = new StringBuilder();
xml.append("<shapes>\n");
}
@Override
public void visit(Circle circle) {
xml.append(" <circle radius=\"").append(circle.getRadius()).append("\"/>\n");
System.out.println("📤 원 내보내기");
}
@Override
public void visit(Rectangle rectangle) {
xml.append(" <rectangle width=\"").append(rectangle.getWidth())
.append("\" height=\"").append(rectangle.getHeight()).append("\"/>\n");
System.out.println("📤 사각형 내보내기");
}
@Override
public void visit(Triangle triangle) {
xml.append(" <triangle base=\"").append(triangle.getBase())
.append("\" height=\"").append(triangle.getHeight()).append("\"/>\n");
System.out.println("📤 삼각형 내보내기");
}
public String getXML() {
return xml.toString() + "</shapes>";
}
}
/**
* ConcreteVisitor 3: 그리기
*/
public class DrawVisitor implements ShapeVisitor {
@Override
public void visit(Circle circle) {
System.out.println("🎨 원 그리기 (반지름: " + circle.getRadius() + ")");
}
@Override
public void visit(Rectangle rectangle) {
System.out.println("🎨 사각형 그리기 (" + rectangle.getWidth() +
"x" + rectangle.getHeight() + ")");
}
@Override
public void visit(Triangle triangle) {
System.out.println("🎨 삼각형 그리기 (밑변: " + triangle.getBase() +
", 높이: " + triangle.getHeight() + ")");
}
}
/**
* 사용 예제
*/
public class VisitorExample {
public static void main(String[] args) {
// 도형 생성
List<Shape> shapes = Arrays.asList(
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8),
new Circle(3)
);
// 1. 넓이 계산
System.out.println("=== 넓이 계산 ===");
AreaCalculator areaCalc = new AreaCalculator();
for (Shape shape : shapes) {
shape.accept(areaCalc);
}
System.out.println("총 넓이: " + String.format("%.2f", areaCalc.getTotalArea()));
// 2. XML 내보내기
System.out.println("\n=== XML 내보내기 ===");
XMLExporter xmlExporter = new XMLExporter();
for (Shape shape : shapes) {
shape.accept(xmlExporter);
}
System.out.println("\n" + xmlExporter.getXML());
// 3. 그리기
System.out.println("\n=== 그리기 ===");
DrawVisitor drawer = new DrawVisitor();
for (Shape shape : shapes) {
shape.accept(drawer);
}
}
}실행 결과:
=== 넓이 계산 ===
⭕ 원 넓이: 78.54
⬜ 사각형 넓이: 24.00
🔺 삼각형 넓이: 12.00
⭕ 원 넓이: 28.27
총 넓이: 142.81
=== XML 내보내기 ===
📤 원 내보내기
📤 사각형 내보내기
📤 삼각형 내보내기
📤 원 내보내기
<shapes>
<circle radius="5.0"/>
<rectangle width="4.0" height="6.0"/>
<triangle base="3.0" height="8.0"/>
<circle radius="3.0"/>
</shapes>
=== 그리기 ===
🎨 원 그리기 (반지름: 5.0)
🎨 사각형 그리기 (4.0x6.0)
🎨 삼각형 그리기 (밑변: 3.0, 높이: 8.0)
🎨 원 그리기 (반지름: 3.0)
/**
* Visitor: 파일 시스템 방문자
*/
public interface FileSystemVisitor {
void visit(File file);
void visit(Directory directory);
}
/**
* Element: 파일 시스템 노드
*/
public interface FileSystemNode {
void accept(FileSystemVisitor visitor);
String getName();
}
/**
* ConcreteElement: 파일
*/
public class File implements FileSystemNode {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public String getName() {
return name;
}
public int getSize() {
return size;
}
@Override
public void accept(FileSystemVisitor visitor) {
visitor.visit(this);
}
}
/**
* ConcreteElement: 디렉토리
*/
public class Directory implements FileSystemNode {
private String name;
private List<FileSystemNode> children;
public Directory(String name) {
this.name = name;
this.children = new ArrayList<>();
}
public void add(FileSystemNode node) {
children.add(node);
}
@Override
public String getName() {
return name;
}
public List<FileSystemNode> getChildren() {
return children;
}
@Override
public void accept(FileSystemVisitor visitor) {
visitor.visit(this);
}
}
/**
* ConcreteVisitor: 크기 계산
*/
public class SizeCalculator implements FileSystemVisitor {
private int totalSize = 0;
@Override
public void visit(File file) {
totalSize += file.getSize();
System.out.println("📄 " + file.getName() + ": " + file.getSize() + " KB");
}
@Override
public void visit(Directory directory) {
System.out.println("📁 " + directory.getName());
for (FileSystemNode child : directory.getChildren()) {
child.accept(this); // 재귀 방문
}
}
public int getTotalSize() {
return totalSize;
}
}
/**
* ConcreteVisitor: 파일 검색
*/
public class FileSearcher implements FileSystemVisitor {
private String extension;
private List<String> results;
public FileSearcher(String extension) {
this.extension = extension;
this.results = new ArrayList<>();
}
@Override
public void visit(File file) {
if (file.getName().endsWith(extension)) {
results.add(file.getName());
System.out.println("🔍 발견: " + file.getName());
}
}
@Override
public void visit(Directory directory) {
for (FileSystemNode child : directory.getChildren()) {
child.accept(this);
}
}
public List<String> getResults() {
return results;
}
}
/**
* 사용 예제
*/
public class FileSystemVisitorExample {
public static void main(String[] args) {
// 파일 시스템 구성
Directory root = new Directory("root");
Directory docs = new Directory("documents");
docs.add(new File("report.pdf", 100));
docs.add(new File("memo.txt", 10));
Directory images = new Directory("images");
images.add(new File("photo1.jpg", 500));
images.add(new File("photo2.jpg", 600));
root.add(docs);
root.add(images);
root.add(new File("readme.txt", 5));
// 크기 계산
System.out.println("=== 크기 계산 ===");
SizeCalculator sizeCalc = new SizeCalculator();
root.accept(sizeCalc);
System.out.println("\n총 크기: " + sizeCalc.getTotalSize() + " KB");
// 파일 검색
System.out.println("\n=== .txt 파일 검색 ===");
FileSearcher searcher = new FileSearcher(".txt");
root.accept(searcher);
System.out.println("\n검색 결과: " + searcher.getResults());
}
}/**
* Visitor: 표현식 방문자
*/
public interface ExpressionVisitor {
int visit(NumberExpression expr);
int visit(AddExpression expr);
int visit(MultiplyExpression expr);
}
/**
* Element: 표현식
*/
public interface Expression {
int accept(ExpressionVisitor visitor);
}
/**
* ConcreteElement: 숫자
*/
public class NumberExpression implements Expression {
private int value;
public NumberExpression(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public int accept(ExpressionVisitor visitor) {
return visitor.visit(this);
}
}
/**
* ConcreteElement: 덧셈
*/
public class AddExpression implements Expression {
private Expression left;
private Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public Expression getLeft() {
return left;
}
public Expression getRight() {
return right;
}
@Override
public int accept(ExpressionVisitor visitor) {
return visitor.visit(this);
}
}
/**
* ConcreteElement: 곱셈
*/
public class MultiplyExpression implements Expression {
private Expression left;
private Expression right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public Expression getLeft() {
return left;
}
public Expression getRight() {
return right;
}
@Override
public int accept(ExpressionVisitor visitor) {
return visitor.visit(this);
}
}
/**
* ConcreteVisitor: 계산기
*/
public class Evaluator implements ExpressionVisitor {
@Override
public int visit(NumberExpression expr) {
return expr.getValue();
}
@Override
public int visit(AddExpression expr) {
int left = expr.getLeft().accept(this);
int right = expr.getRight().accept(this);
System.out.println(" " + left + " + " + right + " = " + (left + right));
return left + right;
}
@Override
public int visit(MultiplyExpression expr) {
int left = expr.getLeft().accept(this);
int right = expr.getRight().accept(this);
System.out.println(" " + left + " * " + right + " = " + (left * right));
return left * right;
}
}
/**
* ConcreteVisitor: 프린터
*/
public class ExpressionPrinter implements ExpressionVisitor {
@Override
public int visit(NumberExpression expr) {
System.out.print(expr.getValue());
return 0; // 사용 안 함
}
@Override
public int visit(AddExpression expr) {
System.out.print("(");
expr.getLeft().accept(this);
System.out.print(" + ");
expr.getRight().accept(this);
System.out.print(")");
return 0;
}
@Override
public int visit(MultiplyExpression expr) {
System.out.print("(");
expr.getLeft().accept(this);
System.out.print(" * ");
expr.getRight().accept(this);
System.out.print(")");
return 0;
}
}
/**
* 사용 예제
*/
public class ExpressionVisitorExample {
public static void main(String[] args) {
// (2 + 3) * (4 + 5)
Expression expr = new MultiplyExpression(
new AddExpression(
new NumberExpression(2),
new NumberExpression(3)
),
new AddExpression(
new NumberExpression(4),
new NumberExpression(5)
)
);
// 표현식 출력
System.out.println("=== 표현식 출력 ===");
ExpressionPrinter printer = new ExpressionPrinter();
expr.accept(printer);
System.out.println();
// 계산
System.out.println("\n=== 계산 ===");
Evaluator evaluator = new Evaluator();
int result = expr.accept(evaluator);
System.out.println("\n결과: " + result);
}
}| 장점 | 설명 | 예시 |
|---|---|---|
| OCP 준수 | 새 연산 추가 용이 | Visitor만 추가 |
| SRP 준수 | 연산을 별도 클래스로 | AreaCalculator |
| 연산 집중 | 관련 연산이 한 곳에 | XMLExporter |
| 타입 안전 | instanceof 제거 | 메서드 오버로딩 |
| 단점 | 설명 | 해결책 |
|---|---|---|
| Element 변경 어려움 | 새 타입 추가 시 모든 Visitor 수정 | 안정적 구조에만 |
| 캡슐화 약화 | Element 내부 노출 | getter 최소화 |
| 복잡도 | Double Dispatch | 문서화 |
// 잘못된 예: Element가 자주 추가/삭제
interface Shape {
void accept(Visitor v);
}
// Pentagon, Hexagon... 계속 추가
// → 모든 Visitor 수정!해결:
// 안정적인 구조에만 Visitor 사용
// 자주 변하는 구조는 Strategy 등 고려✅ Visitor 인터페이스
✅ Element 인터페이스
✅ accept() 메서드
✅ visit() 오버로딩
✅ ConcreteVisitor 구현
| 상황 | 추천도 | 이유 |
|---|---|---|
| 안정적 구조 | ⭐⭐⭐ | Element 변경 적음 |
| 다양한 연산 | ⭐⭐⭐ | 연산 분리 |
| 복잡한 구조 순회 | ⭐⭐⭐ | AST, 파일 시스템 |
| 타입별 처리 | ⭐⭐⭐ | instanceof 제거 |
- Double Dispatch
- 연산과 구조 분리
- 안정적 구조에만
- 타입 안전