[2022-03-07] - 방문자 패턴

Onni·2022년 3월 7일
1

📌 방문자 패턴이란?

Visitor pattern, 방문자 패턴, 실제 로직을 가지고 있는 객체(Visitor)가 로직을 적용할 객체(Element)를 방문하면서 실행하는 패턴이다. 즉, 로직과 구조를 분리하는 패턴이라고 볼 수 있다. 로직과 구조가 분리되면 구조를 수정하지 않고도 새로운 동작을 기존 객체 구조에 추가할 수 있다.

✅ 도메인 문제

잘나가 쇼핑몰은 고객을 등급별로 나누고 등급에 따라 차별화된 혜택을 주기로 한다. 고객 등급은 Gold, VIP 가 있고 혜택은 포인트 혜택, 할인 혜택이 있다. 고객 등급별 혜택을 줄 수 있는 확장 가능한 해결책을 찾자.

✔ 1. 고객 등급 객체가 받을 혜택을 직접 구현

public interface Member{ }

public class GoldMember implements Member { }

public class VipMember implements Member { }
class GoldMember implements Member {
    public void point() { System.out.println("Point for Gold Member"); }
    public void discount() { System.out.println("Discount for Gold Member"); }
}

class VipMember implements Member {
    public void point() { System.out.println("Point for Vip Member"); }
    public void discount() { System.out.println("Discount for Vip Member"); }
}


public class Main {
    public static void main(String[] args) {
        Member goldMember = new GoldMember();
        Member vipMember = new VipMember();

        goldMember.point();
        vipMember.point();
        goldMember.discount();
        vipMember.discount();
    }
}

문제점

  1. 고객들을 순회하면서 혜택을 주고자 할 때 명시적으로 혜택을 주기 위한 메소드를 호출해야 한다. 즉, iterator를 이용해서 순회하며 처리할 수 없다.
  2. 혜택이 늘어났을 때 모든 멤버들에 대해서 그 혜택을 구현했다는 보장이 없다

=> 두 가지 문제점을 해결하기 위해서 혜택을 위한 인터페이스를 정의하고 멤버들이 인터페이스를 구현하게 한다.

✔ 2. 고객과 혜택 클래스를 분리

  • Meber 인터페이스
public interface Member{ }
public class GoldMember implements Member { }
public class VipMember implements Member { }
  • Benefit 인터페이스
public interface Benefit {
    void point();
    void discount();
}
  • Main
public class Main {
    public static void main(String[] args) {
        Benefit benefit = new BenefitImpl();

        Member goldMember = new GoldMember();
        Member vipMember = new VipMember();
        benefit.point(goldMember);
        benefit.point(vipMember);
        benefit.discount(goldMember);
        benefit.discount(vipMember);
    }
}

문제점

  1. Member 등급이 추가됐을 때 등급별로 혜택을 정확하게 구현했다는 보장이 있는가? 라는 문제

    예를 들어 GreenMember가 추가됐는데 그 멤버에 대한 혜택을 주기 위한 코드가 point에 대해서만 했다 하더라도 컴파일러는 개발자에게 어떠한 정보도 주지 않는다. 불완전한 코드를 만들어낼 가능성이 매우 높아진다.

public class BenefitImpl implements Benefit {
    @Override
    public void point(Member member) {
        if(member instanceof GoldMember){
            System.out.println("point for Gold Member");
        } else if (member instanceof VipMember) {
            System.out.println("point for Vip Member");
        }else if(member instanceof GreenMember){
            System.out.println("point for Green Member");
        }
    }

    @Override
    public void discount(Member member) {
        if(member instanceof GoldMember){
            System.out.println("discount for Gold Member");
        } else if (member instanceof VipMember) {
            System.out.println("discount for Vip Member");
        }
    }
}
  1. 혜택이 추가되면 될수록 같은 구조를 가지는 구분을 반복적으로 사용하게 되어 코드 중복이 일어난다.

    예를 들면 freeRent라는 혜택을 모든 멤버에게 주기 위해서는 instanceof 를 사용한 분기 구분을 반복해서 사용할 수밖에 없다.

public class BenefitImpl implements Benefit {
    @Override
    public void point(Member member) {
        if(member instanceof GoldMember){
            System.out.println("point for Gold Member");
        } else if (member instanceof VipMember) {
            System.out.println("point for Vip Member");
        }
    }

    @Override
    public void discount(Member member) {
        if(member instanceof GoldMember){
            System.out.println("discount for Gold Member");
        } else if (member instanceof VipMember) {
            System.out.println("discount for Vip Member");
        }
    }

