디자인 패턴 - 5. 반복자 패턴, 상태 패턴, 프록시 패턴

크리링·2025년 1월 18일
0

디자인패턴

목록 보기
5/5
post-thumbnail

반복자 패턴

깃허브 코드
컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공




요구사항

두개의 식당이 합병하는데
한쪽은 메뉴를 List로 한쪽은 Array로 관리한다.
하지만 두 식당 모두 메뉴 코드를 건드리는 것을 반대한다.

각 메뉴에 반복자 패턴 인터페이스를 구현해보자.



BEFORE

public class PancakeHouseMenu {
	List<MenuItem> menuItemList;
	
	public PancakeHouseMenu() {
		menuItemList = new ArrayList<>();

		addItem("K&B 팬케이크 세트", "스크램블 에그와 토스트가 곁들여진 팬케이크", true, 2.99);
		addItem("레귤러 팬케이크 세트", "달걀 후라이와 소시지가 곁들여진", false, 2.99);
		addItem("블루베리 팬케이크 세트", "신선한 블루베리와 블루베리 시럽으로 만든 팬케이크", true, 3.49);
		addItem("와플", "취향에 따라 블루베리나 딸기 추가 와플", true, 3.59);
	}
	
	public void addItem(String name, String description,
		boolean vegetarian, double price) {
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItemList.add(menuItem);
	}
	
	public List<MenuItem> getMenuItemList() {
		return menuItemList;
	}
}
public class DinerMenu {
	static final int MAX_ITEMS = 6;
	int numberOfItems = 0;
	MenuItem[] menuItems;
	
	public DinerMenu() {
		menuItems = new MenuItem[MAX_ITEMS];

		addItem("채식주의자용 BLT", "통밀 위에 베이컨, 상추, 토마토 얹은 메뉴", false, 2.99);
		addItem("BLT", "통밀 위에 상추, 토마토 얹은 메뉴", false, 2.99);
		addItem("오늘의 수프", "감자 샐러드를 곁들인 오늘의 스프", false, 3.29);
		addItem("핫도그", "사워크림, 양념, 양파, 치즈가 곁들여진 핫도그", false, 3.05);
	}
	
	public void addItem(String itemName, String description,
		boolean vegan, double price) {
		MenuItem menuItem = new MenuItem(itemName, description, vegan, price);
		if (numberOfItems >= MAX_ITEMS) {
			System.out.println("메뉴가 꽉차서 더 추가할 수 없습니다.");
		} else {
			menuItems[numberOfItems] = menuItem;
			numberOfItems++;
		}
	}

	public MenuItem[] getMenuItems() {
		return menuItems;
	}
}

반복자 디자인 패턴 적용 후

public interface Iterator {
	boolean hasNext();
	MenuItem next();
}
public class DinerMenuIterator implements Iterator {
	MenuItem[] menuItems;
	int position = 0;
	
	public DinerMenuIterator(MenuItem[] menuItems) {
		this.menuItems = menuItems;	
	}

	@Override
	public boolean hasNext() {
		return position < menuItems.length && menuItems[position] != null;
	}

	@Override
	public MenuItem next() {
		MenuItem menuItem = menuItems[position];
		position = position + 1;
		return menuItem;
	}
}
public class DinerMenu {
	static final int MAX_ITEMS = 6;
	int numberOfItems = 0;
	MenuItem[] menuItems;
	
	public DinerMenu() {
		menuItems = new MenuItem[MAX_ITEMS];

		addItem("채식주의자용 BLT", "통밀 위에 베이컨, 상추, 토마토 얹은 메뉴", false, 2.99);
		addItem("BLT", "통밀 위에 상추, 토마토 얹은 메뉴", false, 2.99);
		addItem("오늘의 수프", "감자 샐러드를 곁들인 오늘의 스프", false, 3.29);
		addItem("핫도그", "사워크림, 양념, 양파, 치즈가 곁들여진 핫도그", false, 3.05);
	}
	
	public void addItem(String itemName, String description,
		boolean vegan, double price) {
		MenuItem menuItem = new MenuItem(itemName, description, vegan, price);
		if (numberOfItems >= MAX_ITEMS) {
			System.out.println("메뉴가 꽉차서 더 추가할 수 없습니다.");
		} else {
			menuItems[numberOfItems] = menuItem;
			numberOfItems++;
		}
	}

	// public MenuItem[] getMenuItems() {
	// 	return menuItems;
	// }
	
	public Iterator createIterator() {
		return new DinerMenuIterator(menuItems);
	}
}
public class Waitress {
	PancakeHouseMenu pancakeHouseMenu;
	DinerMenu dinerMenu;

	public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
	}
	
	public void printMenu() {
		Iterator pancakeHouseMenuIterator = pancakeHouseMenu.createIterator();
		Iterator dinerMenuIterator = dinerMenu.createIterator();

		System.out.println("------아침 메뉴------");
		printMenu(pancakeHouseMenuIterator);
		System.out.println("------점심 메뉴------");
		printMenu(dinerMenuIterator);
	}
	
	private void printMenu(Iterator menuIterator) {
		while (menuIterator.hasNext()) {
			MenuItem menuItem = menuIterator.next();
			System.out.println(menuItem.getName());
			System.out.println(menuItem.getDescription());
			System.out.println(menuItem.getVegetarian());
			System.out.println(menuItem.getPrice());
		}
	}
}

