728x90
반응형
클래스 구조는 이미 잘 짜여 있는데, 여기에 새로운 기능을 추가할 때마다 기존 클래스를 수정해야 한다면 어떨까요? 기능이 늘어날 때마다 코드는 복잡해지고, 자칫 기존 로직을 건드려 버그가 생길 수도 있습니다.
오늘은 "객체의 구조는 그대로 두고, 동작만 외부에서 추가"하는 비지터 패턴(Visitor Pattern)에 대해 깊이 있게 알아보겠습니다.

1. 비지터 패턴이란?
비지터 패턴은 데이터 구조(객체)와 그 데이터 위에서 수행되는 알고리즘(동작)을 분리하는 패턴입니다.
핵심 아이디어
- Element (장소): 데이터 구조를 나타내며, 방문자를 받아들이는 accept() 메서드를 가집니다.
- Visitor (방문객): 데이터 구조를 돌아다니며 특정 작업을 수행하는 객체입니다.
쉽게 말해, 장소(Element)는 그대로 있고, 방문객(Visitor)이 돌아다니며 각 장소에 맞는 특별한 행동을 하는 것과 같습니다.
2. 왜 비지터 패턴을 쓰는가? (더블 디스패치)
비지터 패턴은 더블 디스패치(Double Dispatch)라는 독특한 방식으로 동작합니다.
- 첫 번째 디스패치: 어떤 Element 객체의 accept()가 호출되는지 결정됩니다.
- 두 번째 디스패치: accept() 내부에서 visitor.visit(this)를 호출하여, 어떤 Visitor의 메서드가 실행될지 결정됩니다.
이 과정을 통해 우리는 클래스 타입을 체크하는 instanceof나 긴 if-else 문 없이도 런타임에 적절한 로직을 수행할 수 있게 됩니다.
3. Java 실무 예제: 쇼핑 카트 할인 시스템
과일과 과자가 담긴 장바구니에 '할인 혜택'과 '포인트 적립'이라는 기능을 클래스 수정 없이 추가해 보겠습니다.
Step 1. Visitor와 Element 인터페이스
Java
// 모든 방문자가 구현해야 할 인터페이스
interface ShoppingVisitor {
void visit(Fruit fruit);
void visit(Snack snack);
}
// 모든 상품이 구현해야 할 인터페이스
interface Product {
void accept(ShoppingVisitor visitor);
}
Step 2. Concrete Element (데이터 구조)
Java
class Fruit implements Product {
private String name;
private int price;
public Fruit(String name, int price) { this.name = name; this.price = price; }
public int getPrice() { return price; }
@Override
public void accept(ShoppingVisitor visitor) {
visitor.visit(this); // 방문자에게 자신을 맡깁니다.
}
}
class Snack implements Product {
private int price;
public Snack(int price) { this.price = price; }
public int getPrice() { return price; }
@Override
public void accept(ShoppingVisitor visitor) {
visitor.visit(this);
}
}
Step 3. Concrete Visitor (기능 추가)
Java
// 할인 로직을 담당하는 방문자
class DiscountVisitor implements ShoppingVisitor {
@Override
public void visit(Fruit fruit) {
System.out.println(fruit.getPrice() * 0.9 + "원 (과일 10% 할인 적용)");
}
@Override
public void visit(Snack snack) {
System.out.println(snack.getPrice() * 0.95 + "원 (과자 5% 할인 적용)");
}
}
Step 4. 실행 결과
Java
public class VisitorDemo {
public static void main(String[] args) {
Product[] cart = {new Fruit("사과", 2000), new Snack(1500)};
ShoppingVisitor discount = new DiscountVisitor();
for (Product product : cart) {
product.accept(discount);
}
}
}
4. 비지터 패턴의 장단점
👍 장점
- OCP(개방-폐쇄 원칙) 극대화: 새로운 기능(Visitor)을 추가할 때 기존 Element 클래스를 전혀 수정하지 않아도 됩니다.
- 단일 책임 원칙(SRP) 준수: 관련된 알고리즘을 한곳(Visitor 클래스)에 모아 관리할 수 있습니다.
- 유연성: 서로 연관 없는 다양한 동작을 동일한 객체 구조 위에 적용할 수 있습니다.
👎 단점
- 구조 변경의 어려움: 새로운 Element 클래스가 추가되면 모든 Visitor 인터페이스와 구현체를 수정해야 합니다. (구조가 고정된 경우에만 추천!)
- 캡슐화 위반: Visitor가 작업을 수행하기 위해 Element의 내부 상태(getter 등)를 외부에 노출해야 할 수도 있습니다.
5. 실제 사용 사례
- 컴파일러: 추상 구문 트리(AST)를 분석할 때 각 노드(변수, 연산자 등)를 방문하여 최적화나 코드 생성을 수행합니다.
- 파일 시스템: 디렉토리 구조를 돌며 크기를 계산하거나, 특정 파일을 찾는 로직을 분리할 때 사용합니다.
- 문서 생성기: XML이나 HTML 구조를 방문하여 PDF, 텍스트 등 다양한 포맷으로 변환할 때 유용합니다.
결론
비지터 패턴은 "데이터 구조는 변하지 않는데, 기능은 계속 늘어날 것 같다"는 확신이 들 때 최고의 선택입니다. 구조와 동작을 분리하여 코드의 유연성을 극대화해 보세요!
포스팅이 도움이 되셨다면 공감과 구독 부탁드립니다! 여러분은 비지터 패턴을 어떤 복잡한 구조에 적용해보고 싶으신가요? 댓글로 의견을 들려주세요!
반응형
'Mobile & App Stack > Java Software Architecture & Patterns' 카테고리의 다른 글
| 실무에서 바로 써먹는 디자인 패턴 활용 사례 및 오남용 방지 가이드 (0) | 2025.01.07 |
|---|---|
| 이터레이터 패턴: 데이터 구조와 상관없이 일관되게 순회하는 법 (0) | 2025.01.06 |
| 상태 패턴(State Pattern): if-else 조건문 지옥에서 탈출하는 법 (0) | 2025.01.04 |
| 템플릿 메서드 패턴: 중복 코드를 줄이는 상속의 기술 (Java 예제) (0) | 2025.01.03 |
| 커맨드 패턴 완벽 정리: Undo/Redo 기능을 만드는 가장 스마트한 방법 (0) | 2025.01.02 |