여러 방식으로 기능이 작동되는 메서드에서, 또 다른 기능을 추가해야 할 경우 어떻게 해야 할까요? 🤔
코드 형태에 따라 구현 방식이 천지 차이지만, if-else로 분기를 태워서 한 메서드에서 서로 다른 기능을 제공하는 경우가 많이 존재할 겁니다. 간단한 예시로 연산 종류에 따른 다른 동작을 수행시키는 계산기 코드를 다음과 같이 작성해 보았어요.
public static int calculate(String operation, int operand1, int operand2) {
int result;
if (operation.equals("add")) {
result = operand1 + operand2;
} else if (operation.equals("subtract")) {
result = operand1 - operand2;
} else if (operation.equals("multiply")) {
result = operand1 * operand2;
} else if (operation.equals("divide")) {
if (operand2 == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
result = operand1 / operand2;
} else {
throw new IllegalArgumentException("Invalid operation");
}
return result;
}
위에 코드를 보면 어떤 생각이 드나요? 생각보다 직관적이고 간단하지 않나요? 😊
계산기에 들어 있는 기능들이어서 간단한 조건에 따른 분기 처리하여, 기능을 완성시킬 수 있습니다.
if-else문은 대부분의 개발자가 익숙한 구문이며, 위와 같은 간단한 기능들은 직관적으로 확인할 수 있습니다.
하지만 이와 같은 구조는 기능을 확장하기 위해서는 수정이 필요한 구조예요. 🤨
이는 객체지향 설계 5원칙인 SOLID 중 개방 폐쇄 원칙(Open-Closed Principle)의 위배를 합니다. 🙅♂️
전략 패턴(Strategy Pattern)
위와 같이 한 메서드 안에서 조건에 따른 다른 기능을 반환할 때 사용되는 적합한 디자인 패턴 중에 전략 패턴이 있습니다.
전략 패턴이란 유사한 동작을 하지만 다른 방식으로 구현되어 있는 행위(전략)들을 공통의 인터페이스로 구현하는 각각의 전략 클래스로 구현하고, 동적으로 바꿀 수 있도록 하는 패턴이에요.
전략 패턴으로 구현한 코드는 개발자가 직접 행위에 대한 코드를 수정할 필요 없이 전략만 변경하여 유연하게 확장할 수 있습니다.
다음은 if-else문으로 분기를 태워서 다른 기능을 작동시켰던 계산기 메서드를 전략 패턴 구조를 사용한 예시입니다.
전략 패턴을 사용한 구조 예시
전략 패턴을 사용하기 위해서는 전략에 대한 인터페이스를 먼저 구현해야 합니다. 따라서 연산 값을 반환하는 calculate 메서드를 갖는 Strategy 인터페이스를 다음과 같이 정의했습니다.
// 전략 인터페이스
interface Strategy {
int calculate(int operand1, int operand2);
}
다음은 연산 종류에 따른 전략을 클래스로 구현하였습니다. 각각의 클래스는 Strategy 인터페이스의 구현체예요.
// 덧셈 전략
class AddStrategy implements Strategy {
@Override
public int calculate(int operand1, int operand2) {
return operand1 + operand2;
}
}
// 뺄셈 전략
class SubtractStrategy implements Strategy {
@Override
public int calculate(int operand1, int operand2) {
return operand1 - operand2;
}
}
// 곱셈 전략
class MultiplyStrategy implements Strategy {
@Override
public int calculate(int operand1, int operand2) {
return operand1 * operand2;
}
}
// 나눗셈 전략
class DivideStrategy implements Strategy {
@Override
public int calculate(int operand1, int operand2) {
if (operand2 == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return operand1 / operand2;
}
}
각 연산 종류에 해당하는 전략을 구현한 클래스들과 전략을 사용하는 클라이언트 클래스인 Calculator를 다음과 같이 작성하였습니다. 클라이언트는 필요에 따라 적절한 전략을 선택하여 사용할 수 있어요.
// 연산 수행 클래스
class Calculator {
private Strategy strategy;
public Calculator(Strategy strategy) {
this.strategy = strategy;
}
public int calculate(int operand1, int operand2) {
return strategy.calculate(operand1, operand2);
}
}
Calculator의 인스턴스가 생성될 때, 인자를 통해 외부로부터 연산 전략을 주입받고, calculate 메서드에서는 Strategy 인터페이스가 제공하는 calculate 메서드를 호출하여 연산의 결과 값을 반환할 수 있어요.
전략 패턴을 사용한 구조에서, 아래와 같이 코드를 사용할 수 있습니다.
Strategy addStrategy = new AddStrategy();
Strategy subtractStrategy = new SubtractStrategy();
Strategy multiplyStrategy = new MultiplyStrategy();
Strategy divideStrategy = new DivideStrategy();
Calculator calculator = new Calculator(addStrategy);
Calculator calculator = new Calculator(subtractStrategy);
Calculator calculator = new Calculator(multiplyStrategy);
Calculator calculator = new Calculator(divideStrategy);
이렇게 전략 패턴으로 설계된 코드에서 새로운 연산을 추가된다고 해도 Calculator 클래스의 코드를 더 이상 수정할 필요 없게 됩니다.
만약 클라이언트에서 지수 연산 기능도 추가 요청을 한다면, 기존 구조에서 해당 연산 전략 클래스만 추가하면 됩니다.
// 지수 연산 전략
class ExponentiationStrategy implements Strategy {
@Override
public int calculate(int operand1, int operand2) {
return (int) Math.pow(operand1, operand2);
}
}
이렇게 새롭게 추가한 지수 전략 클래스를 사용하여 계산기에서 지수 연산을 추가하면 다음과 같습니다.
Strategy exponentiationStrategy = new ExponentiationStrategy();
Calculator calculator = new Calculator(exponentiationStrategy);
System.out.println("Exponentiation: " + calculator.calculate(2, 3)); // 8
새로운 연산을 추가하기 위해 기존 코드를 수정할 필요 없이 새로운 전략 클래스를 추가하고 적절한 전략으로 설정함으로써 새로운 기능을 확장할 수 있게 됩니다.
따라서, 전략 패턴은 복잡한 조건 분기를 간소화하고 유지보수성을 향상하는 데 도움이 되며, 새로운 기능을 추가하거나 기존 기능을 변경할 때 코드 수정이 용이하도록 합니다.
만약 알고리즘의 동적 교체나 다양한 구현을 지원해야 할 때에는 전략 패턴을 적용하는 것이 적합한 거 같아요 😊
전략 패턴 구조(Strategy Pattern Structure)
전략 패턴의 주요 구성 요소는 다음과 같습니다.
- Context(콘테스트): 전략 객체를 사용하는 클라이언트를 나타내며, 콘텍스트는 전략 인터페이스를 참조하고, 필요에 따라 전략을 교체할 수 있습니다. 위에 예시에서는 연산을 수행하는 Calculator 클래스가 이에 해당합니다.
// 연산 수행 클래스
class Calculator {
private Strategy strategy;
public Calculator(Strategy strategy) {
this.strategy = strategy;
}
public int calculate(int operand1, int operand2) {
return strategy.calculate(operand1, operand2);
}
}
- Strategy(전략): 알고리즘을 나타내는 인터페이스로, 인터페이스를 구현하여 각각의 알고리즘을 캡슐화합니다. 위에 예시에서는 연산 메서드를 작성한 전략 인터페이스가 이에 해당합니다.
// 전략 인터페이스
interface Strategy {
int calculate(int operand1, int operand2);
}
- ConcreteStrategy(구체적인 전략): 전략 인터페이스를 구현한 구체적인 전략 클래스입니다. 위에 예시에서는 각각의 연산 전략 클래스가 이에 해당합니다.
// 지수 연산 전략
class ExponentiationStrategy implements Strategy {
@Override
public int calculate(int operand1, int operand2) {
return (int) Math.pow(operand1, operand2);
}
}
전략 패턴 특징(Strategy Pattern Characteristic)
전략 패턴 장점
- 코드의 유연성과 확장성이 높음.
- 각 전략은 독립적으로 구현되어 있으므로 유사한 기능을 수행하는 다양한 알고리즘들을 재사용 가능.
- 각 전략이 독립적으로 변경될 수 있으므로 유지보수가 용이하며, 한 전략의 변경이 다른 전략에 영향을 끼치지 않음.
전략 패턴 단점
- 클라이언트가 적절한 전략을 선택하는 책임이 있기 때문에, 클라이언트가 전략을 정확하게 이해하고 선택해야 함.
- 전략마다 별도의 클래스를 생성해야 하므로 클래스의 수가 늘어날 수 있기 때문에, 코드의 복잡성을 높음.
전략 패턴 특징
- 전략 패턴을 사용하면 알고리즘과 클라이언트 사이의 결합도를 낮출 수 있음.
- 전략 패턴을 사용하면 동일한 인터페이스를 갖는 다양한 알고리즘을 사용할 수 있어서, 알고리즘을 동적으로 교체 가능.
업무를 하다 보면,다른 분이 작성한 코드에 새로운 기능을 추가할 때가 있었는데,,
if-else문으로 되어 있으면, else if() {... }로 간단하게 작성한 저를 다시 돌아보게 되는 계기가 되었어요.🥲
다음 번에 해당 로직을 수정할 일이 있으면,
오늘 배운 전략 패턴을 사용해서 리팩토링을 해봐야겠어요.🤔🤔
이번에 4번째 디자인 패턴 스터디인데,
오늘도 흥미로운 디자인 패턴을 공부할 수 있어서 좋았네요.
이번에 배운 전략 패턴은 정말 유용하게 사용할 거 같아요.
'Computer Science > Design Pattern' 카테고리의 다른 글
[Design Pattern] 빌더 패턴(Builder Pattern)에 대해서, @Builder - 컴도리돌이 (0) | 2024.05.05 |
---|---|
[Design Pattern] 퍼사드 패턴(Facade Pattern)에 대해서 - 컴도리돌이 (0) | 2024.04.04 |
[Design Pattern] 데코레이터 패턴(Decorator Pattern)에 대해서 - 컴도리돌이 (0) | 2024.03.18 |
[Design Pattern] 복합체 패턴(Composite Pattern)에 대해서 - 컴도리돌이 (0) | 2024.02.20 |
[Design Pattern] 추상 팩토리(Abstract Factory)에 대해서 - 컴도리돌이 (2) | 2024.01.24 |