테스트 코드 강의를 듣다가 본 리팩토링 코드 예제가 무척 깔끔하고 확장성이 좋아보였다. 무엇보다 리팩토링 하기 전의 결합도 높은 소스코드가 평소 내가 많이 사용하는 방식이었다. 이참에 제대로 알아두면 좋겠다 싶어 찾아보니 디자인 패턴 중에 하나였다. Strategy Pattern(전략 패턴)
이라고 하는데 어떤 개념이고 어떻게 사용하는지 공부해 보았다.
1. Strategy Pattern의 정의
전략 패턴은 실행(런타임) 중에 전략을 선택하여 객체의 기능이나 동작을 실시간으로 바꿀 수 있는 행위 디자인 패턴
이다. 하나의 인터페이스를 각각 다른 클래스에서 구현해두고, 클라이언트의 요청에 맞추어 컨텍스트에서 메서드를 호출해서 사용한다.
이럴 때 사용하면 좋다 :
- 동작이 실행 중에 실시간으로 교체되어야 할 때
- 구현체가 자주 변경 되거나 추가 될 때
- 테스트 코드를 작성할 때
이런 점은 주의 :
- 구현체가 많아질수록 관리해야 할 객체의 수가 늘어난다
- 구현체가 자주 변경되거나 추가되지 않는다면 굳이 인터페이스를 따로 빼서 복잡하게 만들 필요가 없다.
- 소스코드 파악에 좀 더 시간이 걸릴 수 있다.
2. 쉬운 예시와 함께 알아보기
디자인패턴 이론 만으로는 무슨 말인지 완전히 와닿지 않았다. 이해하기 쉽도록 예시를 작성해보니 좀 더 쉽게 이해가 갔다.
- 우선
뭔가를 끓임
이라는 추상화 상태의 인터페이스를 만든다. - 무엇을 끓일 건지 각각의 클래스에서 인터페이스를 구체화 시킨다.
- 받은 재료로 스프를 끓일 건지, 소스를 끓일 건지 결정하고 요리하기 위한
냄비(Context)
가 필요하다. 요리사(Client)
는 어떤 재료를 사용할 건지 결정해서냄비에 전달
한다.
이 내용을 코드로 작성해보자.
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
이렇게 하면 스프를 죽으로 바꿔야 하는 경우가 생겨도 냄비를 건드리지 않고 스프 클래스만 변경하면 된다. 국을 추가해야 하는 경우에도 새 구체화 메서드만 만들어두면 언제든지 셰프가 냄비에 넣을 수 있다.
3. 좀 더 실무에 가까운 예제
배달 음식 주문 시스템을 상정하고 예제를 만들어보았다.
- 음식 정보가 들어갈 도메인 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public class Item { private String menu; private int price; private int amount; private int tip; public Item(String menu, int price, int amount, int tip) { this.menu = menu; this.price = price; this.amount = amount; this.tip = tip; } public String getMenu() { return menu; } public int getPrice() { return price; } public int getAmount() { return amount; } public int getTip() { return tip; } }
주문하기
라는 인터페이스 만들기1
2
3
4public interface OrderStrategy { //주문 기능 void order(int totalPrice); }
도보배달 주문
과택시배달 주문
으로 구체화 하기1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class WalkDeliveryStrategy implements OrderStrategy { private String name; private String payType; private boolean useCoupon; private double discount; public WalkDeliveryStrategy(String name, String payType, boolean useCoupon, double discount){ this.name = name; this.payType = payType; this.useCoupon = useCoupon; this.discount = discount; } @Override public void order(int totalPrice){ int lastPrice = totalPrice; int discountPrice = (int)(totalPrice*discount); if(useCoupon){ lastPrice = lastPrice-discountPrice; } System.out.println("도보 배달 시 총 지불금액은 "+lastPrice+"원입니다."); } }
1 |
|
-
배달 방식을 결정하고 그에 따른 정보를 전달할
장바구니
만들기
```java
public class OrderCart { List- items = new ArrayList<>();
public void addItem(Item item){ items.add(item); } public void totalPrice(OrderStrategy orderStrategy){ int totalPrice = 0; for(Item item : items){ totalPrice += item.getPrice()*item.getAmount()+item.getTip(); } orderStrategy.order(totalPrice); }
}
1 |
|
4. 테스트 코드에서는 어떻게 활용할까
여러가지 결과값이 나올 수 있는 테스트 상황을 가정해보자. 예를 들어 패스워드가 8자리 이상 12자리 이하인지 아닌지
를 테스트 해야 한다고 치자. 글자수 1~12자리를 랜덤으로 출력하면 테스트 코드 결과가 어쩔때는 성공하고, 어쩔때는 실패해서 테스트 코드로 결과를 확인하기가 어렵다. 글자수가 무조건 8~12자리인 경우와 무조건 잘못된 경우를 나누어서 만들어 놓고 테스트하면 좀 더 쉽게 결과를 알 수 있을 것이다.
패스워드 생성
인터페이스 작성 ```java
@FunctionalInterface public interface PasswordGenerator { String generatePassword(); }
1 |
|
1 |
|
-
패스워드를 입력받고 정보를 전달할 Context 만들기
```java
public class User { private String password; public void initPassword(PasswordGenerator passwordGenerator){1
2
3
4
5
6
7String randomPassword = passwordGenerator.generatePassword(); /* 비밀번호는 최소 8자 이상, 12자 이하여야 한다. */ if(randomPassword.length()>=8 && randomPassword.length()<=12){ this.password = randomPassword; } }
public String getPassword() { return password; } }
1 |
|