CS_Step3 전략 패턴(Strategy Pattern)

장선웅·2022년 7월 13일
0

전략 패턴?

  1. 객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의한다.
  2. 객체의 행위를 동적으로 바꾸고 싶을 때, 직접 행위를 수정하지 않고 전략 클래스를 바꿈으로써 행위를 유연하게 확장한다.

간단히 정리하자면, 객체가 할 수 있는 행위들 각각을 전략으로 만들고(캡슐화), 동적으로 행위의 수정이 필요한 경우 전략을 바꿔 객체의 행위 수정이 가능하도록 하는 패턴.


1. 전략 패턴은 왜 사용할까?

예를 들어, 차(Car)와 비행기(Ariplane)이라는 클래스가 있고, 이 두 클래스는 Moving(이동)이라는 인터페이스를 구현했다고 가정해보자. 그리고 이 두 객체를 사용하는 이용자(User)가 있다.

//Moving 인터페이스 구형
public interface Moving {
	public void move(); 
}
//Car 클래스 구현 (feat.Moving)
public class Car implements Moving{
	public void move() {
    	System.out.println("차도를 달린다.");
    }
}
//Airplane 클래스 구현 (feat.Moving)
pulblic class Airplane implements Moving{
	public void move() {
    	System.out.println("하늘을 달린다.");
    }
}
//Car와 Airplane을 이용하는 User 클래스
public class User {
	public static void main(String[] args) { 
        //User가 Car 객체 사용
    	Moving car = new Car();
        //User가 Airplane 객체 사용
        Moving airplane = new Airplane();
        // Car를 움직이면 "차도를 달린다."
        car.move(); 
        //Airplane을 움직이면 "하늘을 달린다."
        airplane.move();
    }
}

현재 Car는 차도를 달리고 있고 Airplane는 하늘을 달린다. 하지만 시대가 바뀌어 하늘을 달릴 수 있는 Car가 개발이 되었다고 가정해보자. 그렇다면 Car 클래스에 있는 move()메서드를 다음과 같이 바꾸면 된다.

public class Car implements Moving{
	public void move() {
    	System.out.println("하늘을 달린다.");
    }
}

하지만 이렇게 한다면 SOLID의 원칙(객체지향 프로그래밍의 5대 원칙)중의 O(CP) 즉, 개방-폐쇄의 원칙에 위배된다. 또한, 차와 비행기 뿐만이 아니라 오토바이, 버스, 트럭같은 객체가 더 생기게 된다고 생각해보자.(소프트웨어 요소의 확장) 시간이 지나며 이들도 하늘을 달리게 된다면 오토바이, 버스, 트럭의 move()메서드를 일일이 수정해야 한다. 뿐만 아니라, 같은 메서드를 여러 클래스에서 똑같이 정의하고 있기 때문에 메서드의 중복이 발생하게 된다. 이를 해결하고자 사용되는 것이 전략 패턴이다.

SOILD의 OCP(Open-Close-Principle)?

SOLID의 원칙는 객체지향 프로그래밍의 5대 원칙이다. 그 중에 O는 개방-폐쇄의 원칙이다. 개방-폐쇄의 원칙이란, "소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다" 라는 원칙이다. 다시 말하자면 오토바이, 버스, 트럭같은 객체가 추가될 수 있지만(소프트웨어의 확장), 그 자체의 move메서드의 수정(소프트웨어의 변경)이 일어나서는 안된다는 것이다.

2. 전략 패턴 구현 방법

1) 전략 생성

현재 차와 비행기는 차도를 달리던, 하늘을 달리는 두 가지의 방법이 있다. 즉, 움직이는 두 방식에 대해 전략(Strategy) 클래스를 생성한다.(RoadStrategy/SkyloadStrategy) 그리고 두 전략 클래스를 캡슐화 하기 위한 MovingStrategy 인터페이스를 생성한다. 이렇게 캡슐화를 하는 이유는 추가적 확장이 되는 경우(주유방식에 대한 전략 등)를 고려한 것이다.

