Java/Design Pattern

전략 패턴

haechan29 2023. 1. 15. 14:41

전략 패턴이란?

알고리즘을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 하는 패턴입니다.

 

 

뒤로가기 누르지 마세요!!

무슨 소린지 전혀 감이 안 오는게 당연해요! 천천히 알아봅시다.

 

우리가 침착맨 꾸미기 게임을 만든다고 생각해볼까요?

 

<상황>

침펄 꾸미기 게임을 만든다.

옷을 입고(dress), 머리 스타일을 바꾸는(changeHairstyle) 기능을 구현하려고 한다.

 

 

위의 기능을 어떻게 구현할 수 있을까요?

 

침착맨과 주펄 클래스를 따로 만들 수도 있겠지만,

추후에 다른 사람들도 게임에 추가될 것을 고려하여

사람 클래스를 만든 후, 그 서브 클래스로 침착맨과 주펄 클래스를 만들어 보려고 해요.

 

<예상>

[Person.java]

public abstract class Person {

	String name;
	String hairstyle = "검은색 머리";
	String clothes = "흰티에 청바지";

	public void dress(String clothes) {
		this.clothes = clothes;
		updated();
 	}

	public void changeHairstyle(String hairstyle) {
		this.hairstyle = hairstyle;
		updated();
	}
    
	public void updated() { // 꾸미는 메소드가 호출될 때마다 실행된다.
		System.out.printf("%s(은)는 %s에 %s(을)를 입고 있다.\n", name, hairstyle, clothes);
	}
}

 

[CalmDownMan.java]

public class CalmDownMan extends Person {
	public CalmDownMan() {
		name = "침착맨";
	}
}

 

[JooPearl.java]

public class JooPearl extends Person {
	public JooPearl() {
		name = "주펄";
		hairstyle = "민머리";
	}
}

 

기본적인 머리 스타일은 "검은색 머리"이지만, 주펄은 "민머리"로 설정했어요.

 

그러면 침착맨에게 "지능이 떨어져 보이는 코디를 입혀볼까요?

 

Person p = new CalmDownMan();
p.dress("지능이 떨어져보이는 코디");

>> 침착맨(은)는 검은색 머리에 지능이 떨어져보이는 코디(을)를 입고 있다.

 

 

침착맨이 성공적으로 옷을 갈아입었어요!

 

머리 스타일도 씹숭이 머리로 바꿔봐요!

 

Person p = new CalmDownMan();
p.changeHairstyle("씹숭이 머리");

>> 침착맨(은)는 씹숭이 머리에 흰티에 청바지(을)를 입고 있다.

 

 

머리 스타일도 성공적으로 바꿨어요!

 

마지막으로 주펄도 똑같은 기능을 실행해봐야겠죠?

이번엔 주펄의 머리스타일을 "검은색 머리로" 바꿔봐요.

 

Person p = new JooPearl();
p.changeHairstyle("검은색 머리");

>> 주펄(은)는 검은색 머리에 흰티에 청바지(을)를 입고 있다.

 

앗! 예상치 못한 결과가 나왔어요!

아무래도 머리 스타일이 민머리인 사람은 changeHairstyle() 메소드를 사용할 수 없게 해야겠어요...

그렇지만 changeHairstyle() 메소드는 Person으로부터 상속받은 메소드인데 이를 어떡하죠?

 

<해결 방법1>

JooPearl 클래스의 changeHairstyle() 메소드를 아무 기능도 하지 않도록 오버라이드한다.

 

public class JooPearl extends Person {
	public JooPearl() {
		name = "주펄";
		hairStyle = "민머리";
	}

	@Override
	public void changeHairstyle(String hairstyle) {}
}

 

이렇게 해두면 확실히 주펄 클래스는 문제를 일으키지 않겠어요.

 

 

그치만 민머리인 사람이 추가될 때마다 changeHairstyle() 메소드를 바꿔야 할까요?

클래스가 많아지면 바꾸는 걸 까먹을 가능성도 있고,

무엇보다도 코드가 중복해서 작성되고 있어요.

 

이럴때 쓸 수 있는게 전략 패턴이에요

 

<해결 방법2 : 전략 패턴 사용하기>

 

우리는 똑같은 changeHairstyle() 메소드라도 클래스에 따라 다른 내용을 구현하길 원해요.

즉, 하위 클래스가 수퍼 클래스와 같은 멤버를 가지지만 그 값이 다른 것처럼

Person 클래스의 하위 클래스가 같은 changeHairstyle() 메소드를 가져도 그 내용이 다르길 바라는 거죠.

 

이럴 때 어떻게 하냐면... 정답부터 알려드릴게요.

 

changeHairstyle() 메소드를 가지는 인터페이스를 하나 만들고,

그 인터페이스를 구현하는 구상 클래스가 서로 다른 방식으로 changeHairstyle() 메소드를 구현하면 돼요!

 

어렵죠? 당연히 당연히 어렵습니다... 제발 포기하지 마세요!!

