[Design Pattern]strategy-pattern 학습 과정
https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
[Design Pattern] 스트래티지 패턴이란 - Heee's Development Blog
Step by step goes a long way.
gmlwjd9405.github.io
heejeong kwon님 블로그를 바탕으로 작성했습니다.
저의 생각을 주석 또는 재방식으로 다시 전달하는 방식으로 작성했습니다. 즉 잘못 이해한 내용이 다소 포함 될 수 있습니다.
strategy-pattern에 대해서 전혀 모르신다면 제글을 먼저 보시는 것보다 해당 블로그를 먼저 읽어보시는 것을 추천드립니다.
- 동기
회사에 입사하고 얼마 되지 않아 그래프를 그리는 작업이 있었다.
그래서 회사에서 common에 그래프를 HTML에 그리는 메서드를 뜯어서 공부하게 되었는데, 코드는 1000줄이 넘어가고 90%는 1차원 배열의 값을 이용해서 2차원 그래프를 그리는데 옵션을 사용하지 않고 10개이상의 모든 파라미터를 메서드에 때려박아 한번에 1000줄의 코드에 2차원, 3차원 그래프 1차원 배열, 2차원 배열, 3차원 배열을 모두 아우러 그래프를 그리는 메서드 였다. 가장 큰 문제는 상황마다 그래프를 그리는데 프로젝트마다, 동선마다 매번 메서드의 커스텀 수정 작업또한 필요하다는 것이었는데, 심지어 경력 4개월차 직원 조차 메서드를 매번 고치는데 이해하지도 못하고 금방 끝날 작업에 계속해서 시간을 할애하고 있었다.
그저 사용하는 것은 다른 사용자가 사용한 것을 보고 파라미터만 맞추어 넣어서 마감에 영향은 없었지만, 메서드를 보고 다 뜯어 고치고 싶은 마음에 문제점 정리, 아이디어 정리, 해당 메서드에서 사용한 라이브러리 튜토리얼을 공부하느라 당일 새벽 4시까지 잠도 자지 못하고 다음날 출근하게 되었다.
이 메서드를 내가 만족하는 기준에 고치는데 생각보다 오랜 작업과 학습이 필요할 것 같지만, 이 또한 학습과 자기발전에 도움이 되고 재밌을 것 같아 이 메서드를 고치기 위한 학습 과정 중 하나인 디자인 패턴을 올리게 되었다.
- 시작합니다.
스트래티지 패턴이란:
행위를 클래스로 캡슐화해 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴
같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화되어 있고 이들이 필요할 때 교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결할 수 있게 하는 디자인 패턴
‘행위(Behavioral) 패턴’의 하나 (아래 참고)
즉, 전략을 쉽게 바꿀 수 있도록 해주는 디자인 패턴이다.
전략이란
어떤 목적을 달성하기 위해 일을 수행하는 방식, 비즈니스 규칙, 문제를 해결하는 알고리즘 등
특히 게임 프로그래밍에서 게임 캐릭터가 자신이 처한 상황에 따라 공격이나 행동하는 방식을 바꾸고 싶을 때 스트래티지 패턴은 매우 유용하다.
이분께서는 유명한 '로봇 클래스'를 이용해서 코드와 설명을 진행해 주셨다.
클래스 교체를 통해서 추상화된 공통 기능(공격)을 '아중리 꿀주먹 펀치', '다시 돌아 오지 않는 로켓 펀치' 등 세부 기능으로 나누어 클래스 교체를 통해서 구체적 작업을 다르게하는 것이다.
Strategy
인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시
ConcreteStrategy
스트래티지 패턴에서 명시한 알고리즘을 실제로 구현한 클래스
Context
스트래티지 패턴을 이용하는 역할을 수행한다.
필요에 따라 동적으로 구체적인 전략을 바꿀 수 있도록 setter 메서드(‘집약 관계’)를 제공한다.
참고
행위(Behavioral) 패턴
객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서도 객체 사이의 결합도를 최소화하는 것에 중점을 둔다.
집약 관계
참조값을 인자로 받아 필드를 세팅하는 경우
전체 객체의 라이프타임과 부분 객체의 라이프 타임은 독립적이다.
즉, 전체 객체가 메모리에서 사라진다 해도 부분 객체는 사라지지 않는다.
Strategy에 해당하는 클래스는 꿀주먹 펀치나 로켓 펀치처럼 구체적인 클래스가 아니라 '공격'처럼 추상화되고 포괄적인 클래스(인터페이스)이다.
ConcreteStrategy에 해당하는 클래스는 꿀주먹 펀치나 로켓펀치 처럼 공격의 구체화된 클래스이고 구체적 공격을 메서드로 정의한다.
Context에 해당하는 클래스는 로봇과 같은 Straegy(공격, 꿀주먹, 로켓)를 이용하는 클래스이다. 로봇은 또 구체화 되어 '태권브이', '아톰'과 같이 서로 비슷하지만 다른 기능을 수행할 클래스를 만들기 위해 역할을 서술해야하고 Strategy에 서술된 메서드를 사용하기 위해 내부 변수로 Strategy선언하고 setter로 Strategy가 생성한 객체를 받야와야한다.
예시 로봇 만들기
<기존 해당 블로거가 자바로 작성한 코드>
public abstract class Robot {
private String name;
public Robot(String name) { this.name = name; }
public String getName() { return name; }
// 추상 메서드
public abstract void attack();
public abstract void move();
}
public class TaekwonV extends Robot {
public TaekwonV(String name) { super(name); }
public void attack() { System.out.println("I have Missile."); }
public void move() { System.out.println("I can only walk."); }
}
public class Atom extends Robot {
public Atom(String name) { super(name); }
public void attack() { System.out.println("I have strong punch."); }
public void move() { System.out.println("I can fly."); }
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
public class Client {
public static void main(String[] args) {
Robot taekwonV = new TaekwonV("TaekwonV");
Robot atom = new Atom("Atom");
System.out.println("My name is " + taekwonV.getName());
taekwonV.move();
taekwonV.attack();
System.out.println()
System.out.println("My name is " + atom.getName());
atom.move();
atom.attack();
}
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
<이를 js로 바꾸어 작성한 코드>
class Robot{
name; robot;
constructor(name) {
this.name = name;
//메서드 체닝을 위해서
return this;
}
attack(){
console.log(`${this.name}, 공격 시작`);
}
move(){
console.log(`${this.name}, 움직여!!`)
}
setRobot(robot){
this.robot = robot;
//메서드 체이닝을 위해서
return robot;
}
}
class TaekwonV extends Robot{
//js에서는 화살표 함수로 super기능으로 상위 클래스의 메서드에 접근할 수 없다..
attack(){
super.attack();
console.log(`${this.name}, 펀치공격`);
}
move(){
super.move();
console.log(`${this.name}, 걸어 다니기!!`);
}
}
class Atom extends Robot{
attack(){
super.attack();
console.log(`${this.name}, 로켓 공격!!`);
}
move(){
super.move();
console.log(`${this.name}, 날아 다니기!!`);
}
}
//client 가 사용하게 될 스크립트
//new Robot() 로봇 클래스 리턴 new Robot()은 로봇클래스이므로 setRobot사용가능.
//setRobot은 구체적 taekwonV객체 리턴 attack(), move() 메서드 사용가능.
let taekwonV = new Robot().setRobot(new TaekwonV('고물'));
taekwonV.attack();
taekwonV.move();
console.log('---------------------------------------')
let atom = new Robot().setRobot(new Atom('깡통'));
atom.attack();
atom.move();
Context에 해당하는 로봇클래스에는 로봇이 추상화 되어 어떤 동작을 구현하게 될 지 서술 되어 있다.
그리고 태권브이와 아톰 클래스는 각자 로봇 클래스를 상속받아 구체적인 동작을 각자 정의하고 있다.
하지만, 태권브이가 냥냥펀치를, 걸어 다니는 것이 아니라 뛰어다니는 동작을 구현하고 싶다면?
아톰이 strong 펀치가 아니라 솜주먹 펀치를, 날아 다니는 것이 아니라 순간이동하는 동작을 하고 싶다면?
각 태권브이와 아톰 클래스를 들어가서 해당 메서드를 '지우고' 다시 작성해서 '수정' 해야한다.
그러므로 이 클래스의 메서드를 사용한 다른 클라이언트 클래스의 내용 또한 내용이 변하게 되어 의도한 대로 동작하지 않게 된다.
즉 과거에 작성한 태권 브이는 '미사일 공격'하고 '걸어' 다녀야 하는데, 현재 수정한 내용 때문에 '냥냥펀치'하고 '뛰어'다니게 되고,
과거에 작성한 아톰은 '강펀치하고' '날아' 다녀야 하는데, '솜주먹 펀치'하고 '순간이동'하게 된다.
새로운 기능을 위해 기존 코드의 내용을 수정해야 하므로 OCP에 위배된다.
OCP(Open-Closed Principle, 개방-폐쇄 원칙): 기존 코드의 내용을 수정해야 하므로 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다
또한 선가드 라는 새로 클래스를 추가할때
public class Sungard extends Robot {
public Sungard(String name) { super(name); }
public void attack() { System.out.println("I have Missile."); } // 중복
public void move() { System.out.println("I can only walk."); }
}
선가드라는 클래스는 이미 태권브이에 작성한 미사일공격, 걷기라는 내용을 다시 작성해야한다.
코드를 변화하면서 문제가 되는 요인은 이동방식과 공격 방식이 변화하면서 생기는 문제이다.
그래서 이러한 문제를 해결하기 위해 상위의 공격과, 이동(Strategy)이라는 추상화 하여 구체적 행위를 하는 클래스들(ConcreteStrategy)을 포괄할수 있는 클래스를 만들고 만들어 준다.
그리고 '미사일 공격', '냥냥 펀치', '강펀치' 등등 구체화된 클래스들은 개념이 추상화된 클래스를 상속받아 작성한다.
마지막으로 Context에 해당하는 로봇 클래스에는 이들 공격을 사용할 수 있도록 Strategy에 해당하는 공격, 이동등의 클래스를 내부에 변수로 선언해주고, 이 변수에 값을 할당하기 위해 setter도 만들어준다.
직접 property에 접근하여 할당하지 않는 이유는 보안상의 이유이다.
<해당 블로거의 최종코드>
public abstract class Robot {
private String name;
private AttackStrategy attackStrategy;
private MovingStrategy movingStrategy;
public Robot(String name) { this.name = name; }
public String getName() { return name; }
public void attack() { attackStrategy.attack(); }
public void move() { movingStrategy.move(); }
// 집약 관계, 전체 객체가 메모리에서 사라진다 해도 부분 객체는 사라지지 않는다.
// setter 메서드
public void setAttackStrategy(AttackStrategy attackStrategy) {
this.attackStrategy = attackStrategy; }
public void setMovingStrategy(MovingStrategy movingStrategy) {
this.movingStrategy = movingStrategy; }
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
public class TaekwonV extends Robot {
public TaekwonV(String name) { super(name); }
}
public class Atom extends Robot {
public Atom(String name) { super(name); }
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
// 인터페이스
interface AttackStrategy { public void attack(); }
// 구체적인 클래스
public class MissileStrategy implements AttackStrategy {
public void attack() { System.out.println("I have Missile."); }
}
public class PunchStrategy implements AttackStrategy {
public void attack() { System.out.println("I have strong punch."); }
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
// 인터페이스
interface MovingStrategy { public void move(); }
// 구체적인 클래스
public class FlyingStrategy implements MovingStrategy {
public void move() { System.out.println("I can fly."); }
}
public class WalkingStrategy implements MovingStrategy {
public void move() { System.out.println("I can only walk."); }
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
public class Client {
public static void main(String[] args) {
Robot taekwonV = new TaekwonV("TaekwonV");
Robot atom = new Atom("Atom");
/* 수정된 부분: 전략 변경 방법 */
taekwonV.setMovingStrategy(new WalkingStrategy());
taekwonV.setAttackStrategy(new MissileStrategy());
atom.setMovingStrategy(new FlyingStrategy());
atom.setAttackStrategy(new PunchStrategy());
/* 아래부터는 동일 */
System.out.println("My name is " + taekwonV.getName());
taekwonV.move();
taekwonV.attack();
System.out.println()
System.out.println("My name is " + atom.getName());
atom.move();
atom.attack();
}
}
//https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
<JS로 바꾸어 본 나의 코드>
class Robot{
name; robot; attackStrategy; movingStrategy;
constructor(name) {
this.name = name;
return this;
}
attack() {
console.log(`${this.name}: `)
this.attackStrategy.attack();
}
move(){
console.log(`${this.name}: `)
this.movingStrategy.move();
}
setRobot(robot){
this.robot = robot;
return robot;
}
setAttackStrategy(attackStrategy){
this.attackStrategy = attackStrategy;
return this;
}
setMovingStrategy(movingStrategy){
this.movingStrategy = movingStrategy;
return this;
}
}
class TaekwonV extends Robot{
constructor(name) {
super(name);
}
}
class Atom extends Robot{
constructor(name) {
super(name);
}
}
class AttackStrategy{
attack(){};
}
class MissileStrategy extends AttackStrategy{
attack(){
super.attack();
console.log('미사일 공격!!')
}
}
class PunchStrategy extends AttackStrategy{
attack() {
super.attack();
console.log('강펀치 공격!!');
}
}
class MovingStrategy {
move(){};
}
class FlyingStrategy extends MovingStrategy{
move(){
super.move();
console.log('날아다니기!!!');
}
}
class WalkingStrategy extends MovingStrategy{
move() {
super.move();
console.log('걸어 다니기!!!')
}
}
/*
----------------기존을 수정하지 않고 새로 추가함--------------------
*/
class TelePortStrategy extends MovingStrategy{
move() {
super.move();
console.log(`걸어다니기!!`);
}
}
class RunStrategy extends MovingStrategy{
move() {
super.move();
console.log(`뛰어다니기!!`);
}
}
class CatPunchStrategy extends AttackStrategy{
attack() {
super.attack();
console.log(`냥냥펀치 공격!!`);
}
}
class CottonPunchStrategy extends AttackStrategy{
attack() {
super.attack();
console.log(`솜주먹 공격!!`);
}
}
/*
----------------------client script--------------------------------
*/
let atom = new Robot().setRobot(new Atom("Atom"))
.setAttackStrategy(new MissileStrategy())
.setMovingStrategy(new WalkingStrategy());
let taekwonV = new Robot()
.setRobot(new TaekwonV("TaekwonV"))
.setMovingStrategy(new FlyingStrategy())
.setAttackStrategy(new PunchStrategy());
atom.attack();
atom.move();
taekwonV.move();
taekwonV.attack();
/*
---------------------------값 바꾸어 출력하기---------------------------------
*/
atom.setAttackStrategy(new CatPunchStrategy()).setMovingStrategy(new TelePortStrategy());
taekwonV.setAttackStrategy(new CottonPunchStrategy()).setMovingStrategy(new RunStrategy());
atom.attack();
atom.move();
taekwonV.move();
taekwonV.attack();
console결과

소감
원본 출처의 글과 나의 생각을 동시에 작성하느라 장황한 포스트였다..
객체지향 기법이라 그런 지 너무나 많은 클래스를 생성해야하는데, 이런 기법으로 아름답게 작성한다 하더라도 '회사에서 추가적으로 유지보수를 할 수 있을까?' 또는 '사용할 수 있을까?' 라는 의문이 든다.
캡슐화 기법은 객체값에 직접 접근 방지라는 보안을 위해서라는 이유도 존재하고 JS는 대부분 프론트를 위해서 쓰이고 코드를 볼 수 있는데, JS에서 의미가 있는 지 아직은 모르겠다.