//두 전략 클래스를 캡슐화 하기 위한 인터페이스
public interface MovingStrategy{
	public void move();
}
//전략 클래스 생성/RoadStrategy
public class RoadStrategy implements MovingStrategy {
	public void move() {
    	System.out.println("차도를 달린다.");
    }
}
//전략 클래스 생성/SkyloadStrategy
public class SkyloadStrategy implements MovingStrategy {
	public void move() {
    	System.out.println("하늘을 달린다.");
    }
}

2) 객체에 대한 클래스 정의

차나 비행기같은 객체들은 move() 메서드를 통해 움직인다. 하지만 move() 메서드를 직접 구현하지 않고, 어디를 달릴지에 대한 전략을 설정하여, 그 전략의 움직임 방식을 사용하여 움직이도록 한다. 그러므로 객체마다 전략을 설정하는 메서드인 setMovingStrategy()를 생성한다.

//Car와 Airplane이 상속받을 상위 클래스 생성
public class Vehicle {

	//전략 클래스 두 개를 캡슐화하기 위한 인터페이스 가져오기
	private MovingStrategy movingStrategy;
    
    //부모 클래스에 move()메서드 생성
    public void move() {
    	movingStrategy.move();
    }
    
    //객체마다 전략을 설정 할 메서드 생성
    public void setMovingStrategy(MovingStrategy movingStrategy) {
    	this.movingStrategy = movingStrategy;
    }
}
//Vehicle을 상속받은 하위 클래스 Car 생성
public class Car extends Vehicle{

}
//Vehicle을 상속받은 하위 클래스 Airplane 생성
public class Airplane extends Vehicle{

}

즉, Car 클래스와 Airplane 클래스는 상위 클래스인 Vehicle을 상속받았기 때문에 이 두 클래스는 move()메서드와 setMovingStrategy()메서드를 사용할 수 있게 된다.

3) 두 객체를 사용하는 User클래스 구현

Car와 Airplane 객체를 생성한 후에, 각 객체가 어디를 달리는지 설정하기 위해 setMovingStrategy()메서드를 호출한다.

public class User {
	public void static main(String args[]) {
    	//인스턴스 변수 생성
    	Vehicle car = new Car();
        Vehicle airplane = new Airplane();
        
        //setMovingStrategy 메서드를 통해 달리는 곳 설정
        car.setMovingStrategy(new LoadStrategy());
        airplane.setMovingStrategy(new SkyloadStrategy());
        
        //LoadStrategy 클래스에 있는 move()메서드 호출
        car.move();
        //SkyloadStrategy 클래스에 있는 move()메서드 호출
        airplane.move();
    }
}

그렇다면 이 상황해서 추가로 하늘을 달리는 차가 발명되었다고 해보자.

public class User {
	public void static main(String args[]) {
    	//인스턴스 변수 생성
    	Vehicle car = new Car();
        Vehicle airplane = new Airplane();
        //setMovingStrategy 메서드를 통해 달리는 곳 설정
        car.setMovingStrategy(new LoadStrategy());
        airplane.setMovingStrategy(new SkyloadStrategy());
        
        //LoadStrategy 클래스에 있는 move()메서드 호출
        car.move();
        //SkyloadStrategy 클래스에 있는 move()메서드 호출
        airplane.move();
        
        //setMovingStrategy 메서드를 통해 달리는 곳 설정
        //SkyloadStrategy클래스를 참조하였다
        car.setMovingStrategy(new SkyloadStrategy);
        
        //SkyloadStrategy 클래스에 있는 move()메서드 호출
        car.move();
    }
}

2. 전략 패턴의 장/단점

장점

  1. 객체의 코드를 수정할 필요 없이 새로운 행위를 추가할 수 있다.(소프트웨어의 확장에 유리하며, O에 위배되지 않는다.)
  2. 코드가 실행되는 중간에 전략을 변경할 수 있다.

단점

  1. 추가해야할 행위가 많을 때, 복잡해진다.(모든 전략을 알고 있어야 하기 때문)
  2. 인터페이스와 전략 클래스가 많아지게 되므로 코드가 복잡해 질 수 있다.

자바의 Abstract Class와 Interface

