Woowacourse/레벨1

[레벨 1 회고] 전략패턴을 사용해보며

s.rookie 2022. 4. 18. 11:38

미션을 진행하다보면 객체들이 할 수 있는 다양한 행위가 설계해야 하는 경우가 생긴다. 행위를 변경해주기 위해서는 개발자가 직접 수정해주어야 하는데 이는 OCP를 위배하게 되는 요소중 하나이다. 이러한 문제들을 해결하는 방법 중 하나로 디자인 패턴 중 하나인 전략패턴을 제시해본다.

 

 

전략패턴을 사용하기 전 생기는 문제점들


자전거라는 예시를 통해서 한번 살펴보도록 하자.

 

자전거라는 클래스는 display 메서드를 통해서 이름을 말해주며, fuel 메서드를 통해서 움직이는데 필요한 에너지를 말해주는 역할을 수행한다.

 

public class Bicycle {
    public void display() {
    	System.out.println("자전거 입니다.");
    }
    
    public void fuel() {
        System.out.println("연료는 동력입니다.");  	
    }
}

 

시간이 지나서, 전기 자전거가 발매되었다. 이렇게 되면 전기 자전거, 일반 자전거로 클래스를 분리하여서 관리해야 할 것이다.

 

public class ElectricBicycle {
    public void display() {
    	System.out.println("전기 자전거 입니다.");
    }
    
    public void move() {
        System.out.println("움직입니다.");  	
    }
}

public class GenearlBicycle {
    public void display() {
    	System.out.println("일반 자전거 입니다.");
    }
    
    public void move() {
        System.out.println("움직입니다.");  	
    }
}

 

시간이 더 지나서, 태양열로 움직이는 자전거를 발매했다고 가정하자. 그렇다면 또 SolorHeatBicycle를 만들어서 해당 객체를 관리해주어야 할 것이다. 결국 개발자가 신경써야 하는 관리 지점이 늘어나게 되는 것이다.

 

이러한 객체 관리 문제를 해결하기 위해서 상속을 사용해서 해결을 시도해 볼 수 있다.

 

public abstract class Bicycle {
    public abstract void display();
    
    public void move() {
    	System.out.println("움직입니다.");
    }
}


public class ElectricBicycle extends Bicycle {
    @Override
    public void display() {
    	System.out.println("전기 자전거 입니다.");
    }
}

public class GenearlBicycle extends Bicycle {
    @Override
    public void display() {
    	System.out.println("일반 자전거 입니다.");
    }
}

 

이렇게 Bicycle을 기능들을 추상화하여, 각 Bicycle들을 좀 더 편하게 관리할 수 있게 된다. 하지만 상속을 사용한 방법 역시 몇가지 문제점이 존재한다.

 

 

1. 기능 확장의 어려움

 

만약 새로운 기능으로 `fule`라는 기능을 추가한다면 어떻게 될까?? fule라는 abstract 메서드를 정의하고 상속받는 모든 자식클래스에서 재정의를 해주어야 할 것이다.

public abstract class Bicycle {
    public abstract void display();
    public abstract void fuel();
    
    public void move() {
    	System.out.println("움직입니다.");
    }
}


public class ElectricBicycle extends Bicycle {
    @Override
    public void display() {
    	System.out.println("전기 자전거 입니다.");
    }
        
    @Override
    public void fuel() {
        System.out.println("연료는 전기입니다.");  	
    }
}

public class GenearlBicycle extends Bicycle {
    @Override
    public void display() {
    	System.out.println("일반 자전거 입니다.");
    }
        
    @Override
    public void fuel() {
        System.out.println("연료는 동력입니다.");  	
    }
}

 

2. OCP 위반

 

만약 move 메서드가 `빠르게 움직인다`는 기능이 업데이트 되면서 '움직인다'와 '빠르게 움직인다' 2개의 버전으로 관리된다면 어떻게 해야 할까? 기존에 사용하고 있는 모든 코드가 수정되야 할 것이다. 결국 이는 객체지향의 5대 원칙 중 OCP 원칙을 위반하게 된다.

 

전략패턴의 적용


전략 패턴을 이용하면 위에서 언급된 문제들을 제거할 수 있게 된다.

 

짧게 전략 패턴의 정의에 대해서 설명하고 넘어가자면, 전략 패턴은 디자인 패턴 중의 하나로 객체가 할 수 있는 행위를 각각의 전략으로 만들어 사용하고 이를 동적으로 수정이 가능하도록 설계된 패턴을 말한다.

 

코드를 통해서 한번 살펴보도록 하자.

 

public class Bicycle {
    private MoveStrategy moveStrategy();
    private FuleStrategy fuleStrategy();
    
    public Bicycle(MoveStrategy moveStrategy, FuleStrategy fuleStrategy) {
        this.moveStrategy = moveStrategy;
        this.fuleStrategy = fuleStrategy;
    }
    
    public void move() {
        moveStrategy.move();
    }
    
    public void fule() {
        fuleStrategy.fule();
    }
}

 

public interface MoveStrategy() {
    void move();
}

public class General implements MoveStrategy {
    @Override
    public void move() {
        System.out.println("움직입니다".);
    }
}

public class Fast implements MoveStrategy {
    @Override
    public void move() {
        System.out.println("더 빨리 움직입니다".);
    }
}

 

public interface FuleStrategy() {
    void fule();
}

public class General implements FuleStrategy {
    @Override
    public void fule() {
        System.out.println("연료는 동력입니다.".);
    }
}

public class Electric implements FuleStrategy {
    @Override
    public void fule() {
        System.out.println("연료는 전기입니다.".);
    }
}

public class SolarHeat implements FuleStrategy {
    @Override
    public void fule() {
        System.out.println("연료는 태양열입니다.".);
    }
}

 

이렇게 추후 상황에 따라 이동 정책이 변화 혹은 수정되거나, 자전거에 사용되는 연료가 추가되더라도 그에 대한 전략을 추상화하고 객체 컴파일 시점에 구체 클래스를 주입하여서 각 상황에 맞는 다양한 로직을 실행할 수 있게 된다.

 

public class Main {
    public static void main(String[] args) {
        Bicycle bicycle1 = new Bicycle(new General(), new Electric());
        Bicycle bicycle2 = new Bicycle(new General(), new SolorHeat());
    }
}

 

정리하자면, 전략 패턴은 동일한 계열의 역할(알고리즘)을 정의하고, 각 역할(알고리즘)을 캡슐화 한다고 할 수 있다.

 

이러한 위의 예시들은 상속 vs 조합의 관점에서도 접근 할 수 있는 내용이다. 상속 vs 조합에 대한 내용은 추후 포스팅을 통해서 다시 한번 자세히 정리할 생각이다.