티스토리 뷰
[개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴] 7-1. 전략(Strategy) 패턴
로또 미션 1단계 코드 리뷰에서 나는 다음과 같은 리뷰를 받았다.
당시 자동차 경주 미션에서는 인터페이스를 사용하지 않았어서.. 어떻게 해야하는거지?? 🤔 하는 당혹스러움이 있었다.
테코블에 "인터페이스를 분리하여 테스트하기 좋은 메서드로 만들기"라는 주제의 글이 있었고 많은 도움을 받았다.
해당 글에는 자동차 경주 미션을 예시로 보여줬기에 "아 이런 방식을 로또 미션에 적용해보면 되겠다"라는 생각으로 적용을 해봤었다.
LottoGenerator
랜덤 로또 생성을 테스트하기 어렵기 때문에 랜덤이 아니라 고정된 로또를 생성하는 기능이 필요하다고 생각했다.
둘 모두 "로또를 생성"한다는 점에서 같으므로 일단 LottoGenerator 인터페이스로 추상화하였다.
public interface LottoGenerator {
List<LottoNumber> makeLottos();
}
RandomLottoGenerator vs. FixedLottoGenerator
제공하는 기능에 따라 LottoGenerator 구현체를 생성했다.
RandomLottoGenerator
생성자에서 1~45까지의 LottoNumber 리스트를 생성하고,
makeLottos() 메서드로 랜덤 생성한 LottoNumber 리스트를 반환한다.
FixedLottoGenerator
생성자에서 이미 지정된 로또 번호를 받고,
makeLottos() 메서드로 LottoNumber 리스트를 생성해 반환한다.
처음 의도는 테스트를 쉽게하기 위함이었지만, FixedLottoGenerator의 경우 당첨번호 생성에도 사용할 수 있으며 추후 로또 미션 2단계에서 수동 로또 생성 기능에 활용할 수 있겠다고 생각했다. 개이득 👍
public class RandomLottoGenerator implements LottoGenerator {
private static final int LOTTO_NUMBER_START_INDEX = 0;
private static final int LOTTO_NUMBER_END_INDEX = 6;
private static final int LOTTO_NUMBER_LOWER_BOUND = 1;
private static final int LOTTO_NUMBER_UPPER_BOUND = 46;
private final List<LottoNumber> numbers;
public RandomLottoGenerator(){
numbers = new ArrayList<>();
for (int i = LOTTO_NUMBER_LOWER_BOUND; i < LOTTO_NUMBER_UPPER_BOUND; i++) {
numbers.add(new LottoNumber(i));
}
}
public List<LottoNumber> makeLottos() {
Collections.shuffle(numbers);
return numbers.subList(LOTTO_NUMBER_START_INDEX, LOTTO_NUMBER_END_INDEX);
}
}
public class FixedLottoGenerator implements LottoGenerator {
private final List<Integer> numbers;
public FixedLottoGenerator(final List<Integer> numbers) {
this.numbers = numbers;
}
@Override
public List<LottoNumber> makeLottos() {
return numbers.stream()
.map(LottoNumber::new)
.collect(toList());
}
}
LottoGenerator 사용 방법의 문제
처음에는 다음과 같이 코드를 작성했었는데, 뭔가 찜찜함이 느껴졌었다. 지금 보니 진짜 엉망이다..
public void makeManualLottos(List<List<Integer>> inputLottos) {
validateEqualToManualLottoCount(inputLottos);
for (List<Integer> inputLotto : inputLottos) {
lottos.add(new Lotto(new FixedLottoGenerator(inputLotto).makeLottos()));
}
}
public void makeAutoLottos() {
LottoGenerator lottoGenerator = new RandomLottoGenerator();
for (int i = 0; i < autoLottoCount; i++) {
lottos.add(lottoGenerator.makeLottos());
}
}
이러면.. 인터페이스로 추상화를 왜 한거지..? 의미가 없지 않나..? 🤔
그러던 와중 무심코 지나쳤던 "전략 패턴"이라는 디자인 패턴이 생각났다.
분명 테코블 글에서도 본것 같고 저번에 읽었던 "개발자가 반드시 정복해야할 객체 지향과 디자인 패턴" 책에서도 봤던 것 같다. (사실 자세히 안 읽고 훌렁훌렁 읽었었다 ㅎㅎ)
다시 책을 펴보니 지금 내가 리팩터링 한 방식이 "전략 패턴" 방식으로 구현하는 것이었다. WOW
알고보니 콘텍스트의 클라이언트에서 콘텍스트에 전략을 주입해야줘야하는데 잘못해주고 있었다. 맨 아래까지 읽으면 이해 가능
이에 대해 리뷰어 럿고에게 말씀드리니 다음 블로그를 추천해주셨다.
전략(Strategy) 패턴
전략 패턴은 객체가 할 수 있는 행위 중 유사한 행위들을 클래스로 분리하고 그 클래스들 캡슐화하는 인터페이스를 정의하여 사용하는 것을 말한다. 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장할 수 있게 된다. 이게 무슨말인지 로또 미션에 적용해 생각해보자!! 😄
로또 미션 2단계에는 수동으로 로또를 생성하는 기능과 자동으로 로또를 생성하는 기능이 필요하다.
이 둘 모두 "로또를 생성한다"는 "공통 기능"을 가진다.
하지만 "수동"으로 로또를 생성하는지, "자동"으로 로또를 생성하는지와 같이 "전략"이 다를 뿐이다.
우리는 공통 기능인 "로또를 생성한다"를 "LottoGenerator 인터페이스"로 추상화할 수 있다. -> 전략 (Strategy)
또한 LottoGenerator를 사용해 "로또를 직접 생성하는 기능을 수행"하는 "Lotto 클래스"를 만들 수 있다. -> 콘텍스트(Context)
이렇게 특정 콘텍스트에서 전략을 별도로 분리하고 추상화하는 설계 방식을 "전략 패턴"이라 이해할 수 있다.
그래서 구현은 어떻게 하는데?
전략 패턴에서 콘텍스트는 사용할 전략을 직접 선택하지 않는다.
대신, 콘텍스트의 클라이언트가 콘텍스트에 사용할 전략을 전달해 준다.
다음과 같이 Lotto(콘텍스트)에서는 lottoGenerator.makeLottos()로 로또를 생성할 뿐 어떤 LottoGenerator(전략)을 사용할지는 직접 선택하지 않는다.
Lotto(콘텍스트)에서는 LottoGenerator(전략)의 실제 구현체를 전혀 알 필요 없다는 말과 같다.
따라서 어떤 전략(Fixed/RandomLottoGenerator)을 사용하느냐에 상관 없이 같은 코드를 사용하게 된다.
또 하나 중요한 점은 지금 가지는 두가지 전략 말고도 나중에 다른 전략이 추가되도 Lotto 클래스의 코드를 변경할 필요 없다는 것이다.
public class Lotto {
private final Set<LottoNumber> numbers;
public Lotto(final LottoGenerator lottoGenerator) {
final Set<LottoNumber> lottoNumbers = new HashSet<>(lottoGenerator.makeLottos());
validateDuplicationAndSize(lottoNumbers);
this.numbers = lottoNumbers;
}
}
다음과 같이 Lotto(콘텍스트)의 클라이언트인 LottoMachine에서 제공해야하는 기능에 따라 Fixed/RandomLottoGenerator(전략)을 선택해 Lotto(콘텍스트)에 주입해준다.
private void makeManualLottos(final Lottos lottos, final List<List<Integer>> manualLottos) {
for (List<Integer> manualLotto : manualLottos) {
final LottoGenerator lottoGenerator = new FixedLottoGenerator(manualLotto);
lottos.add(new Lotto(lottoGenerator));
}
}
private void makeAutoLottos(final Lottos lottos) {
final LottoGenerator lottoGenerator = new RandomLottoGenerator();
for (int i = 0; i < autoLottoCount; i++) {
lottos.add(new Lotto(lottoGenerator));
}
}
프로그램의 유연함 상승!!
위에서 계속 봤던 것과 같이 전략 패턴을 사용하면
Lotto 객체를 생성하는 모든 곳에서 new Lotto(lottoGenerator)을 통해 객체를 생성할 수 있다. 혁명적이다. 👍
random, fixed에 따라 lottoMachine에 다른 객체를 생성해서 주입하면 되기 때문에 기능 확장의 유연함이 상승한다.
또 다른 전략을 추가하고 싶으면 LottoGenerator의 구현체만 생성해 Lotto에 주입해주면 된다.
또한 랜덤 생성과 같이 테스트하기 어려운 코드를 쉽게 테스트할 수 있게 된다. 일석이조!!
Ref.
https://tecoble.techcourse.co.kr/post/2020-05-17-appropriate_method_for_test_by_interface/
'Backend > Java' 카테고리의 다른 글
제네릭(Generic)의 기본 개념 (0) | 2022.03.07 |
---|---|
@ParameterizedTest에도 각각의 이름을 부여할 수 있다?? (2) | 2022.03.04 |
디폴트 메서드를 너무 믿지 말고, 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다 (0) | 2022.03.01 |
생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.02.28 |
Unmodifiable Collection (0) | 2022.02.28 |