바로 위에 밑줄친 내용 있죠? 그걸 조금만 더 구체적으로 얘기해봐요.

 

우선 changeHairstyle() 메소드를 선언하는 인터페이스를 만든다고 했죠?

이 인터페이스는 머리 스타일을 바꾸는 기능만 하니까 HairSalon(헤어샵)이라고 이름 지어볼게요.

 

[HairSalon.java]

public interface HairSalon {
	void changeHairstyle(String hairstyle);
}

 

그리고 뭘 하기로 했죠?

이 HairSalon 인터페이스를 구현하는 서로 다른 구상 클래스를 만든다고 했죠?

침착맨이 갈 HairSalon 구상 클래스와 주펄이 갈 HairSalon 구상 클래스를 만들어야 하는데요,

각각 AnsanHairSalon, NoHairSalon이라고 이름 지어볼게요!

 

그러면 각각의 HairSalon 구상 클래스에서는 뭘 해야 하죠?

당연히 changeHairstyle() 메소드를 구현해야죠~

 

AnsanHairSalon 클래스에서는 changeHairstyle()이 그냥 Person 객체의 hairstyle을 바꿔주면 돼요.

 

[AnsanHairSalon.java] (변경 전)

public class AnsanHairSalon implements HairSalon {
	@Override
	public void changeHairstyle(String hairstyle) {
		customer.hairstyle = hairstyle;
	}
}

 

만들고 보니까 Person 타입의 customer 멤버를 어디선가 입력받아야겠네요.

생성자에서 입력을 받도록 해볼까요?

 

[AnsanHairSalon.java] (변경 후)

public class AnsanHairSalon implements HairSalon {
	private Person customer;

	public AnsanHairSalon(Person customer) {
		this.customer = customer;
	}
    
	@Override
	public void changeHairstyle(String hairstyle) {
		customer.hairstyle = hairstyle;
	}
}

 

마찬가지로 NoHairSalon 클래스에서도 changeHairstyle() 메소드를 구현하면 되겠어요.

어떤 기능을 구현하냐구요? 우리가 처음에 했던 것처럼 아무 것도 하지 않을 거에요. (ㅠㅠ)

구현해봅시다!

 

[NoHairSalon.java]

public class NoHairSalon implements HairSalon {
	private Person customer;

	public NoHairSalon(Person customer) {
		this.customer = customer;
	}
    
	@Override
	public void changeHairstyle(String hairstyle) {
		System.out.println("무에서 유를 창조할 수 없습니다.");
	}
}

 

아무 것도 하지 않는다는 걸 강조하기 위해 "무에서 유를 창조할 수 없습니다."라는 문구를 출력하게 했어요.

 

반 정도는 완료했습니다! (나머지 반이 더 쉬워요 😄)

 

이제 서로 다른 방식으로 changeHairstyle()을 구현하는 구상 클래스를 만들었으니까요,

클래스마다 서로 다르게 changeHairstyle()을 구현하는 대신에

클래스마다 서로 다른 HairSalon 타입의 멤버만 가지면 돼요.

(침착맨 클래스는 AnsanHairSalon을, 주펄 클래스는 NoHairSalon을 가지기로 했죠?)

 

그러면 Person 클래스에서 hairstyle을 바꾸는 방식을 살펴보죠.

우리가 아까 Person 클래스를 구현한 방식은 이랬어요.

 

[Person.java] // hairstyle과 관련된 부분만 남겨서 표시했습니다.

public abstract class Person {
	String hairstyle = "검은색 머리";

	public void changeHairstyle(String hairstyle) {
		this.hairstyle = hairstyle;
	}
}

 

changeHairstyle() 메소드 내부에서 머리 스타일을 바꾸는 기능을 직접 구현했죠?

하지만 이제는 HairSalon 인터페이스의 changeHairstyle() 메소드를 호출하기만 하면 돼요.

그러기 위해서는 Person 클래스가 HairSalon 타입의 멤버를 가져야 겠네요.

그러니까 이런 식이 되겠죠?

 

[Person.java] // hairstyle과 관련된 부분만 남겨서 표시했습니다.

public abstract class Person {
	String hairstyle = "검은색 머리";
	HairSalon hairSalon;

	public void changeHairstyle(String hairstyle) {
		hairSalon.changeHairstyle(hairstyle);
	}
}

 

이제 거의 다 됐습니다.

침착맨 클래스와 주펄 클래스를 만들 때, 생성자 내부에서 hairSalon 변수를 본인에게 맞게 초기화해주면 돼요.

바꿔보죠!!

 

[CalmDownMan.java]

public class CalmDownMan extends Person {
	public CalmDownMan() {
		name = "침착맨";
		hairSalon = new AnsanHairSalon(this);
	}
}

 

[JooPearl.java]

public class JooPearl extends Person {
    public JooPearl() {
        name = "주펄";
        hairstyle = "민머리";
        hairSalon = new NoHairSalon(this);
    }
}

 

