전략 패턴의 사전적 의미
- 23가지 GoF디자인 패턴 중 하나
- 알고리즘 군을 정의하여 알로리즘을 캡슐화한 뒤 필요할 때 교환해서 사용
전략 : 전쟁, 정치, 사업, 산업 등과 같은 상황에서 성공을 달성할기 위한 상세한 계획 또는 그러한 상황에 대한 기술
전술 : 전략을 짜면서 정한 목표를 달성하기 위한 행동
프로그래밍에서의 전략과 전술
프로그램의 목적은 문제를 해결하는 것
문제를 해결하기 위한 큰 틀을 설계(계획) => 전략
알고리즘으로 문제를 해결(행동) => 전술
MySQL을 사용해서 간단한 프로그램을 만든다고 가정해보자
아래와 같이 4줄짜리 코드가 있다.
public static void main(String[] args) {
MySqlDb mySqlDb = new MySqlDb();
mySqlDb.saveUser(new User("glen"));
User foundUser = mySqlDb.findUser("glen");
mySqlDb.deleteUser(foundUser);
}
MySqlDb라는 클래스가 있고 MySqlDb라는 클래스에는 saveUser, findUser, deleteUser 이런 메소드들이 있고 메소드 안에 로직은 좀 복잡하다고 가정한다.
MySQL과 자바를 소유한 오라클이 2010년에 오라클이 구글에 자바 관련 저작권 소송을 낸 적이 있다. 이 사건으로 인해서 MySQL을 쓰는 기업 대다수가 다른 데이터베이스로 교체하는 사건이 있었다.
그런 사건에 맞춰서 이 MySQL을 버리고 다른 데이터베이스를 사용한다고 가정을 하고, 아래와 같이 MongoDb란 클래스를 새로 만들어서 기존 MySqlDb에 있던 saveUser, findUser, deleteUser 메소드들을 처음부터 끝까지 새로 싹 구현했다고 가정해보자
public static void main(String[] args) {
// MySqlDb mySqlDb = new MySqlDb();
MongoDb mongoDb = new MongoDb();
mongoDb.saveUser(new User("glen"));
User foundUser = mongoDb.findUser("glen");
mongoDb.deleteUser(foundUser);
}
그런데 이렇게 많은 코드를 작성한다고 끝이 아니다. 기존에 MongoDb를 사용했던 다른 클래스들도 전부 코드를 수정해 주어야 한다. 이 상황에서 MongoDb만 사용하는 게 아니라 MariaDb 등 다른 DB로 또 교체해야 한다고 가정한다면 이전에 했던 복잡한 과정들을 다시 해야한다.
이런 방식이 올바른 방식일까?
정답은 전략 패턴을 사용하는 것이다.
전략 패턴은 다음과 같은 구조로 되어있다.
인터페이스인 Strategy 전략을 가지고 있고, 전략을 구현하는 Strategy 클래스 그리고 전략 인터페이스를 가지고 있는 Context 클래스로 이루어져 있다.
public class DbService {
private final DbStrategy dbStrategy;
public DbService(DbStrategy dbStrategy) {
this.dbStrategy = dbStrategy;
}
public void saveUser(User user) {
dbStrategy.saveUser(user);
}
}
위에서 만들었던 DB클래스를 가지고 전략 패턴을 사용해 보았다. DbService가 Context 역할을 하고 내부 필드로 DbStrategy를 가지고 있다. DbService는 생성자로 DbStrategy를 주입받는다. 즉, FooStrategy, BarStrategy 와 같은 구현 클래스들을 주입받는다. saveUser는 Context의 operation() 메서드이고 DbStrategy는 Strategy의 algorithm(), 즉 행동 역할을 하고 있다. 이것으로 아래와 같이 DB 클래스를 다시 재설계 해보았다.
public static void main(String[] args) {
DbService dbService = new DbService(new MySqlDbStrategy());
dbService.saveUser(new User("glen"));
}
public static void main(String[] args) {
DbService dbService = new DbService(new MariaDbStrategy());
dbService.saveUser(new User("glen"));
}
public static void main(String[] args) {
DbService dbService = new DbService(new MongoDbStrategy());
dbService.saveUser(new User("glen"));
}
전략 패턴을 요약 하자면 알고리즘을 객체로 분리하여 패턴화 한 뒤, 전략 객체의 외부로부터 전달을 받아 관계를 설정하는 것을 말한다. 이 말은 의존성 주입이란 말과 비슷하다.
의존성 주입은 결합도를 느슨하게 하여 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것을 말한다.
정리하자면 전략 패턴을 사용하게 된다면 의존성 주입을 사용하게 되고 의존성 주입을 사용한다면 의존관계 역전 원칙과 단일 책임 원칙을 지키면서 더욱 객체지향적인 프로그램 설계를 할 수 있게 된다는 것이다.
의존성 주입의 이점
1) 코드를 유연하게 하고 확장을 쉽게 한다.
2) 클래스 간의 결합을 낮춰 리팩터링이 쉽다.
3) 클래스를 더 독립적으로 만들어 테스트가 쉽다.
이 중에서 확장을 쉽게 하고 리팩터링이 쉽다는 말은 위에서 본것과 같이 DB 코드를 봐서 이해가 되지만 테스트가 쉽다는 건 무슨 말일까?
public class Car {
private static final int MIN_MOVE_NUMBER = 4;
private int position;
public Car(int position) {
this.position = position;
}
public void move(int number) {
if (number >= MIN_MOVE_NUMBER) {
position++;
}
}
public int getPosition() {
return position;
}
}
public class CarService {
private final Random random = new Random();
public void moveCar(Car car) {
car.move(random.nextInt(10));
}
}
위와같이 Car 클래스와 Service 클래스가 있다.
Car 클래스는 position이 있고 일정 숫자가 들어올 시 최소 숫자와 비교해서 자기의 position을 증가시키는 move 메소드를 가지고 있다. CarService는 Car를 인수로 받고 random에서 생성된 숫자로 차를 움직이는 역할을 하는 moveCar 메소드가 있다. 이 코드를 테스트한다고 가정한다.
@Test
@DisplayName("자동차는 움직여야 한다.")
void car_move() {
Car car = new Car(0);
CarService carService = new CarService();
carService.moveCar(car);
assertThat(car.getPosition())
.isGreaterThan(0);
}
위와같이 테스트 코드를 작성하였다. 위 코드는 항상 성공한다고 보장할 수 없다. 이유는 random이라는 필드를 직접적으로 사용하고 있기 때문이다. random이란 필드는 랜덤하니까 이 테스트 코드가 항상 성공한다고 보장할 수 없게된다.
테스트 원칙 중 FIRST 테스트 원칙이라는 것이 있다. FIRST 테스트 원칙은 빠르고, 독립적이고, 반복가능하며, 자체 검증가능하고, 철저하게 테스트를 작성해야 한다는 원칙이다. 이 부분에서 실수한 부분은 '반복가능하며'라는 부분이다. 왜냐면 랜덤값으로 테스트를 하고 있어서 그렇다.
그렇다면 랜덤값을 사용하지 않고 어떻게 해야 프로그램의 구조를 크게 변경하지 않으면서 테스트를 할 수 있을까?
위와같이 전략 패턴 구조를 맞춰서 설계를 하면 된다.
public class CarService {
private final NumberStrategy numberStrategy;
public CarService(NumberStrategy numberStrategy) {
this.numberStrategy = numberStrategy;
}
public void moveCar(Car car) {
car.move(numberStrategy.generate());
}
}
CarService에는 random이라는 필드를 직접 사용하는 것이 아니라 NumberStrategy라는 전략 객체를 사용한다. 이 전략 객체(NumberStrategy)는 CarService 생성자에서 주입을 받는다. 그리고 moveCar 메서드를 보면 NumberStrategy에서 생성한 값으로 차를 움직인다. 위와 같이 코드를 설계하게 되면 아래와 같이 테스트 코드를 작성할 수 있다.
@Test
@DisplayName("자동차는 움직여야 한다.")
void car_move() {
Car car = new Car(0);
CarService carService = new CarService(new FixedNumberStrategy(5));
carService.moveCar(car);
assertThat(car.getPosition())
.isEqualTo(1);
}
프로덕션 코드에서 RandomNumberStrategy를 사용해서 차를 항상 랜덤하게 움직일 수 있고 테스트 코드에서는 FixedNumberStrategy를 사용해서 우리가 항상 원하는대로 차를 움직일 수 있다. 이처럼 테스트 코드 작성이 쉬워지고 더욱 객체지향적인 프로그램을 설계할 수 있으며 리팩터링이 매우 쉬워진다.
전략 패턴의 단점은 인터페이스가 변하게 되면 기존에 구현했던 전략들이 전부 변하게 되면서 새로 설계를 해야 된다는 점이다.
참고
https://www.youtube.com/watch?v=nSIVFhtrnFo&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC
'개발 관련 강의 정리 > 10분 테코톡' 카테고리의 다른 글
[10분 테코톡] 클레이의 상속과 조합 정리 (0) | 2023.05.25 |
---|---|
[10분 테코톡] 아서의 싱글턴 패턴과 정적 클래스 정리 (0) | 2023.05.24 |
[10분 테코톡] 포키의 Parameter와 Argument 정리 (0) | 2023.05.22 |
[10분 테코톡] 이프의 성능 테스트 정리 (0) | 2023.05.21 |
[10분 테코톡] 부나의 Java에서 Kotlin으로 정리 (0) | 2023.05.20 |
댓글