[디자인패턴] 비지터 패턴
행위패턴: 객체 간 더 나은 상호작용, 책임 분배 방법 제공하는 패턴
향후 쉽게 확장할 수 있는 유연성에 대한 솔루션을 제공한다.
비지터패턴
방문자는 어떤 장소에 찾아가는 사람을 의미
장소, 방문자 두 개로 구성되어있기에 공간과 방문자로 분리할 수 있습니다.
=> 특정 장소가 방문자가 맞이한 경우, 그 이후에 대한 행동을 방문자에게 위임한다.
즉, 로직과 구조를 분리하는 패턴입니다.
구조
<구현 내용>
멤버들의 등급별로 받는 혜택의 종류가 다릅니다.
브론즈,골드 존재하며 받는 혜택은 아래와 같다고 가정합니다.
★★또한 지속적으로 등급 및 혜택이 추가될 수 있으며, 등급 별로 받는 혜택 종류가 다를 수 있습니다.
[등급 및 혜택표]
1. 브론즈
혜택)
- 포인트1프로 적립
- 1프로 할인
2. 골드
혜택)
- 포인트 2프로 적립
- 2프로 할인
1) 멤버 인터페이스를 정의하고 혜택 관련 메소드를 선언
interface Member{
void discount();
void point();
}
class BronzeMember implements Member{
..
}
class BronzeMember implements Member{
..
}
혜택 관련 메소드가 정의된 Member인터페이스를 등급 별 구현체(GoldMember, BronzeMember를 생성하도록 하였습니다.
구현하고 보니 객체의 역할에 대해 생각을 하게 되었습니다.
멤버 객체에선 기존 메소드(로그인, 로그아웃 등) 및 혜택(포인트적립, 할인 등)이란 1가지 이상의 책임을 지니고 있어 보입니다.
따라서, 멤버와 혜택을 구분하여 구현하는 게 좋다고 생각했습니다.
2) 멤버와 혜택 분리
우선 Member인터페이스에서 혜택 관련 로직을 전부 제거했습니다.
interface Member{
//혜택 관련 코드 모조리 제거
}
혜택 관련 로직을 아래처럼 하나로 묶었습니다.
멤버 등급별 구분하여 혜택을 제공하도록 구현하였습니다.
interface Benefit{
void point(Member member);
void discount(Member member);
}
class BenefitImpl implements Benefit{
@Override
public void point(Member member) {
if(member instanceof GoldMember){
System.out.println("골드멤버 포인트 2프로 적립");
}
if(member instanceof BronzeMember){
System.out.println("브론즈멤버 포인트 1프로 적립");
}
}
@Override
public void discount(Member member) {
if(member instanceof GoldMember){
System.out.println("골드멤버 2프로 할인");
}
if(member instanceof BronzeMember){
System.out.println("브론즈멤버 1프로 할인");
}
}
}
클라이언트 코드 작성
Member goldMember =new GoldMember();
Member bronzeMember =new BronzeMember();
Benefit benefit = new BenefitImpl();
benefit.discount(goldMember);
benefit.point(bronzeMember);
/결과/
골드멤버 2프로 할인
브론즈멤버 포인트 1프로 적립
등급별 원하는 혜택 적용은 되었으나 문제점이 두 가지 존재합니다.
첫번째, 등급이 추가되었을 경우 해당 등급에 대한 구현이 보장되는가 에 관한 문제입니다.
class BenefitImpl implements Benefit{
@Override
public void point(Member member) {
if(member instanceof GoldMember){
...
}
if(member instanceof BronzeMember){
...
}
if(member instanceof SilverMember){
System.out.println("실버멤버 포인트 1프로 적립");
}
}
@Override
public void discount(Member member) {
if(member instanceof GoldMember){
...
}
if(member instanceof BronzeMember){
...
}
//실버등급에 대한 할인로직 구현 까먹었따!!!!!
}
}
예를 들어, 위처럼 실버등급이 추가되었지만 실버등급에 대한 혜택을 포인트적립만 구현하고 할인은 까먹고 구현하지 않았다고 가정해봅니다.
구현하지 않았더라고 컴파일러는 어떠한 에러도 응답하지 않습니다.... 즉, 개발자가 일일이 찾아야 한다는 소리죠...
두번째, if문으로 객체의 타입을 구별해야하는 중복코드가 너무 많이 존재합니다..
if(member instanceof SilverMember){
System.out.println("실버멤버 포인트 1.5프로 적립");
}
if(member instanceof GreenMember){
System.out.println("실버멤버 포인트 1.5프로 적립");
}
if(member instanceof BlueMember){
System.out.println("블루멤버 포인트 1.5프로 적립");
}
위 두가지 문제 때문에 구현을 보장할 수 있는 방식을 아래처럼 생각해보았습니다.
인터페이스 내 멤버별로 혜택관련 메소드를 정의하는 것 => 이러면 구현객체에선 해당 메소드를 반드시 재정의해야한다.
Benefit 인터페이스 내 멤버 구현객체 별 타입의 파라미터를 갖는 메소드 정의
as-is : Memeber인터페이스 타입
to-be : 구현객체의 타입
인터페이스 내 등급별 객체를 반드시 인자로 갖도록 메소드를 만드는 것이다.
interface Benefit{
//point 메소드 오버로딩
void point(GoldMember goldMember);
void point(BronzeMember bronzeMember);
//discount 메소드 오버로딩
void discount(GoldMember goldMember);
void discount(BronzeMember bronzeMember);
}
이러면 아래와 같이 구현이 보장된다.
class BenefitImpl implements Benefit{
@Override
public void point(GoldMember goldMember) {
System.out.println("골드멤버 포인트 2프로 적립");
}
@Override
public void discount(GoldMember goldMember) {
System.out.println("골드멤버 2프로 할인");
}
@Override
public void point(BronzeMember bronzeMember) {
System.out.println("브론즈멤버 포인트 1프로 적립");
}
@Override
public void discount(BronzeMember bronzeMember) {
System.out.println("브론즈멤버 1프로 할인");
}
}
위와 동일한 클라이언트 코드
Member goldMember =new GoldMember();
Member bronzeMember =new BronzeMember();
Benefit benefit = new BenefitImpl();
benefit.discount(goldMember);
benefit.point(bronzeMember);
/결과/
???
실행하려고 보니 컴파일 되지 않는다...
왜????
Benefit인터페이스에서 point,discount 메소드를 오버로딩하고 있고 그렇기에 정적 디스패치를 사용합니다.
=> 정적 디스패치는 컴파일 시점에 매개변수 타입 및 개수로 어떤 메소드를 호출할지 결정되어야 한다.
참고) 동적 디스패치는 런타임 시점에 어떤 메소드를 호출할지 결정된다.(오버라이딩)
void discount(GoldMember goldMember);
void discount(BronzeMember bronzeMember);
위 코드는 매개변수 타입 GoldMember , BronzeMember만 메소드 호출 가능하도록 구현한 것입니다.
위와 동일한 클라이언트 코드
Member goldMember =new GoldMember();
BronzeMember bronzeMember = new BronzeMember();
...
benefit.discount(goldMember);
benefit.discount(bronzeMember);
그러나 여기서 보이는 타입은 GoldMember도 BronzeMember도 아닌 Member인터페이스 타입입니다.
따라서 컴파일 시점에 호출되어야 할 메소드를 찾지 못하여 컴파일 되지 않는 것이다...
( discount(Member member)메소드가 없어요!! 컴파일 에러 발생!! )
그렇다고 아래처럼 인터페이스 구현체의 타입으로 선언한다면 코드는 실행은 됩니다....
GoldMember goldMember =new GoldMember();
BronzeMember bronzeMember = new BronzeMember();
그러나 단점이 존재하게 됩니다.
다형성의 이점도 활용하지 못합니다. 이럴려면 Member 인터페이스를 둔 이유가 없는 셈이죠.
따라서 위 방식은 사용이 불가합니다....
우선 다형성을 위해 멤버 객체 생성 시 인터페이스 타입의 객체 생성은 변함 없습니다.
interface Benefit{
//point 메소드 오버로딩
void point(GoldMember goldMember);
void point(BronzeMember bronzeMember);
//discount 메소드 오버로딩
void discount(GoldMember goldMember);
void discount(BronzeMember bronzeMember);
}
오버로딩으로 인해 컴파일이 안되던 부분을 자세히 보고 생각했습니다.
흠...
메소드들을 호출할 때 동적디스패치 활용되도록 오버라이딩 메소드들을 통해 호출되도록 바꿔보면 될 거같은데..
비지터 패턴 적용
Benefit
기존 BenefitImpl 클래스는 할인,적립 메소드를 모두 지니고 있었는데 이를 할인, 적립 클래스로 분리하였습니다.
interface Benefit{
void doBenefit(BronzeMember member);
void doBenefit(GoldMember member);
}
//포인트 적립
class SavePointBenefitImpl implements Benefit {
@Override
public void doBenefit(BronzeMember member) {
System.out.println("브론즈멤버 포인트 1프로 적립");
}
@Override
public void doBenefit(GoldMember member) {
System.out.println("골드멤버 포인트 2프로 적립");
}
}
//할인
class DiscountBenefitImpl implements Benefit{
@Override
public void doBenefit(BronzeMember member) {
System.out.println("브론즈멤버 1프로 할인");
}
@Override
public void doBenefit(GoldMember member) {
System.out.println("골드멤버 2프로 할인");
}
}
doBenefit메소드를 호출할 때 오버라이딩을 통하여 호출된다면 문제가 없을 것입니다.
Member
멤버 객체에서 오버라이딩을 통해 원하는 혜택을 적용하도록 수정하였습니다.
interface Member{
void getBenefit(Benefit benefit); //오버라이딩
}
class BronzeMember implements Member{
@Override
public void getBenefit(Benefit benefit) {
benefit.doBenefit(this); //메소드를 호출할 때 동적 디스패칭
}
}
class GoldMember implements Member{
public void getBenefit(Benefit benefit) {
benefit.doBenefit(this); //메소드를 호출할 때 동적 디스패칭
}
}
doBenefit메소드를 호출할 때 오버라이딩을 통하여 호출된다면 문제가 없을 것이다.
=> 오버라이딩을 활용해서 호출되기 때문에 문제가 없다!!
클라이언트 코드
Member goldMember =new GoldMember();
Member bronzeMember =new BronzeMember();
Benefit discountBenefit = new DiscountBenefitImpl();
goldMember.getBenefit(discountBenefit);
bronzeMember.getBenefit(discountBenefit);
Benefit savePointBenefit = new SavePointBenefitImpl();
goldMember.getBenefit(savePointBenefit);
bronzeMember.getBenefit(savePointBenefit);
/ 결과 /
골드멤버 2프로 할인
브론즈멤버 1프로 할인
골드멤버 포인트 2프로 적립
브론즈멤버 포인트 1프로 적립
요구사항 추가
1) 만약 혜택이 추가된다면?
Benefit
class newBenefitImpl implements Benefit{
@Override
public void doBenefit(BronzeMember member) {
System.out.println("브론즈 멤버 새로운 혜택 적용");
}
@Override
public void doBenefit(GoldMember member) {
System.out.println("골드 멤버새로운 혜택 적용");
}
}
원하는 혜택 클래스만 추가를 해주면 된다.
클라이언트코드
Benefit newBenefitImpl = new newBenefitImpl();
goldMember.getBenefit(newBenefitImpl);
bronzeMember.getBenefit(newBenefitImpl);
2) 등급이 추가된다면?
Member
class SilverMember implements Member{
@Override
public void getBenefit(Benefit benefit) {
benefit.doBenefit(this);
}
}
Benefit인터페이스
interface Benefit{
void doBenefit(BronzeMember member);
void doBenefit(GoldMember member);
void doBenefit(SilverMember silverMember);
}
Benefit구현체
class DiscountBenefitImpl implements Benefit{
...
@Override
public void doBenefit(SilverMember silverMember) {
System.out.println("실버멤버 1.5프로 할인");
}
}
class SavePointBenefitImpl implements Benefit {
...
@Override
public void doBenefit(SilverMember silverMember) {
System.out.println("실버멤버 포인트 1.5프로 적립");
}
}
혜택이 늘어나는 것보다 추가해야할 코드들이 많은 게 확연히 느껴집니다...
비지터 패턴 활용 시점
대상 객체는 최대한 바뀌지 않으면서 알고리즘은 추가될 가능성이 많은 경우
느낀점
위 내용을 이해하고 적용하기 위해선 정적디스패칭,동적디스패칭의 내용 숙지는 반드시 필요하다.
2023.05.16 - [JAVA] - 오버로딩과 오버라이딩은 왜 어떤 바인딩인가?
참고)
방문자 패턴 - Visitor pattern (thecodinglog.github.io)
[Java] 더블 디스패치(Double Dispatch) (tistory.com)