275. 방문자 (Visitor) - 객체 구조 변경 없이 새로운 연산 추가
핵심 인사이트 (3줄 요약)
- 본질: 방문자(Visitor) 패턴은 데이터(객체 구조)와 알고리즘(연산)을 분리하여, 기존의 복잡한 객체 구조(주로 트리나 컴포지트 패턴)를 변경하지 않고도 새로운 연산(기능)을 무한히 추가할 수 있게 해주는 행동(Behavioral) 패턴이다.
- 가치: 데이터 클래스(Element) 내부에 수많은 비즈니스 로직 메서드들을 쑤셔 넣는 대신, 외부의 '방문자' 객체로 연산을 몰아냄으로써 OCP(개방-폐쇄 원칙)와 SRP(단일 책임 원칙)를 극단적으로 준수하게 만든다.
- 융합: '이중 디스패치(Double Dispatch)'라는 컴파일러 런타임 기술을 응용하여 구현되며, AST(추상 구문 트리)를 분석하는 컴파일러 설계, 문서 파싱(XML/HTML), 복잡한 보고서 생성기 등에서 핵심 데이터 탐색 아키텍처로 사용된다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 방문자 패턴은 연산을 수행할 대상 객체(Element)가 '방문자(Visitor)' 객체를
accept(Visitor)메서드로 받아들이면, 내부에서 자신(this)을 다시 방문자의 매개변수로 넘겨주어(visitor.visit(this)) 방문자가 대상 객체의 데이터를 바탕으로 연산을 수행하게 하는 설계 기법이다. -
필요성: 회사의 조직도 트리(부서-팀-직원)가 있다. 인사팀은 '총 연봉 계산' 기능을 원하고, 총무팀은 '부서별 비품 개수 계산', 개발팀은 '사내 연락망 출력' 기능을 원한다. 매번 새로운 요구사항이 생길 때마다
Department와Employee클래스 안에calcSalary(),calcItems(),printContact()메서드를 끝없이 추가해야 한다면, 데이터 클래스는 수만 줄짜리 괴물이 되고 만다. 데이터 구조는 가만히 냅두고, '계산기'나 '출력기'가 데이터 구조를 돌아다니며 작업을 수행하게 할 수는 없을까? -
💡 비유: 박물관(객체 구조)에 공룡 뼈와 미라(요소들)가 전시되어 있습니다. 전시품은 가만히 뼈와 붕대(데이터)만 유지합니다. 대신 청소부(방문자 A)가 오면 먼지를 털고, 사진작가(방문자 B)가 오면 사진을 찍고, 큐레이터(방문자 C)가 오면 상태를 감정합니다. 새로운 직업(새 연산)이 필요하면 전시품을 뜯어고칠 필요 없이 새로운 방문자만 고용하면 됩니다.
-
등장 배경 및 발전 과정:
- 데이터와 로직의 강결합 문제: 객체 지향의 기본 철학(데이터와 행위의 결합)에 너무 충실한 나머지, 이질적인 수많은 행위(로직)가 데이터 클래스에 과적재되는 현상이 발생했다.
- 기능의 외부 위임 필요성 대두: "자주 변하는 것(로직)과 변하지 않는 것(데이터 구조)을 분리하라"는 원칙에 따라, 행위를 별도의 클래스 집합(Visitor)으로 뜯어냈다.
- 이중 디스패치(Double Dispatch)의 발견: 다형성을 두 번 연속으로 발생시켜, 런타임에 '누가(어느 방문자가)' '무엇을(어떤 요소를)' 처리할지를 동적으로 결정하는 언어적 트릭을 패턴화했다.
-
📢 섹션 요약 비유: 택배 박스(데이터)에 온갖 배송 규칙과 요금 계산법을 인쇄해 두면 박스가 너무 지저분해집니다. 박스는 그냥 물건만 담고 있고, '저울(방문자 A)'에 올리면 요금이 나오고 '바코드 스캐너(방문자 B)'에 찍으면 배송지가 나오는 것처럼, 연산을 외부 기계로 빼내는 원리입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소 (클래스 다이어그램)
| 요소명 | 역할 | 비유 |
|---|---|---|
| Visitor (방문자 인터페이스) | 데이터 구조 내의 각 Element 클래스 타입별로 방문할 수 있는 visit(ElementA), visit(ElementB) 메서드를 모두 오버로딩(Overloading)하여 선언한다. | 박물관 직원의 행동 매뉴얼 (공룡뼈 닦기, 미라 닦기) |
| ConcreteVisitor | Visitor를 구현하여, 특정 목적(예: XML 추출, 요금 계산)을 달성하기 위한 구체적인 알고리즘을 작성한다. | 청소부, 사진작가 |
| Element (요소 인터페이스) | 방문자를 자신의 내부로 받아들이는 accept(Visitor v) 추상 메서드를 정의한다. | 박물관의 모든 전시품 규격 |
| ConcreteElement | accept(Visitor v)를 구현할 때, 반드시 v.visit(this); 한 줄을 호출하여 방문자에게 자기 자신의 타입(this)을 넘겨준다. | 공룡 뼈, 미라 |
| ObjectStructure | 요소(Element)들을 포함하고 있는 복합 데이터 구조(주로 List나 Composite 트리). 요소들을 순회하며 방문자를 밀어 넣는 역할을 한다. | 박물관 건물 전체 |
동작 메커니즘 (코드 뼈대 구조) : 이중 디스패치의 마법
방문자 패턴의 심장부는 **이중 디스패치(Double Dispatch)**다. 컴파일러는 v.visit(element)를 만났을 때 element가 정확히 A인지 B인지 컴파일 타임에는 모른다. 이를 런타임에 동적으로 매핑하기 위해 메서드 호출을 두 번(핑퐁) 튕긴다.
┌─────────────────────────────────────────────────────────────┐
│ 이중 디스패치(Double Dispatch)의 핑퐁 릴레이 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [Client] │
│ Element e = new ElementA(); │
│ Visitor v = new SalaryVisitor(); │
│ │
│ // 1차 디스패치: 어떤 Element의 accept를 부를지 동적 결정 │
│ e.accept(v); ────────────────────┐ │
│ │ │
│ ┌────────────────────────────────▼────────────────┐ │
│ │ [ElementA 클래스 내부] │ │
│ │ public void accept(Visitor v) { │ │
│ │ // 2차 디스패치: 나(this=A)를 들고 어떤 Visitor의 │ │
│ │ // visit 메서드를 부를지 동적 결정 │ │
│ │ v.visit(this); ────────────┐ │ │
│ │ } │ │ │
│ └────────────────────────────────┼────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼────────────────┐ │
│ │ [SalaryVisitor 클래스 내부] │ │
│ │ public void visit(ElementA a) { │ │
│ │ // 최종 로직: ElementA 전용 연봉 계산 수행 │ │
│ │ } │ │
│ │ public void visit(ElementB b) { ... } │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
[다이어그램 해설] Java나 C# 같은 단일 디스패치 언어는 visitor.visit(element)에서 element의 실제 타입(A인지 B인지)을 런타임 다형성으로 풀어내지 못한다. 그래서 일단 element.accept(visitor)로 먼저 호출(1차)하여 실제 객체(A) 안으로 들어간 뒤, 그 안에서 명확해진 내 타입(this)을 visitor.visit(this)로 던져넣는(2차) 우회로를 쓴다. 이 핑퐁 기법 덕분에 SalaryVisitor는 A에 딱 맞는 계산 로직을 정확히 찾아 실행할 수 있다.
반복자(Iterator) 패턴과의 차이점
방문자와 반복자 모두 '컬렉션을 순회'하는 성격을 가지지만, 목적이 다르다.
-
반복자 (Iterator): 목적은 **데이터 추출(순차 접근)**이다. "다음 것 줘, 다음 것 줘" 하면서 꺼내오는 데 집중한다.
-
방문자 (Visitor): 목적은 **연산(Operation)**이다. "너희 집(객체)에 들어가서, 네 타입에 맞는 특수 작업을 해줄게"라는 비즈니스 로직 처리에 집중한다. 주로 이터레이터가 각 객체를 꺼내고, 방문자가 그 객체 안으로 들어가는 방식으로 혼합해서 쓴다.
-
📢 섹션 요약 비유: 이터레이터가 창고에서 사과, 배를 순서대로 하나씩 꺼내주는 '컨베이어 벨트'라면, 방문자는 그 과일들에 각기 다른 농약(연산)을 맞춤형으로 뿌려주는 '살포 기계'입니다.
Ⅲ. 융합 비교 및 다각도 분석
1. 방문자 패턴의 치명적인 모순 (Trade-off)
방문자 패턴은 무적의 아키텍처가 아니다. OCP(개방-폐쇄 원칙)를 절반만 달성하는 **'비대칭적 OCP'**의 특징을 가진다.
| 시나리오 | 방문자 패턴의 대응력 | 결론 |
|---|---|---|
| 새로운 '연산(로직)'이 자주 추가될 때 | NewVisitor 클래스 1개만 만들면 됨. 기존 데이터 클래스는 전혀 수정 불필요. | 환상적 (Very Good) |
| 새로운 '데이터 타입(Element)'이 추가될 때 | ElementC가 추가되면, 기존에 만들어둔 수십 개의 Visitor 인터페이스와 구현체에 visit(ElementC)를 모조리 뜯어고치며 추가해야 함. | 재앙 (Disaster) |
이러한 모순 때문에, 데이터 구조(Element의 종류)가 향후 거의 변하지 않고 안정적으로 확정된 상태에서만 방문자 패턴을 도입해야 한다. 컴파일러의 AST(추상 구문 트리 - if문, while문, 변수 선언 등 노드의 종류가 변할 일이 없음) 구조에 방문자 패턴이 국룰로 쓰이는 이유가 바로 이 때문이다.
과목 융합 관점
-
소프트웨어 공학 (SE) / 설계: 비지터 패턴은 마이크로서비스 아키텍처(MSA)에서 여러 도메인 객체를 거치며 데이터를 수집/가공해야 하는 복잡한 오케스트레이션(Orchestration) 로직을 짤 때, 도메인 엔티티를 오염시키지 않고 외부의 'Service'가 각 엔티티의 '내부 속살'을 활용해 비즈니스를 처리하도록 돕는 철학적 배경이 된다.
-
컴파일러 (Compiler): 렉서(Lexer)와 파서(Parser)가 소스 코드를 분석해 만든 거대한 AST(Abstract Syntax Tree) 트리를 순회하면서, 어떤 방문자는 '문법 오류 체크'를 하고, 어떤 방문자는 '최적화'를 하고, 어떤 방문자는 '기계어 코드(바이트코드) 생성'을 한다. 컴파일러 설계의 핵심 뼈대다.
-
📢 섹션 요약 비유: 옷장(객체 구조) 안에 옷 종류(바지, 치마, 셔츠)가 영원히 안 바뀐다면, 새로운 세탁법(방문자)을 추가하기 너무 편합니다. 하지만 갑자기 '한복'이라는 새 옷 종류가 추가되면, 전국의 모든 세탁소(수많은 방문자들) 매뉴얼을 다 뜯어고쳐야 하는 대공사가 벌어집니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 다국어 문서 변환기(Markdown/HTML 파서) 개발: 마크다운 형식으로 작성된 문서 트리가 있다. 노드 종류는
TitleNode,TextNode,ImageNode,LinkNode딱 4개로 고정되어 있다. 이 문서를 HTML로도 변환하고, PDF로도 변환하고, 일반 Text로도 추출해야 한다.- 아키텍트의 해결책: 각 노드(Element) 안에
toHtml(),toPdf()를 모두 넣는 것은 미친 짓이다. 노드 구조는accept(Visitor)만 열어두고 고정시킨다. 그리고HtmlExportVisitor,PdfExportVisitor를 만들어 각 노드를 순회하며 자신이 원하는 형식으로 렌더링 문자열을 토해내게 만든다. 완벽한 방문자 패턴의 교과서적 도입 사례다.
- 아키텍트의 해결책: 각 노드(Element) 안에
-
시나리오 — 클라우드 인프라 자원 비용 계산기: 클라우드 상에
EC2_Instance,S3_Bucket,RDS_Database등 수만 개의 자원이 컴포지트 패턴(그룹핑)으로 관리되고 있다. 매달 요금을 정산해야 하는데, 회사 내부 비용, 고객 청구 비용, 세금 포함 비용 등 계산 알고리즘이 매주 바뀐다.- 아키텍트의 해결책: 클라우드 자원(Element)의 종류는 잘 바뀌지 않지만 요금 정책(로직)은 수시로 변한다. 요금 계산 로직을 분리해
InternalCostVisitor,CustomerBillingVisitor로 쪼개어 인프라 트리 노드를 순회(accept)시킨다. 인프라 객체는 단순히 CPU 코어 수, 용량 데이터만 내어주며(캡슐화 허용) 계산의 책임에서 해방된다.
- 아키텍트의 해결책: 클라우드 자원(Element)의 종류는 잘 바뀌지 않지만 요금 정책(로직)은 수시로 변한다. 요금 계산 로직을 분리해
도입 체크리스트
- 기술적: 방문자 객체가 연산을 수행하려면 Element 객체의 내부 데이터(필드값)를 읽어야 한다. 이때 필연적으로 Element의
Getter를 많이 열어주어야 하므로, 캡슐화(Encapsulation)가 훼손될 위험이 있다. 캡슐화 파괴의 손해보다 로직 분리의 이득이 압도적으로 큰가? - 설계적: 데이터 요소(Element 클래스)의 종류가 앞으로 얼마나 자주 추가될 것인가? 한 달에 한 번씩
Element종류가 늘어나는 시스템이라면 절대로 방문자 패턴을 도입해선 안 된다.
안티패턴
-
Visitor 내부의 비대한 분기문 (If-else Type Checking): 이중 디스패치(
accept↔visit)의 핑퐁을 설계하기 귀찮아서, Visitor 안에visit(Element e)메서드 딱 1개만 만들고, 그 안에서if (e instanceof ElementA)로 타입 검사를 도배하는 끔찍한 안티패턴. 이는 다형성을 죽이고 객체 지향을 C언어 수준으로 후퇴시키는 행위다. -
📢 섹션 요약 비유: 이중 디스패치(핑퐁)는 직원(A, B)과 손님(X, Y)이 "너 누구야?", "나 A인데 너는?", "나는 X니까 우리 1번 룰대로 놀자" 하고 서로 명함을 주고받으며 자동으로 짝을 찾는 아름다운 규칙입니다. 이를 무시하고 안내데스크에서 명단표(if문)를 보고 일일이 지시를 내리면 시스템이 꽉 막혀버립니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 내부 로직 추가 (AS-IS) | 방문자 패턴 도입 (TO-BE) | 개선 효과 |
|---|---|---|---|
| 정량 | 새 기능 1개 추가 시 데이터 클래스 N개 모두 수정 | NewVisitor 클래스 1개만 신규 생성 | 기능 추가로 인한 데이터 객체의 버그/회귀율 0% |
| 정량 | 데이터 클래스 라인 수 폭증 (수천 줄의 God Class) | 데이터와 로직 분리로 클래스당 라인 수 대폭 감소 | 단일 책임 원칙(SRP) 달성 및 코드 가독성 극대화 |
| 정성 | 서로 연관 없는 비즈니스 로직(출력, 계산, 저장)이 뒤엉킴 | 각 비즈니스 로직이 종류별 방문자로 깔끔하게 그룹핑됨 | 기능별 모듈화 및 병렬 개발(팀별 협업) 용이성 확보 |
미래 전망
- 다중 디스패치(Multiple Dispatch) 지원 언어의 확산: Julia, Lisp(CLOS) 같은 최신 언어들은 런타임에 인자들의 타입을 모두 조합해 가장 알맞은 메서드를 호출하는 다중 디스패치를 언어 스펙으로 지원한다. 이런 언어에서는 복잡하게
accept와visit을 핑퐁 치는 전통적 방문자 패턴 없이도, 함수 오버로딩만으로 똑같은 효과를 달성할 수 있어 패턴 자체가 필요 없어진다. - 패턴 매칭(Pattern Matching)의 흡수: Java 21+, C# 8.0+, Scala 등 현대 언어들은 향상된
switch-case와 레코드(Record)/데이터 클래스를 결합한 '패턴 매칭'을 적극 도입했다. 외부 함수에서 타입 매칭만으로 로직을 분리해 낼 수 있어, 무거운 Visitor 클래스를 만들지 않고도 함수형 프로그래밍 방식으로 방문자 패턴의 철학을 우아하게 구현하는 추세다.
참고 표준
- GoF (Gang of Four): Behavioral Patterns - Visitor
- Java API:
java.nio.file.FileVisitor(디렉토리 트리를 순회하며 파일 탐색, 삭제, 복사 등의 작업을 수행하는 완벽한 표준 예시) - 컴파일러 AST 파서: Eclipse ASTVisitor, Roslyn (C#) SyntaxWalker
방문자 패턴은 **"객체 지향(행위와 데이터의 결합)의 한계를 인정하고, 절차 지향(행위와 데이터의 분리)의 이점을 전략적으로 차용"**한 고도의 아키텍처 타협안이다. 기술사는 데이터 클래스가 끝없는 요구사항(로직)의 쓰레기장으로 전락하는 것을 막기 위해, 데이터 트리에 '방문자 출입증(accept)' 하나만 발급해 두고, 미래의 수많은 변경(로직)을 외부의 방문자에게 완전히 떠넘기는 방화벽 설계를 할 수 있어야 한다.
- 📢 섹션 요약 비유: 방문자 패턴은 컴퓨터(데이터 구조)를 분해해서 기능을 업그레이드하는 대신, USB 포트(
accept메서드) 하나만 딱 뚫어두고, 필요할 때마다 프린터, 키보드, 마우스(방문자들)를 꽂아서 기능을 무한히 확장하는 플러그 앤 플레이(Plug & Play) 철학의 완성입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 컴포지트 패턴 (Composite) | 방문자가 순회해야 하는 객체 구조(트리)를 만들 때 99% 확률로 결합되어 사용되는 구조적 단짝 패턴. |
| 이터레이터 패턴 (Iterator) | 이터레이터가 컬렉션의 요소를 '꺼내주는' 역할을 담당하고, 방문자는 꺼내진 요소에 '연산을 적용'하는 시너지를 낸다. |
| 이중 디스패치 (Double Dispatch) | 런타임에 다형성을 두 번 발생시켜(객체 타입 확인 + 방문자 타입 확인), 최적의 메서드를 찾아가는 방문자 패턴의 작동 엔진. |
| 단일 책임 원칙 (SRP) | 데이터 보관(Element)과 비즈니스 로직 연산(Visitor)이라는 책임을 완벽하게 분리하여 코드를 정화하는 핵심 원칙. |
| 패턴 매칭 (Pattern Matching) | 향상된 switch문을 통해 외부에서 데이터 타입을 분석하여 분기하는, 방문자 패턴을 해체/대체하고 있는 현대 함수형 패러다임. |
👶 어린이를 위한 3줄 비유 설명
- 동물원에 사자, 코끼리, 기린(데이터 객체들)이 살아요. 이 동물들은 먹고 자는 것만 할 줄 알아요.
- 어느 날 수의사(방문자 A)가 와서 주사를 놔주고, 다음 날엔 사진사(방문자 B)가 와서 사진을 찍어줍니다. 동물들은 그저 가만히 "들어오세요(accept)" 하고 문만 열어주면 돼요.
- 이렇게 동물(데이터)을 직접 훈련(코드 수정)시키지 않고도, 외부의 전문가(방문자)를 불러 새로운 일(연산)을 척척 해내는 똑똑한 방법을 **'방문자 패턴'**이라고 한답니다!