테스트

public class Main {
	public static void main(String[] args) {
		PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
		DinerMenu dinerMenu = new DinerMenu();

		Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
		waitress.printMenu();
	}
}

결과




단일 역할 원칙

  • 어떤 클래스가 바뀌는 이유는 하나뿐이어야한다.



상태 패턴

깃허브 코드
객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다.
마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있습니다.




요구사항

예전에 요구사항으로 만들어 둔 동전 뽑기 기계에서
10분의 1 확률로 두개가 뽑힐 수 있게 만들어 달라는 요청

기존의 코드를 보면



BEFORE

public class GumballMachine {
	
	final static int SOLD_OUT = 0;
	final static int NO_QUARTER = 1;
	final static int HAS_QUARTER = 2;
	final static int SOLD = 3;
	
	int state = SOLD_OUT;
	int count = 0;

	public GumballMachine(int count) {
		this.count = count;
		if (count > 0) {
			state = NO_QUARTER;
		}
	}
	
	public void insertQuarter() {
		if (state == HAS_QUARTER) {
			// ...
		} else if (state == SOLD) {
			// ...
		} else if (state == NO_QUARTER) {
			// ...
		} else if (state == SOLD_OUT) {
			// ...
		}
	}

	public void ejectQuarter() {
		if (state == HAS_QUARTER) {
			// ...
		} else if (state == SOLD) {
			// ...
		} else if (state == NO_QUARTER) {
			// ...
		} else if (state == SOLD_OUT) {
			// ...
		}
	}

	public void turnCrank() {
		if (state == HAS_QUARTER) {
			// ...
		} else if (state == SOLD) {
			// ...
		} else if (state == NO_QUARTER) {
			// ...
		} else if (state == SOLD_OUT) {
			// ...
		}
	}

	public void dispense() {
		if (state == HAS_QUARTER) {
			// ...
		} else if (state == SOLD) {
			// ...
		} else if (state == NO_QUARTER) {
			// ...
		} else if (state == SOLD_OUT) {
			// ...
		}
	}
}

각 상황별로 모든 상태에 따른 조건 처리를해서 구현한 이전 코드

상태 패턴으로 리팩토링

public interface State {
	void insertQuarter();
	void ejectQuarter();
	void turnCrank();
	void dispense();
}
public class NoQuarterState implements State {
	GumballMachine gumballMachine;

	public NoQuarterState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	@Override
	public void insertQuarter() {
		System.out.println("동전을 넣으셨습니다.");
		gumballMachine.setState(gumballMachine.getHasQuarterState());
	}

	@Override
	public void ejectQuarter() {
		System.out.println("동전을 넣어주세요.");
	}

	@Override
	public void turnCrank() {
		System.out.println("동전을 넣어주세요.");
	}

	@Override
	public void dispense() {
		System.out.println("동전을 넣어주세요.");
	}
}

GumballMachine 수정

public class GumballMachine {

	State soldOutState;
	State noQuarterState;
	State hasQuarterState;
	State soldState;
	State winnerState;

	State state;
	int count = 0;

	public GumballMachine(int numberGumballs) {
		this.soldOutState = new SoldOutState(this);
		this.noQuarterState = new NoQuarterState(this);
		this.hasQuarterState = new HasQuarterState(this);
		this.soldState = new SoldState(this);

		this.count = numberGumballs;
		if (numberGumballs > 0) {
			state = noQuarterState;
		} else {
			state = soldOutState;
		}
	}

	public void insertQuarter() {
		state.insertQuarter();
	}

	public void ejectQuarter() {
		state.ejectQuarter();
	}

	public void turnCrank() {
		state.turnCrank();
		state.dispense();
	}

	void setState(State state) {
		this.state = state;
	}

	void releaseBall() {
		System.out.println("알맹이를 내보내고 있습니다.");
		if (count > 0) {
			count = count - 1;
		}
	}

	public State getSoldOutState() {
		return soldOutState;
	}

	public void setSoldOutState(State soldOutState) {
		this.soldOutState = soldOutState;
	}

	public State getNoQuarterState() {
		return noQuarterState;
	}

	public void setNoQuarterState(State noQuarterState) {
		this.noQuarterState = noQuarterState;
	}

	public State getHasQuarterState() {
		return hasQuarterState;
	}

	public void setHasQuarterState(State hasQuarterState) {
		this.hasQuarterState = hasQuarterState;
	}

	public State getSoldState() {
		return soldState;
	}

	public void setSoldState(State soldState) {
		this.soldState = soldState;
	}

	public int getCount() {
		return count;
	}
}




프록시 패턴

깃허브 코드
특정 객체로의 접근을 제어하는 대리인을 제공합니다.

