02. 전략 패턴

AlmondGood·2022년 7월 13일
0

디자인패턴

목록 보기
3/16
post-thumbnail

이번엔 디자인 패턴의 행위 패턴 중 전략 패턴에 대해서 알아보겠습니다.

전략 패턴(Strategy Pattern)

전략 패턴이란, 행위를 클래스로 캡슐화동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴을 말합니다.
전략 패턴은 객체지향 프로그래밍 편에서 다뤘던 캡슐화, 개방-폐쇄 원칙(OCP)와 긴밀한 관계를 가지고 있습니다.

다시 한번 설명하면, 사람이 걸어가는 데 필요한 다리, 발이 변수가 되고, '걷다'라는 동작이 함수가 되어, 'Person'이라는 클래스를 만드는 것이 캡슐화입니다.
그리고 변수와 함수에 함부로 접근하지 못 하게 하고, 'Person'을 'Baby', 'Child', 'Adult'가 상속받아 확장하는 것이 바로 개방-폐쇄 원칙입니다.


추상 클래스 / 인터페이스

이렇게 상속으로도 OCP를 실현할 수 있지만, 추상클래스와 인터페이스라는 것으로도 실현할 수 있습니다.

추상 클래스(Abstract Class)란, 클래스와 똑같지만, 메소드는 추상 메소드를 가지고 있는 클래스를 말합니다. 추상 메소드는 선언만 해두고 구현해두지 않은 메소드를 말합니다.
추상 클래스는 메소드가 없기 때문에 직접 객체로 생성할 수 없고, 상속해서만 사용이 가능합니다.

abstract class Person {
	abstract void walking();
}

class Baby extends Person {
	@Override
    void walking() {...}
}

위처럼 'abstract' 키워드를 붙여 클래스와 메소드를 만들면 각각 추상 클래스, 추상 메소드가 됩니다. 그리고 구현이 되어 있지 않기 때문에 자식 클래스에서 직접 오버라이딩하여 구현 해줘야 합니다.

인터페이스(Interface)란, 추상 클래스를 가지지만, 클래스와 달리 멤버 변수는 상수만 가질 수 있는 일종의 추상 클래스입니다.

interface Running {
	/*public static final*/ int RUNNING = 0;

    /*public abstract*/ void run();
}

class Baby extends Person implements Running{
	@Override
    void run() {...}
}
  • 'interface' 키워드로 인터페이스임을 명시합니다.
  • 상속받을 클래스에서 implements하여 인터페이스를 상속받았음을 알려주고, 클래스와 인터페이스를 동시에 상속받을 수 있습니다.
  • 멤버 변수를 작성할 때 public static (final)이 생략됩니다.
  • 메소드를 작성할 때는 abstract가 생략됩니다.
  • 추상 클래스처럼 메소드를 오버라이딩 해야 합니다.

추상 메소드를 활용함으로써 수정을 할 필요 없게하고, 확장에 대해선 자유롭게 만들 수 있습니다.



※ 오버로딩? 오버라이딩?

지금까지 오버라이딩에 관한 설명은 많이 했지만, 오버로딩이라는 것도 있습니다.
짧게만 설명하자면,
오버로딩은 기존 함수와 매개 변수의 개수나 타입만 다른 동명의 함수를 만들어 사용하는 것입니다.

class Calculate {
	// int형 계산
	int sum(int a, int b) {
    	return a + b;
    }

	// double형 계산
   double sum(double a, double b){
   		return a + b;
   }
}

오버로딩과 오버라이딩의 큰 차이점은 확장성에 있겠죠.
오버로딩은 기존 함수가 처리하지 못하는 예외 상황을 처리하는 것에 중점을 둔다면,
오버라이딩은 동작하는 방식을 다시 짜고 싶을 때 사용한다고 생각하면 됩니다.



전략 패턴 활용

다시 전략 패턴으로 돌아와서, 예제를 들어 전략 패턴에 대해 설명하겠습니다.

// 걷기 인터페이스(전략 패턴)
interface WalkStrategy {
	void walk();
}
class BabyWalking implements WalkStrategy {
	@Override
	void walk(){
    	System.out.println("아따따따");
    }
}
class AdultWalking implements WalkStrategy {
	@Override
	void walk(){
    	System.out.println("짱아야~");
    }
}

// 뛰기 인터페이스(전략 패턴)
interface RunStrategy {
	void run();
}
class BabyRunning implements RunStrategy{
	@Override
    void run() {
    	System.out.println("아따땨따땨땨땨ㅏ땨");
    }
}
class AdultRunning implements RunStrategy{
	@Override
    public void run() {
    	System.out.println("짱아야 어디가니!");
	}
}

각각 '걷기'와 '뛰기' 동작을 인터페이스로 캡슐화해서, '사람' 추상 클래스에 상속시켜주고, 또 '사람' 클래스를 '아기'와 '어른' 클래스에 상속했습니다.
그 결과, 아기는 아기에 맞게, 어른은 어른에 맞게 동작을 자유롭게 구현할 수 있었습니다.