    @Override
    public void freeRent(Member member) {
        if(member instanceof GoldMember){
            System.out.println("freeRent for Gold Member");
        } else if (member instanceof VipMember) {
            System.out.println("freeRent for Vip Member");
        }
    }
}

= > Benefit 인터페이스에 명시적으로 맴버 타입을 받아서 처리하면 되지 않을까?

2-1 Benefit 인터페이스에 멤버별 메소드를 미리 정의하고 이걸 구현하는 방법

  • Benefits 인터페이스
public interface Benefit {
    void point(VipMember member);
    void point(GoldMember member);
    void discount(VipMember member);
    void discount(GoldMember member);
}
  • Benefit 구현체
public class BenefitImpl implements Benefit {
    @Override
    public void point(VipMember member) { System.out.println("point for Vip Member"); }

    @Override
    public void point(GoldMember member) { System.out.println("point for Gold Member"); }

    @Override
    public void discount(VipMember member) { System.out.println("discount for Vip Member"); }

    @Override
    public void discount(GoldMember member) { System.out.println("discount for Gold Member"); }
}
  • main
public class Main {
    public static void main(String[] args) {
        Benefit benefit = new BenefitImpl();
        Member goldMember = new GoldMember();
        Member vipMember = new VipMember();

        benefit.point(goldMember);
        benefit.point(vipMember);
        benefit.discount(goldMember);
        benefit.discount(vipMember);
        benefit.freeRent(vipMember);
        benefit.freeRent(vipMember);
    }
}

문제점

  1. 컴파일 실패

    Benefit 인터페이스의 point와 discount 메소드는 파라미터 타입으로 구분하여 오버로딩을 하고 있다. 그런데 main 메소드에서 호출한 point 메소드는 둘 다 Member 타입 파라미터이다.

    메소드 오버로딩은 컴파일 타임에 정해진 타입으로 결정하여 연결된다(Static dispatch). 따라서 동적으로 변경되는 타입으로 오버로딩으로는 우리가 원하는 결과를 낼 수 없게 된다.

    ✔ 3. Visitor 패턴으로 구현

    혜택별 Benefit 인터페이스 구현

  • Benefit 인터페이스
public interface Benefit {
    void getBenefit(GoldMember member);
    void getBenefit(VipMember member);
}
  • Benefit 구현체
    Discount Benefit
public class DiscountBenefit implements Benefit {
    @Override
    public void getBenefit(GoldMember member) {
        System.out.println("Discount for Gold Member");
    }

    @Override
    public void getBenefit(VipMember member) {
        System.out.println("Discount for Vip Member");
    }
}

Point Benefit

public class PointBenefit implements Benefit {
    @Override
    public void getBenefit(GoldMember member) {
        System.out.println("Point for Gold Member");
    }

    @Override
    public void getBenefit(VipMember member) {
        System.out.println("Point for Vip Member");
    }
}

Member에 혜택을 받을 수 있는 메소드 추가

  • Member 인터페이스
public interface Member {
    void getBenefit(Benefit benefit);
}
  • Member 구현체
    GoldMember
public class GoldMember implements Member {
    @Override
    public void getBenefit(Benefit benefit) {
        benefit.getBenefit(this);
    }
}

VIP MEMBER

public class VipMember implements Member {
    @Override
    public void getBenefit(Benefit benefit) {
        benefit.getBenefit(this);
    }
}
  • Main
public class Main {
    public static void main(String[] args) {
        Member goldMember = new GoldMember();
        Member vipMember = new VipMember();
        Benefit pointBenefit = new PointBenefit();
        Benefit discountBenefit = new DiscountBenefit();

        goldMember.getBenefit(pointBenefit);
        vipMember.getBenefit(pointBenefit);
        goldMember.getBenefit(discountBenefit);
        vipMember.getBenefit(discountBenefit);
    }
}

문제점 해결

1-1. 고객들을 순회하면서 혜택을 주고자 할 때 명시적으로 혜택을 주기 위한 메소드를 호출해야 한다. 즉, iterator를 이용해서 순회하며 처리할 수 없다.

  • Member 인터페이스로 등급별 고객을 순회하며 처리

1-2 혜택이 늘어났을 때 모든 멤버들에 대해서 그 혜택을 구현했다는 보장이 없다

  • 혜택이 늘어나더라도 Benefit 인터페이스에 명시적으로 등급별 메소드를 정의하고 있어 구현 누락이 발생하지 않는다.

  • Free Rent 혜택을 추가해보자. 혜택을 추가하기 위해서는 Free Rent를 위한 Benefit 구상 클래스를 하나 추가하기만 하면 된다.