ㅁ Abstract Class(추상 클래스)

  • 클래스가 설계도라면, 추상 클래스는 미완성된 설계도이다.(추상 메서드를 포함하고 있다.) 자동차로 비유 하자면, 다양한 차들은 차마다 차이가 있지만, 엔진이 있고, 바퀴는 4개이며, 핸들이 있다.이와 같이 어느정도 틀을 갖춘 상태를 만들어 줄때 사용한는 것이 추상 클래스이다.

    추상 메서드란 선언부만 작성하고 구현부는 작성하지 않은 채 남겨둔 것이다. 즉 추상 메서드는 상속받는 클래스에 따라 행위가 달라질 수 있는 메서드이다.

ㅁ Interface(인터페이스)

  • 인터페이스는 일종의 추상 클래스로, 추상 메서드를 갖지만 추상 클래스보다 더 추상화되어 있다. 추상 클래스와는 달리 몸통을 갖춘 일반 메서드, 멤버 변수를 가질 수 없다. 위에서 말했듯 추상 클래스가 미완성된 설계도라고 한다면, 인터페이스는 구현된 것은 아무것도 없는 그저 스캐치만 되어있는 기본 설계도이다.

ㅁ 추상 클래스와 인터페이스의 공통점과 차이점

공통점

  1. 둘 다 갖고 있는 추상 메서드 구현을 강제한다. 한 클래스에서 추상 클래스 또는 인터페이스를 사용 한다면, 이 둘에 있는 추상 메서드를 반드시 구현해주어야 한다.
  2. 인스턴스화가 불가능하다.

차이점

  1. 다중상속 - 인터페이스를 구현하는 클래스는 다른 여러개의 인터페이스를 함께 구현할 수 있다. 하지만 추상 클래스같은 경우는 다중으로 상숙을 할 수 없다.
  2. 사용의도
  • 추상 클래스 : 추상 클래스를 상속 할 각 객체들의 공통점을 찾아 추상화시켜 놓은 것이다. 상속 관계를 타고 올라갔을 때, 같은 부모 클래스를 상속하며, 부모 클래스가 가진 기능들을 구현해야할 경우 사용한다.
  • 인터페이스 : 상속 관계를 타고 올라갔을 때, 다른 상위 클래스를 상속하더라도, 같은 기능이 필요할 경우 사용한다. 클래스와 별도로 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용한다. 사람과 식물을 예로 들어보면, 사람과 식물은 서로 다른 클래스이지만 둘 다 호흡을 한다. 이럴때 인터페이스를 사용한다.

Overloading VS Overriding

Overloading(오버로딩)

  • 같은 이름의 메서드 여러개를 가지면서 매개 변수의 유형화 개수가 다르도록 하는 기술

Overriding(오버라이딩)

  • 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용
//오버로딩
class Sum() {
	//기본 plus 메서드
	void plus(int a, int b){
    	System.out.println("2개 합 : " + (a+b))
    }
    //plus 메서드를 변수 3개로 오버로딩
	void plus(int a , int b , int c){
    	System.out.println("3개의 합 : " + (a+b+c));
    }
    //plus 메서드를 변수 4개로 오버로딩
	void plus(int a , int b , int c, int d){
    	System.out.println("4개의 합 : " + (a+b+c+d));
    }
}
public class OverTest{

  public static void main(String[] args) {
		
        //Sum객체 생성
        Sum s = new Sum();
        
        //기본 plus 메서드 호출
        s.plus(1,2); //3
        //변수 3개로 오버로딩된 plus 메서드 호출
        s.plus(1,2,3); //6
        //변수 4개로 오버로딩된 plus 메서드 호출
        s.plus(1,2,3,4); //10
        
  	}
}
//오버라이딩
class Animal{
	public String name;
    public int age;
    
    public void printInfo() {
    	System.out.println("이 아이의 이름은 " +name+"이고 나이는 "+age+ " 살입니다.")
    }
}
class Cat extends Animal{
	String type;
    
    public void printInfo() {
    	System.out.println("이 동물의 종은 " + type+"입니다");
    }
}
public class OverTest{

  public static void main(String[] args) {
		
        //Cat 객체 생성
        Cat c = new Cat();
        
        //Animal 클래스에 있는 변수 오버라이딩
        c.name = "미유";
        c.age = "3";
        //Cat 클래스에 있는 변수 설정
        c.type = "페르시안";
        
        c.Info();
        
  }
}
profile
개발을 꿈꾸는 초짜

0개의 댓글