여기서 주목할 점은 "동작을 인터페이스로 캡슐화했다"는 점입니다.
전략 패턴의 정의에서 "행위를 클래스로 캡슐화한다"고 했죠.
각각 '걷기'와 '뛰기'를 담당하는 인터페이스를 만들어 하위 클래스에서 구현할 수 있도록 했습니다.
그래서 '아기가 걷는' 동작과 '어른이 걷는' 동작을 클래스로 캡슐화 했습니다. '뛰는' 동작도 마찬가지죠.



전체 코드를 보겠습니다.

// 걷기 인터페이스(전략 패턴)
interface WalkStrategy {
	void walk();
}
class BabyWalking implements WalkStrategy {
	@Override
	void walk(){
    	System.out.println("아따따따");
    }
}
class AdultWalking implements WalkStrategy {
	@Override
	void walk(){
    	System.out.println("짱아야~");
    }
}

// 뛰기 인터페이스(전략 패턴)
interface RunStrategy {
	void run();
}
class BabyRunning implements RunStrategy{
	@Override
    void run() {
    	System.out.println("아따땨따땨땨땨ㅏ땨");
    }
}
class AdultRunning implements RunStrategy{
	@Override
    public void run() {
    	System.out.println("짱아야 어디가니!");
	}
}

// 사람 클래스
class Person{
	boolean leg = true;
    boolean foot = true;

    WalkStrategy walkStrategy;
    RunStrategy runStrategy;

    // 동작을 자유롭게 바꿀 수 있음
    void setWalk(WalkStrategy walkStrategy) {
    	this.walkStrategy = walkStrategy;
    }
    void setWalk(RunStrategy runStrategy) {
    	this.runStrategy = runStrategy;
    }


    void walk() {
        walkStrategy.walk();
    };
    void run(){
        runStrategy.run();
    };
}

// 아기 클래스
class Baby extends Person {
	Baby() {}
}

// 어른 클래스
class Adult extends Person {
	Adult() {}
}

// 실행
public static void Main(String[] args) {

	Baby baby = new Baby();
    Adult adult = new Adult();

    baby.setWalk(new AdultWalking);
    adult.setWalk(new babyWalking);

    baby.setRun(new AdultRunning);
    adult.setRun(new babyRunning);

    baby.walk();
    adult.walk();

	baby.run();
    adult.run();
}

실행 부분에 baby에 '어른의 걷기'와 '어른의 뛰기'를 설정시켰고
adult에 '아기의 걷기'와 '아기의 뛰기'를 설정시켰죠.

어른이 기어가고 아기가 뛰어가는 엄청난 상황이 발생할 수 있습니다.

반대로 저 인자만 바꾸면 정상적으로 baby에 '아기의 걷기/뛰기' 와
adult에 '어른의 걷기/뛰기' 를 설정할 수 있다는 말이 됩니다.


한 줄로 정리하면,

아기가 '기어가기' 도 할 수 있고, '걷기' 도 할 수 있다는 뜻입니다.

동작을 마음대로 바꿀 수 있다는 뜻이죠.
이해가 어렵겠지만 코드를 천천히 보면서 따라오시면 이해가 되실 거라 생각합니다.
아니면 코드를 따라 써보셔도 좋습니다.


전략 패턴의 장점과 단점

이렇게 보면 전략 패턴이 정말 좋은 것 같은데, 혹시 장단점은 무엇일까요?

장점

  • if-else를 제거할 수 있다.

    개발하다가 코드가 길어지면 정말 보기가 힘들어집니다. 그 원흉이 바로 if-else죠.
    짧다면 if-else가 좋겠지만 구현해야 할 클래스의 종류가 많아지면 if문으로는 감당하기 힘들겁니다.

  • 확장에 유리하다

    만약, '천천히 걷기'를 추가하고 싶다면 '천천히 걷기'의 클래스만 추가하면 됩니다. 정말 간단하죠.
    그러면서 원본 코드를 수정할 필요가 없으니 동시에 OCP도 지켜집니다.


단점

  • 여기에 사용되는 모든 전략들을 알아야 한다.

    '아기'가 몇 살까지 일까요? 아니면 '어린이'는?
    18개월 아이는 '아기'인가요? '어린이'인가요?
    이런 것들을 명확히 해놓지 않거나, 깜빡한다면 유지보수 하기가 굉장히 어려워 질 것입니다.

  • 전략 패턴이 오히려 비효율적일 수 있다.

    추가해야 할 동작들이 많아지다 보면 필요없는 전략 인터페이스들도 상속받아 정의해주어야 할 수 있습니다.




참고 자료

https://you9010.tistory.com/155

https://velog.io/@oyeon/7-3537-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%9D%98-%EC%84%A0%EC%96%B8-%EC%83%81%EC%86%8D-%EA%B5%AC%ED%98%84

https://joel-dev.site/75

https://gmlwjd9405.github.io/2018/07/06/strategy-pattern.html
https://terms.naver.com/entry.naver?docId=3532973&cid=58528&categoryId=58528&expCategoryId=58528

profile
Zero-Base to Solid-Base

0개의 댓글