// FreeRentBenefit
 class FreeRentBenefit implements Benefit {
    @Override
    public void getBenefit(GoldMember member) {
        System.out.println("FreeRent for Gold Member");
    }

    @Override
    public void getBenefit(VipMember member) {
        System.out.println("FreeRent for Vip Member");
    }
}

//Main
public class Main {
    public static void main(String[] args) {
        ...
        Benefit freeRentBenefit = new FreeRentBenefit();
        ...
        goldMember.getBenefit(freeRentBenefit);
        vipMember.getBenefit(freeRentBenefit);
    }
}

2-1 Member 등급이 추가됐을 때 등급별로 혜택을 정확하게 구현했다는 보장이 있는가? 라는 문제

  • Member를 추가하기 위해 GreenMember 구상클래스를 만들고 똑같이 benefit.getBenefit(this); 구현 구문을 넣으려고 하면 컴파일 오류가 발생한다. 왜냐하면 Benefit 인터페이스에는 GreenMember 를 파라미터로 받는 메소드가 존재하지 않기 때문이다. 따라서 등급이 추가되더라도 등급별 혜택 구현을 누락하지 않도록 강제 할 수 있다.
// 인터페이스
public interface Benefit {
    void getBenefit(GoldMember member);
    void getBenefit(VipMember member);
  //  void getBenefit(GreenMember member); // 이부분이 구현이 안되어있으면
}

//GreenMember
public class GreenMember implements Member {
    @Override
    public void getBenefit(Benefit benefit) {
        benefit.getBenefit(this); // 컴파일오류
    }
}

2-2 혜택이 추가되면 될수록 같은 구조를 가지는 구분을 반복적으로 사용하게 되어 코드 중복이 일어난다.

  • 혜택을 추가하기 위해서 한 일은 단지 혜택을 구현하는 클래스를 추가할 뿐이다. 어느 곳에서도 코드 중복을 찾아볼 수 없다.

✅ 언제 쓰나?

  • 적용해야 할 대상 객체가 잘 바뀌지 않고(특히 개수), 적용할 알고리즘이 추가될 가능성이 많은 상황일 때 사용을 고려해봐야 한다.
    ex) Member 등급은 Gold, Vip, Green 으로 고정이거나 추가될 가능성이 작으면서 혜택은 앞으로 계속해서 추가될 가능성이 있을 때를 말한다. 왜냐하면 Member가 추가되면 모든 Benefit 클래스를 수정해야 하기 때문이다.
public interface Benefit {
    void getBenefit(GoldMember member);
    void getBenefit(VipMember member);
    void getBenefit(GreemMember member); // 멤버가 늘어나면 추가해주고 각 구현체에 GreemMember 오버로딩을 해줘야함 
}
  • 대상 객체가 가지는 동작과 객체를 분리해 코드의 응집도를 높이고자 할 때 사용할 수 있다.
    ex) 멤버별 혜택에 대한 로직을 보기 위해서는 Benefit의 구상 클래스만 보면 쉽게 파악할 수 있다.

참여자

이 패턴에 참여자는 Visitor, ConcreteVisitor, Element, ConcreteElement, ObjectStructure 이다.

Visitor

Element를 방문하고 동작을 구현하기 위한 인터페이스이다. (언어마다 인터페이스가 아닐 수 있음) 위 예제에서는 Benefit 인터페이스가 그 역할을 한다.

ConcreteVisitor

실제 알고리즘을 가지고 있는 구현체이다. 위 예제에서는 PointBenefit, DiscountBenefit 이 그 역할을 한다.

Element

구조를 구성하는 인터페이스이자 Visitor 가 방문하여 수행해야 할 대상이다. Visitor 를 실행할 수 있는 메소드를 하나 가지고 있으며 보통 accept 라는 이름으로 정의한다. 위 예제에서는 Member 가 그 역할을 하고 getBenefit 메소드가 accept 역할을 한다.

ConcreteElement

Element의 실 구현체이다. 위 예제에서는 VipMember, GoldMember 가 그 역할을 한다.

ObjectStructure

Element 를 가지고 있는 객체 구조이다. 위 예제에서는 특별히 구조를 사용하지 않았다. 보통 Set, List, CompositeComponent 가 그 역할을 한다.

🧩 Reference

profile
꿈꿈

0개의 댓글