프로그래밍/디자인패턴

방문자 패턴(Visitor Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 18:31

"쇼핑몰 아이템 클래스(책, 과일, 가전)가 이미 완성되어 있습니다. 그런데 갑자기 '배송비 계산' 로직을 추가하고, 그다음엔 'XML 변환' 로직을, 그다음엔 '할인 적용' 로직을 계속 추가해야 한다면?"

기존 클래스(Book, Fruit)를 계속 수정하는 것은 OCP(개방-폐쇄 원칙) 위반입니다. 이때 데이터 구조(아이템)는 그대로 두고, 새로운 기능(방문자)만 따로 만들어서 끼워 넣는 것이 방문자 패턴입니다.

하지만 이 패턴은 "구현이 너무 복잡하다"는 악명이 높았습니다. Java 최신 버전이 나오기 전까지는 말이죠.

Getty Images
 

1. 방식 1: 클래식 방문자 (Double Dispatch) - "GoF의 정석"

객체지향 언어에서 오버로딩(Overloading)의 한계를 극복하기 위해 **두 번의 호출(Double Dispatch)**을 사용하는 방식입니다.

구조

  1. Element (방문 당하는 객체): accept(Visitor v) 메서드를 통해 방문자를 받아들입니다.
  2. Visitor (방문자): visit(Book b), visit(Fruit f) 처럼 각 타입별 처리 로직을 가집니다.

코드 예시

Java
 
// 1. 방문자 인터페이스
interface Visitor {
    void visit(Book book);
    void visit(Fruit fruit);
}

// 2. 요소 인터페이스 (Element)
interface Item {
    void accept(Visitor v);
}

// 3. 구체적 요소들
class Book implements Item {
    public void accept(Visitor v) {
        // 더블 디스패치: "나(Book)를 방문해줘"라고 방문자에게 다시 요청
        v.visit(this); 
    }
}

class Fruit implements Item {
    public void accept(Visitor v) {
        v.visit(this);
    }
}

// 4. 구체적 방문자 (배송비 계산 로직)
class ShippingVisitor implements Visitor {
    int totalCost = 0;

    public void visit(Book book) { totalCost += 2500; }  // 책은 2500원
    public void visit(Fruit fruit) { totalCost += 5000; } // 과일은 5000원
}

장단점

  • 장점: 데이터 클래스(Book)를 건드리지 않고, ExportXmlVisitor, DiscountVisitor 등 기능을 무한히 추가할 수 있습니다.
  • 단점:
    • 구조 수정의 어려움: 만약 Electronics라는 새 아이템이 추가되면, 모든 Visitor 인터페이스와 구현체를 수정해야 하는 대참사가 일어납니다.
    • 복잡함: accept -> visit으로 이어지는 핑퐁 로직(Double Dispatch)이 이해하기 어렵습니다.

2. 방식 2: 모던 방문자 (Pattern Matching) - "Java 17+ 스타일"

Java 17의 Switch Pattern Matching과 sealed class를 활용하면, accept 메서드나 Visitor 인터페이스 없이도 똑같은 효과를 훨씬 직관적으로 낼 수 있습니다.

특징

  • 거추장스러운 Double Dispatch(accept 호출) 과정을 제거합니다.
  • instanceof 검사와 형 변환(Casting)을 Switch 문 안에서 우아하게 처리합니다.

코드 예시

Java
 
// 1. 요소 정의 (sealed class로 하위 타입을 제한하면 더 안전함)
sealed interface Item permits Book, Fruit {}
final class Book implements Item {}
final class Fruit implements Item {}

// 2. 방문자 역할 (별도 클래스 혹은 서비스 메서드)
public class ShippingService {
    
    public int calculateShipping(Item item) {
        // accept 메서드 없이, 타입 패턴 매칭으로 해결
        return switch (item) {
            case Book b -> 2500;
            case Fruit f -> 5000;
            // 만약 Item의 하위 타입이 늘어났는데 여기서 처리 안 하면 컴파일 에러 발생 (안전성 보장)
        };
    }
}

장단점

  • 장점:
    • 간결함: accept 메서드를 뚫을 필요가 없어 도메인 모델이 깨끗해집니다.
    • 가독성: 로직이 한곳(switch 문)에 모여 있어 흐름 파악이 쉽습니다.
  • 단점: 순수 객체지향보다는 절차지향적/함수형 접근에 가깝습니다. (하지만 실무에서는 이 방식이 압도적으로 선호됩니다.)

3. 실무 예시: 언제 쓰이나요?

1) 컴파일러 / 인터프리터 설계

소스 코드의 구문 트리(AST)를 순회하면서 검사할 때, 노드의 종류(IfNode, ForNode, WhileNode)에 따라 다른 처리를 해야 합니다. 이때 방문자 패턴이 필수적입니다.

2) 파일 시스템 처리

폴더와 파일을 순회하면서 "용량 계산"을 할 수도 있고, "압축"을 할 수도 있습니다. 데이터 구조(파일 트리)는 고정되어 있지만, 해야 할 작업(알고리즘)이 계속 늘어날 때 사용합니다.


요약: 고전파 vs 현대파

구분 Classic Visitor (Double Dispatch) Modern Visitor (Pattern Matching)
Java 버전 모든 버전 사용 가능 Java 17 이상 권장
핵심 메커니즘 obj.accept(visitor) -> visitor.visit(obj) switch (obj) { case Type t -> ... }
도메인 객체 accept 메서드 구현 필수 (침투적) 수정 불필요 (비침투적)
타입 안전성 컴파일 타임에 완벽 보장 sealed class와 함께 쓰면 보장됨
추천 상황 레거시 시스템, JDK 버전이 낮을 때 신규 프로젝트, 모던 자바 환경

결론

방문자 패턴은 "데이터 구조(Class)는 잘 안 변하는데, 기능(Method)이 자주 추가될 때" 쓰는 패턴입니다.

  • 만약 여러분이 Java 17 이상을 쓰고 있다면, 복잡한 accept/visit 구조를 만들기보다 Switch Pattern Matching을 적극 활용하세요. 코드가 절반 이하로 줄어드는 마법을 경험할 수 있습니다.