프록시 패턴을 사용하면 원격 객체라든가 생성하기 힘든 객체, 보안이 중요한 객체와 같은 다른 객체로의 접근을 제어하는 대리인 객체를 만들 수 있습니다.




요구사항

마을 사람들의 제 착을 찾아주는 데이팅 서비스를 만들려고 합니다.
상대방의 괴짜 지수를 매기는 기능을 더하려고 합니다.
누군가가 타인의 관심 사항을 바꾸고, 본인의 자기 선호도 점수를 조작하는걸 막아주세요



BEFORE

public interface Person {

	String getName();
	String getGender();
	String getInterests();
	int getGeekRating();

	void setName(String name);
	void setGender(String gender);
	void setInterests(String interests);
	void setGeekRating(int geekRating);
}
public class PersonImpl implements Person {

	String name;
	String gender;
	String interests;
	int rating;
	int ratingCount = 0;

	@Override
	public String getName() {
		return name;
	}

	@Override
	public String getGender() {
		return gender;
	}

	@Override
	public String getInterests() {
		return interests;
	}

	@Override
	public int getGeekRating() {
		if (ratingCount == 0) return 0;
		return rating/ratingCount;
	}

	@Override
	public void setName(String name) {
		this.name = name;
	}

	@Override
	public void setGender(String gender) {
		this.gender = gender;
	}

	@Override
	public void setInterests(String interests) {
		this.interests = interests;
	}

	@Override
	public void setGeekRating(int geekRating) {
		this.rating += geekRating;
		ratingCount++;
	}
}

Person 인터페이스용 동적 프록시를 만들어보자

프록시로 해결할 문제를 정리해보면

  1. 자기 괴짜 지수를 직접 조작할 수 없어야 한다.
  2. 다른 사람들의 개인정보를 수정할 수 없어야 한다.

두가지 프록시를 만들어보자
하나는 자신의 객체에 접근하는 프록시, 하나는 다른 객체에 접근하는 프록시

public class OwnerInvocationHandler implements InvocationHandler {
	private Person person;

	public OwnerInvocationHandler(Person person) {
		this.person = person;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		if (method.getName().startsWith("get")) {
			return method.invoke(person, args);
		} else if (method.getName().equals("setGeekRating")) {
			throw new IllegalAccessException("You cannot rate yourself!");
		} else if (method.getName().startsWith("set")) {
			return method.invoke(person, args);
		}
		return null;
	}
}
public class NonOwnerInvocationHandler implements InvocationHandler {
	private Person person;

	public NonOwnerInvocationHandler(Person person) {
		this.person = person;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		if (method.getName().startsWith("get")) {
			return method.invoke(person, args);
		} else if (method.getName().equals("setGeekRating")) {
			return method.invoke(person, args);
		} else if (method.getName().startsWith("set")) {
			throw new IllegalAccessException("You cannot modify someone else's personal information!");
		}
		return null;
	}
}
package proxy;

import java.lang.reflect.Proxy;

public class ProxyFactory {
	public static Person getOwnerProxy(Person person) {
		return (Person) Proxy.newProxyInstance(
			person.getClass().getClassLoader(),
			person.getClass().getInterfaces(),
			new OwnerInvocationHandler(person)
		);
	}

	public static Person getNonOwnerProxy(Person person) {
		return (Person) Proxy.newProxyInstance(
			person.getClass().getClassLoader(),
			person.getClass().getInterfaces(),
			new NonOwnerInvocationHandler(person)
		);
	}
}

테스트

public class Main {
	public static void main(String[] args) {
		PersonImpl joe = new PersonImpl();
		joe.setName("민수");
		joe.setGender("남");
		joe.setInterests("프로그래밍, 맛집 탐방");

		// 본인 프록시 생성
		Person ownerProxy = ProxyFactory.getOwnerProxy(joe);
		System.out.println("이름 (OwnerProxy): " + ownerProxy.getName());

		ownerProxy.setInterests("컴퓨터 게임, 윙슈트");
		System.out.println("수정된 Interests: " + ownerProxy.getInterests());

		try {
			ownerProxy.setGeekRating(5); // 본인은 본인의 괴짜 지수를 설정할 수 없음
		} catch (Exception e) {
			System.out.println("본인 프록시에는 괴짜 지수를 매길 수 없습니다.");
		}

		// 다른 사람이 접근하는 프록시 생성
		System.out.println();
		Person nonOwnerProxy = ProxyFactory.getNonOwnerProxy(joe);
		System.out.println("이름 (NonOwnerProxy): " + nonOwnerProxy.getName());

		try {
			nonOwnerProxy.setInterests("등산"); // 다른 사람의 개인정보 변경 불가
		} catch (Exception e) {
			System.out.println("타인 프록시에 관심 사항을 수정할 수 없습니다.");
		}

		nonOwnerProxy.setGeekRating(10); // 다른 사람이 괴짜 지수 평가 가능
		System.out.println("괴짜 지수: " + joe.getGeekRating());
	}
}

결과



0개의 댓글