컴파일러 개발하려다가 갑자기?, Visitor 패턴

저는 불법입니다.·2025년 9월 1일
3

방문자 패턴: 기존 클래스를 변경하지 않고도 새로운 연산을 추가할 수 있는 객체지향 패턴중 하나

클래스 그룹에 구조를 수정하지 말라!, 그냥 새로운 연산을 추가하는 방식으로 쓰면 된다.

조금 말은 어렵지만, 곱씹어서 생각해봤을때, 객체와 객체의 알고리즘을 분리하자는 의미로 해석했다.

→ 확장성이 증대함으로 가져오는 이점이 매우 크다. 하지만 구현이 매우 어렵다

예시로, 온라인 쇼핑 카트가 있다고 생각하자

책, 전자제품, 의류 등이 있고, 각 품목은 총 가격 계산이나 할인 적용과 같은 작업을 수행하는 방문자를 수용할 수 있어야 한다.

그렇다면, 내 생각은 이렇다 방문자 패턴별로, 할인 적용을 다르게 한다면 클래스를 조금 이름만 수정해보자

그러면 한번 구현해보자.

기본적으로 Mart_Product라는 클래스를 만들자.

class MartProduct {
  public:
    virtual void accept(class Visitor& v) = 0;
    void buy();
    void sell();
};

이제 MartProduct는 Element를 대체한다. 다시 말해, 실제 데이터를 담을 객체는 MartProduct의 구현체라는 말이다. visitor패턴은 관례적으로, 어떤 동작을 정의하고, visitor패턴에서 받아올 세부 동작은 accept()라는 이름으로 정의한다.

이제, MartProduct 객체의 동작들을 살펴보자. 기본적으로 책과 음식을 기준으로 할 것이다.

class Book : MartProduct {
  public:
    void accept(class Visitor& v);
  
    void buy(){
      cout << "buy Book" << endl;
    }

    void sell() {
      cout << "sell Book" << endl;
    }
};

class Food : MartProduct {
  public:
    void accept(class Visitor& v);

    void buy(){
      cout << "buy Food" << endl;
    }

    void sell() {
      cout << "sell Food" << endl;
    }
};

마트와 책을 MartProduct를 상속 받아 구현을 해주었다. 이렇게 된다면, Book객체와 Food객체는 buy, sell동작을 하고, visitor패턴에서 행동을 정의하는 것은 accept로 호출해내면 된다.

다음으로 Visitor 객체을 구현해보자.

class Visitor {
  public:
    virtual void visit(Book *) = 0;
    virtual void visit(Food *) = 0;
};

Visitor는 각 객체별로 받아오는 매개변수만 다르게 설정해주어 각 객체마다의 알고리즘을 따로 구현해주면 된다.

예를 들어서, 현재 생성된 객체가 Book이고, Book의 buy()는 10%할인을 적용시킨 가격이라고 생각한다면,

book일때 discount_percent 값을 10으로 설정해주고, Food일 경우면, discount_percent를 적용시키지 않은 로직을 구현하면 된다.

위에선 결과 확인용으로 간편하게 궇녀하기 위해 buy, sell의 메서드 안에 print로만 적용시켜서 정말 간단하게만 구현하려고 했다. (추후 시간이 되거나, 필요시 수정하겟다.)

이제 그러면, Visitor라는 객체를 받아오는 로직도 설정이 끝났고, 위에서 accept로 호출해내는것과 마찬가지로, 실제 동작은 visit으로 정의한다.

이제, 각각 동작을 visitor를 상속받은 형태로 조금 더 자세히 구현하자.(지금 상태로는 호출이고, buy고 sell이고 전부 구분이 안되기 때문에 불가능하다.)

class BuyProductVisitor : public Visitor {
public:
    void visit(Book& b) override { b.buy(); }
    void visit(Food& f) override { f.buy(); }
};
class SellProductVisitor : public Visitor {
public:
    void visit(Book& b) override { b.sell(); }
    void visit(Food& f) override { f.sell(); }
};

Element에서 우리는 sell과 buy동작은 MartProduct에선 기본적으로 가져야하는 연산.동작이다. 그러면, visitor패턴을 이용해서 공통되는 부분을 추상화 하여 Visitor에서 해당 구현체만 받아오면 이제 나머지 동작은 클래스를 이용해서 구분하려는게 목적이다.

인터페이스에서 정의한 동작은 다음과 같이 2개다

void buy();
void sell();

그러면, 이걸 분리시켜주는 작업을 하기 위해, 그리고 사용자는 그냥 함수만 호출해서 사용하게끔 하기 위해 아니, 특정 객체를 불러서 호출하는게 아닌, 타입에 맞게 적재적소에 올바른 처리를 해주기 위해 우리는

Visitor를 상속받은 BuyProductVisitor와 SellProductVisitor를 구현해주면 된다. 상위 인터페이스에서 받아온건 객체의 타입별로 로직을 다르게 수정하게 불러오는 역할만 했고, 실제 동작은 Buy, Sell을 구분하여 들어오면 올바른 처리를 할 수 있게 구현 했다.

추가 토론 내용으로, Visitor안에서 로직을 구현해야 하나, 아니면 Element객체 안에서 로직을 구현하고
그걸 호출해내냐의 토론이 있었지만, 응집도와 모듈의 독립성을 판단하여, Visitor는 방문의 역할만 수행하면 되기 때문에
Element안에 알고리즘을 구현하는게 옳다고 판단했다.

이제 이렇게 되면 visitor패턴은 전부 정의했고, 추가로 c++의 개념 부족한 부분이 몇가지 있었는데, 자바를 쓰면서 override를 정말 편하게 해준다는것을 깨달았다. 이건 객체를 상속받으면 각각 Override를 직접 입력해주어야 하고(물론 자바도 ide가 해준것) 그리고, 아직 c++의 호출이나 선언, heap과 stack, static등의 메모리 할당의 구조를 잘 모르고 있기 때문에, 이런 객체지향 설계가 너무 어렵다고 생각이 들었다.

void Book::accept(Visitor& v) { v.visit(*this); }
void Food::accept(Visitor& v) { v.visit(*this); }

마지막으로, accept만 받아주고, visitor가 들어오는것에 따라 visit메서드를 호출해주면, 우리가 생각하려는 코드는 완성된다.

테스트용 메인함수

int main() {
    Book book;
    Food food;

    BuyProductVisitor buyVisitor;
    SellProductVisitor sellVisitor;

    cout << "== 구매 ==" << endl;
    book.accept(buyVisitor);
    food.accept(buyVisitor);

    cout << "== 판매 ==" << endl;
    book.accept(sellVisitor);
    food.accept(sellVisitor);

    return 0;
}
== 구매 ==
buy Book
buy Food
== 판매 ==
sell Book
sell Food
profile
만지면 300만원 내야해요, 참고로 호주에 삽니다.

2개의 댓글

comment-user-thumbnail
2025년 9월 1일

안녕하세요. 벨로그 방문왔습니다.

1개의 답글