Sun's Blog
[10분 테코톡] 전략 패턴 본문
전략 패턴의 사전적 의미
알고리즘 군을 정의하여 알고리즘을 캡슐화한 뒤 필요할 때 교환해서 사용(23가지 GoF 디자인 패턴 중 하나)
전략
전쟁, 정치, 사업, 산업등과 같은 상황에서 성공을 달성하기 위한 상세한 계획 또는 그러한 상황에 대한 기술
전술
전술은 전략과 유사, 전략을 짜면서 정한 목표를 달성하기 위한 행동
프로그래밍에서의 전략과 전술
프로그램의 목적은 문제를 해결하는 것
- 문제를 해걸하기 위해 큰 틀을 설계(계획) => 전략
- 알고리즘으로 문제를 해결(행동) => 전술
예시
public static void main(String[] args) {
MySqlDb mySqlDb = new MySqlDb();
mySqlDb.saveUser(new User("glen"));
User user = mySqlDb.findUser("glen");
mySqlDb.deleteUser(user);
}
위의 코드는 MySQL을 사용하고 있다. 그런데 MySQL에서 MongoDb로 이전한다면 아래와 같이 변경된다.
public static void main(String[] args) {
MongoDb mongoDb = new MongoDb();
mongoDb.saveUser(new User("glen"));
User user = mongoDb.findUser("glen");
mongoDb.deleteUser(user);
}
이렇게만 바꾼다 쳐도 기존의 엔티티나 다른 복잡한 설정들 또한 바꿔야 한다. 이러한 상황을 위해 전략 패턴을 사용해야 한다. 아래는 전략 패턴의 UML이다. 이렇게만 보면 어려우느 예시 코드가 작성되었다.
아래의 코드를 보면 DbService가 Context의 역할, DbStrategy가 Strategy 역할을 하며 각각 operation, algorithm을 가지고 있다.
public class DbService { // Context
private final DbStrategy dbStrategy; // Strategy
public DbService(DbStrategy dbStrategy) {
this.dbStrategy = dbStrategy;
}
public void saveUser(User user) { // operation
dbStrategy.saveUser(user); // 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 MariaDbDbStrategy());
dbService.saveUser(new User("Glen"));
}
public static void main(String[] args) {
DbService dbService = new DbService(new MongoDbStrategy());
dbService.saveUser(new User("Glen"));
}
공통적으로 DbSerivce를 사용하는 것을 볼 수 있다. DB를 교체할 때 DbStrategy만 교체하면 된다.
전략 패턴 요약
알고리즘을 객체로 분리하여 패턴화 한 뒤, 전략 객체의 외부로부터 전달을 받아 관계를 설정한다. -> 의존성 주입과 비슷
의존성 주입
결합도를 느슨하게 하여 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리
의존성 주입 장점
- 코드를 유연하게 하고, 확장을 쉽게 한다.
- 클래스간의 결합을 낮춰 리팩터링이 쉽다.
- 클래스를 더 독립적으로 만들어 테스트가 쉽다.
테스트가 쉽다?
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));
}
}
@Test
@DisplayName("차는 움직여야 한다.")
void car_move() {
//given
Car car = new Car(0);
CarService carService = new CarService();
//when
carService.moveCar(car);
//then
assertThat(car.getPosition())
.isGreaterThan(0);
}
위의 테스트코드는 항상 성공할까? 답은 아니다. 랜덤한 값때문에 차가 움직일 때도 있고 움지이지 않을 때도 있다. 이러면 FIRST 테스트 원칙의 R(Repeatable: 반복가능)을 위반한다. 전략 패턴을 사용하면 랜덤값을 사용하지 않고 테스트 할 수 있다.
public class CarService {
private final NumberStrategy numberStrategy;
public CarService(NumberStrategy numberStrategy) {
this.numberStrategy = numberStrategy;
}
public void moveCar(Car car) {
car.move(numberStrategy.generate());
}
}
CarService에서 직접 Number를 생성하지 않고 NumberStrategy를 이용하여 생성한다.
@Test
@DisplayName("차는 움직여야 한다.")
void car_move() {
//given
Car car = new Car(0);
CarService carService = new CarService(new FixedNumberStrategy(5));
//when
carService.moveCar(car);
//then
assertThat(car.getPosition())
.isGreaterThan(0);
}
이제 이 테스트 코드는 몇번을 반복해도 테스트가 성공한다.
결론
이렇게 전략 패턴을 사용한다면 더욱 객체지향적인 코드를 작성할 수 있으며 리팩토링 또한 쉬워진다. 허나 전략 패턴을 사용할 경우 인터페이스에 변경사항이 있을 때 기존의 전략들이 변경되어 새로 설계해야 한다.
참고
[10분 테코톡: 글렌의 전략 패턴]: https://www.youtube.com/watch?v=nSIVFhtrnFo
'ETC' 카테고리의 다른 글
Git 명령어 (0) | 2023.07.07 |
---|---|
Ubuntu Gitlab 설치 (0) | 2023.07.07 |
[10분 테코톡] Spring vs SpringBoot (0) | 2023.07.05 |
[10분 테코톡] 단위테스트 (0) | 2023.06.15 |
JWT (1) | 2023.05.15 |