유사한 객체들에 대한 공통 관심사를 인터페이스를 통해 설계하는 디자인 패턴.
다시 말해, 비슷한 객체들의 공통 행위를 모아 인터페이스를 만들고, 그것을 객체들로 구현화 하는 작업을 뜻합니다.
많은 이유가 있겠지만 제가 생각하는 가장 큰 이유는 유지 보수에 대한 비용을 줄이기 위해서라고 생각합니다.
전략 패턴을 처음 알게되었을 때 가장 먼저 떠오른 예시는 리그오브레전드 였습니다. 그래서 리그오브레전드로 예시를 들려합니다. (롤을 해보지 않았다면 죄송ㅠㅠ) 리그오브레전드 개발자가 되어 이 챔피언들을 개발한다고 가정하겠습니다.
전략 패턴을 몰랐다면 모든 챔피언 각기 다른 클래스로 정의하고 구현했을 것 같습니다. 챔피언 서로 간의 연관점이 없는 완전히 다른 객체로 말이죠.
하지만 무엇인가 어색하지 않은가요? 챔피언들의 공통점이 있지 않나요?
저는 스킬이 4개인 점과 모든 챔피언이 패시브를 가지고 있다는 점이 공통점이라고 느꼈습니다. 스킬이 4개이다.
와 패시브가 있다.
두 가지를 모든 챔피언의 공통 사항으로 볼 수 있는데, 왜 모든 챔피언을 그저 다른 객체라고 두어야할까요?
이 의문에서 나온 것이 전략패턴이라고 생각합니다. 전략 패턴은 챔피언마다의 공통 관심사를 모아줍니다. 제가 제시한 스킬이 4개이다.
와 패시브가 있다.
를 공통 관심사로 보고 이 두 가지 특징을 챔피언
이라는 인터페이스에 정의하고 각각의 챔피언에 해당하는 스킬은 그 챔피언의 클래스에 구현하는 방식이죠.
인터페이스를 통한 정의를 구현체로 만드는 것이 처음부터 끝까지 구현하는 것보다 비용이 적지 않을까요? 또, 패시브가 없어진다고 했을 때 인터페이스의 추상 메서드만 지우면 되기 때문에 수정에 더 용이하지 않을까요?
저는 이런 이유때문에 전략 패턴을 사용한다고 생각합니다.
지금껏 제가 프로젝트에서 구현했던 코드들을 보았을 때, 소셜 로그인이 전략 패턴이 적용되어야하는 부분으로 보였습니다. 처음에는 각기 다른 class에서 각자의 로그인 기능을 구현하였습니다. 하지만 전략 패턴이 적용된다면 아래의 방법처럼 공통 관심사인 login()
을 모아 각기 다른 구현체로 코드를 작성할 수 있습니다.
interface SocialLogin {
login(socialType: string): string;
}
class AppleLogin implements SocialLogin {
login(socialType: string): string {
// 구현
}
}
class KakaoLogin implements SocialLogin {
login(socialType: string): string {
// 구현
}
}
class NaverLogin implements SocialLogin {
login(socialType: string): string {
// 구현
}
}
class GoogleLogin implements SocialLogin {
login(socialType: string): string {
// 구현
}
}
타입스크립트의 코드이지만 다른 언어를 쓰셨더라도 충분히 이해할 수 있는 부분이라고 생각해 코드에 대한 설명은 생략하겠습니다.
1주차 미션 자동차 경주 미션
의 규칙 중에 "전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다." 라는 조건이 나왔습니다.
저는 이 부분에서 숫자를 발급받는 방법을 전략 패턴으로 설계하였습니다. 전략 패턴으로 설계한 가장 큰 이유는 랜덤 숫자에 대한 테스트입니다. 전진하기를 원하는 테스트 코드에서 4이상의 숫자가 나올 때까지 기다리는 방법이 비효율적이라고 생각했습니다. 원하는 숫자를 바로 발급할 수 있는 방법을 통해 테스트를 진행할 수 있어야 한다고 느꼈습니다. 이를 해결하는 방법은 프로덕션 코드에서 랜덤 숫자 발급기를 구현하여 랜덤 숫자 생성기에 문제가 없도록 만들고, 테스트 코드에서는 평범한 숫자 발급기를 구현하여 원하는 숫자를 발급받아 테스트를 용이하게 할 수 있도록 만드는 것이라고 생각했습니다.
그래서 저는 숫자 발급기라는 인터페이스를 만들었습니다.
public interface NumberGenerator {
int generate();
}
숫자 발급기에 숫자를 발급해주는 하나의 메서드만 존재하고, 이를 위한 구현체 클래스는 프로덕션 코드와 테스트 코드에 각각 다르게 만들어주었습니다.
프로덕션 코드는 랜덤 숫자를 발급하기 때문에 아래와 같이 랜덤 숫자 발급기로 구현하였습니다.
public class RandomNumberGenerator implements NumberGenerator {
private final int minimumNumber;
private final int maximumNumber;
public RandomNumberGenerator(final int minimumNumber, final int maximumNumber) {
this.minimumNumber = minimumNumber;
this.maximumNumber = maximumNumber;
}
@Override
public int generate() {
return (int) (Math.random() * maximumNumber - minimumNumber + 1) + minimumNumber;
}
}
그리고 테스트를 위한 테스트 숫자 발급기는 아래와 같이 구현했습니다.
public class TestNumberGenerator implements NumberGenerator {
private final int number;
TestNumberGenerator(int number) {
this.number = number;
}
@Override
public int generate() {
return number;
}
}
인터페이스에서 숫자를 발급받는 메서드의 구현체를 각기 다르게 만들어주어 프로덕션과 테스트 상황에 맞게 사용할 수 있도록 했습니다.
전략 패턴의 필요성에 대해서는 공감합니다. 하지만 인터페이스의 목적이 과연 전략 패턴이 아닐까요? 왜 굳이 "전략 패턴"이라는 디자인 패턴으로 이름을 지었을까요? 인터페이스를 이용한 또다른 디자인 패턴인 커맨드 패턴과 구분하기 위해서일까요? 저는 이 질문에 대답하지 못했습니다. 아직 전략 패턴의 의도에 대한 이해가 부족하기 때문이겠지요. 조금 더 공부해보겠습니다...
너무 이해가 잘 되는 글이네요!