이로써 전략 패턴이 완성됐습니다~~~~

따라오시느라 고생 많으셨어요! 😂

 

한 번 제대로 작동하는지 확인해볼까요?

 

Person p1 = new CalmDownMan();
p1.changeHairstyle("씹숭이 머리");

Person p2 = new JooPearl();
p2.changeHairstyle("검은 머리");

>> 침착맨(은)는 씹숭이 머리에 흰티에 청바지(을)를 입고 있다.
>> 무에서 유를 창조할 수 없습니다.
>> 주펄(은)는 민머리에 흰티에 청바지(을)를 입고 있다.

 

모두 제대로 작동하네요!

 

<정리>

전략 패턴이 뭐라고 했었죠?

알고리즘을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 하는 패턴이라고 했습니다.

 

처음으로 돌아가보죠. 우리는 클래스마다 changeHairstyle() 메서드를 다르게 구현해야 했어요.

그런데 Java에서는 함수가 하나의 자료형이 아니기 때문에 이를 갈아끼울 수가 없었죠.

 

그래서 Person 클래스에서 changeHairstyle()의 기본형을 구현한 후에,

필요에 따라 각각의 하위 클래스에서 changeHairstyle() 메서드를 오버라이드했어야 했습니다.

 

그러나 이 경우에는 코드를 재사용할 수가 없어 불편했죠?

그래서 HairSalon 인터페이스를 만들어 changeHairstyle() 메서드를 선언하고,

서로 다른 방식으로 changeHairstyle() 메서드를 구현하는 구상 클래스 AnsanHairSalon, NoHairSalon을 만들었습니다.

 

그리고 Person 클래스의 changeHairstyle() 메서드에서 직접 hairstyle을 변경하지 않고,

단순히 hairsalon 멤버의 changeHairstyle() 메서드를 호출하는 방식으로 바꿨죠.

 

전략 패턴의 정의에서 알고리즘을 캡슐화한다는 말이 바로 이 말이에요.

메소드를 변경하려고 하위 클래스에서 일일이 오버라이드하는 대신,

메소드를 구현하는 인터페이스를 만들고, 하위클래스가 해당 인터페이스의 구상 클래스를 변수로 가졌죠.

그 덕에 메소드를 수정하거나, 갈아끼우기도 쉬워졌구요!

 

이제 이해가 되시나요?

 

안되시죠?

 

그럼 여러 번 다시 읽어보세요! 😄

저도 이해가 안되서 설명글을 써보는 방식으로 공부하는 거니까요, 같이 힘내보자구요!

 

 

(여기서부터는 꼭 읽어보셔야 하는 건 아니에요 😂)

그리고 추가적으로, 전략 패턴을 꼭 써야하는 건지 궁금하실 수도 있어서 그 의의를 알려드릴게요.

전략 패턴은 <상속보다는 구상을 활용한다>는 디자인 원칙을 따라요.

또 모르는 말이 나왔죠? 지금부터 설명 들어갑니다잉!

 

"객체 지향은 현실을 모델링한다"는 말 들어보셨나요?

실제로 머리 스타일을 바꿀 때를 생각해봐요.

저희가 헤어드레서가 아닌 이상, 실제로 머리 스타일을 어떻게 바꾸는 지는 모르죠.

 

 

물론 기안84는 예외에요...

 

머리 스타일을 바꿔야겠다는 생각이 들면 그냥 헤어샵에 가죠?

객체 지향도 똑같아요!

우리가 앞에서 어떻게 했었나요?

Person 클래스의 changeHairstyle() 메서드에서 직접 hairstyle을 변경하지 않고,

단순히 hairsalon 멤버의 changeHairstyle() 메서드를 호출했죠.

 

즉 Person 클래스는 내 머리 스타일을 어떻게 바꾸는 지 모르는 거에요.

 

사실 알 필요도 없고, 모르는 게 더 낫죠?

 

우리가 머리 스타일을 직접 바꾸는 게 굉~장히 귀찮은 일인 것처럼,

클래스에서 메서드의 기능을 직접 변경하는 것도 굉~장히 귀찮은 일이에요.

 

왜냐?

만약에 메서드의 기능이 조금 바뀌었다고 생각해봐요.

그러면 하위 클래스를 일일이 살펴 보면서 메서드를 하나하나 바꿔줘야 하잖아요. 😱

 

그렇지만 하나의 기능을 하나의 인터페이스가 전담하고(hairstyle 바꾸기 - HairSalon)

클래스는 해당 인터페이스 타입의 멤버만 가진다면?

바꿔야하는 부분은 인터페이스의 구상 클래스 단 한 곳이기 때문에 훨씬 편하죠!

 

전략 패턴에 대한 소개는 여기까지에요.

궁금증이 잘 해결되셨길 바래요!

 

틀린 내용은 댓글로 남겨주시면 확인하는 대로 반영하겠습니다~

읽어주셔서 감사합니다!! 꾸우벅