미니언 생성으로 팩토리 패턴 알아보기
오늘도 어김없이 롤을 예시로 들어보자.
여러분은 롤 클라이언트 개발팀에서 미니언쪽 부서에서 일하게 되었다.
1분 30초가 되면 미니언을 생성함과 동시에 일정 간격마다 계속 미니언을 생성해야 한다.
미니언들은 아마 이런 특성을 가지고 있을것이다.
미니언들의 특성
고유 특성
1. 근거리
2. 원거리
3. 대포
공통 특성
1. 걷는다
2. 공격한다
미니언 객체를 생성하기 위해서 코드 어디서든 이 미니언 클래스의 인스턴스를 new 해줘야 할것이다.
간단히 코드로 나타내면 다음과 같다.
class MinionSpawner
{
public Minion GetMinion(string type)
{
Minion minion;
if(type == "근거리")
minion = new ShortRangeMinion();
else if(type == "원거리")
minion = new LongRangeMinion();
else if(type == "대포")
minion = new CannonMinion();
minion.StartRunning();
minion.StartAttacking();
return minion;
}
}
abstract class Minion
{
public void StartRunning() { print("달리기 시작"); }
public void StartAttacking() { print("공격 시작"); }
}
class ShortRangeMinion : Minion {}
class LongRangeMinion : Minion {}
class CannonMinion : Minion {}
공통인 특성인 걷는것과 공격하는것을 부모 클래스인 Minion 클래스에 넣어놔서
모든 미니언들이 공통인 특성을 가지도록 하였다.
간단한 문제점
실제로 위와 같은 코드를 나도 개인 프로젝트할 때 많이 썼다.
쓰다보면 느끼는 문제점은 항상
미니언이 추가되거나, 삭제, 변경이 일어날 때
미니언 클래스 뿐만 아니라 미니언을 생성하는 GetMinion 메소드도 같이 수정을 해줘야 된다는 점이다.
또 미니언 생성을 GetMinion 메소드 말고 다른곳에서도 할 수 있기 때문에
우리는 미니언 생성하는 로직을 SomeClass에서 떼어 내 캡슐화 하기로 했다.
팩토리 패턴의 등장
팩토리 패턴은 이름에서 알 수 있듯이 팩토리는 무언가를 찍어내는 공장이다.
우리는 미니언을 찍어내는 공장을 만들어 캡슐화를 해야하므로
코드를 다음과 같이 바꿔보자.
class MinionSpawner
{
MinionFactory factory;
public MinionSpawner(MinionFactory _factory)
=> factory = _factory;
public Minion GetMinion(string type)
{
Minion minion = factory.CreateMinion(type);
minion.StartRunning();
minion.StartAttacking();
return minion;
}
}
class MinionFactory
{
public Minion CreateMinion(string type)
{
Minion minion;
if(type == "근거리")
minion = new ShortRangeMinion();
else if(type == "원거리")
minion = new LongRangeMinion();
else if(type == "대포")
minion = new CannonMinion();
return minion;
}
}
미니언을 만들어 내는 공장인 MinionFactory 클래스를 새로 만들었고
생성된 미니언을 받아야 하는 MinionSpawner 클래스에는 MinionFactory 인스턴스 변수를 가져야 한다.
가지고 있는 인스턴스 변수에게 미니언 타입을 넘겨주어 생성된 미니언을 받는다.
미니언을 생성하는 코드는 다음과 같이 해주면 되겠다.
MinionFactory factory = new MinionFactory();
MinionSpawner spawner = new MinionSpawner(factory);
Minion minion = spawner.GetMinion("근거리"); // 근거리 미니언 생성!
이렇게 코드를 쓰고 난 후 위에 썼던 문제점을 다시 보면
미니언 클래스 뿐만 아니라 미니언을 생성하는 GetMinion 메소드도 같이 수정을 해줘야 된다는 점이다.
이 문제는 해결되었다.
이제 다른 클래스에서 미니언을 생성하려면 MinionFactory를 거쳐서만 생성할 수 있으므로,
미니언 코드에 변경이 일어나도 해당 미니언과 MinionFactory의 코드만 고치면 된다.
팩토리 메소드를 static으로 만들어도 되지 않나?
미니언을 만들기 위해서 굳이 MinionFactory의 인스턴스를 만들지 않고
static으로 CreateMinion만 가져오면 되지 않나? 라고 생각할 수 있다.
그래도 된다. 그게 제일 간단한 구현법이지만, 서브클래스를 만들어 CreateMinion의
행동을 변경시킬 수 없다는 단점이 있다.
여기까지가 일반적으로 소개되는 Simple Factory Pattern이다.
객체 생성과 초기화, 동작을 한 부분에서 다 총괄하는게 아닌
생성을 캡슐화해 OOP에 걸맞게 재구성 하는것이다.
하지만 더 깊게 알아보자.
겨울옷을 입은 미니언들의 등장
지금까지 만들었던 미니언들은 전부 기본 코스튬의 미니언이었다.
곧 다가올 겨울을 대비해 겨울옷을 입은 미니언들도 만들어야 된다고 기획이 날라왔다.
우리는 이미 기본 코스튬 미니언 팩토리를 만들어 놓았기에
단순히 겨울 코스튬 미니언 팩토리를 추가 하기로 했다.
NormalMinionFactory factory = new NormalMinionFactory();
MinionSpawner spawner = new MinionSpawner(factory);
Minion minion = spawner.GetMinion("근거리"); // 기본 근거리 미니언 생성!
WinterMinionFactory factory = new WinterMinionFactory();
MinionSpawner spawner = new MinionSpawner(factory);
Minion minion = spawner.GetMinion("근거리"); // 겨울 근거리 미니언 생성!
문제없이 작동하지만, 매번 MinionSpawner와 코스튬 별 MinionFactory를 연결지어주는 일은
코드의 양이 길어져 조금 별로 같이 보인다.
MinionSpawner를 코스튬 별로 만들고, Factory를 아예 해당 서브클래스에 넣도록 하자.
이러면 MinionSpawn 과정과, MinionCreate 과정을 한 클래스안에 묶을 수 있어 보기가 좋아진다.
MinionSpawner 클래스 추상화 하기
MinionSpawner를 Abstract화 해서 더 이상 단독으로 인스턴스 생성이 불가능 하도록 하겠다.
이러면 그 다음 수순으로는 자연스럽게 코스튬 별 MinionSpawner 클래스를 만들어 줘야 된다.
abstract class MinionSpawner
{
public Minion GetMinion(string type)
{
Minion minion = CreateMinion(type);
minion.StartRunning();
minion.StartAttacking();
return minion;
}
abstract Minion CreateMinion(string type); // CreateMinion 추상화
}
class NormalMinionSpawner : MinionSpawner
{
public Minion CreateMinion(string type)
{
Minion minion;
if(type == "근거리")
minion = new NormalShortRangeMinion();
else if(type == "원거리")
minion = new NormalLongRangeMinion();
else if(type == "대포")
minion = new NormalCannonMinion();
return minion;
}
}
class WinterMinionSpawner : MinionSpawner
{
public Minion CreateMinion(string type)
{
Minion minion;
if(type == "근거리")
minion = new WinterShortRangeMinion();
else if(type == "원거리")
minion = new WinterLongRangeMinion();
else if(type == "대포")
minion = new WinterCannonMinion();
return minion;
}
}
팩토리가 사라졌다
MinionSpawner를 추상화 하면서 여러 코스튬 별 MinionSpawner를 만들었다.
CreateMinion을 추상 메소드로 선언하면서 여러 코스튬 별 MinionSpawner들은
팩토리가 사라져도 각자 코스튬에 맞게 CreateMinion을 잘 수행해 낼 것을 약속하였다.
따라서 기능적으로 우리가 구현하고자 했던 팩토리 패턴과 일치한다.
바뀐 Spawn 과정
MinionSpawner normalMinionSpawner = new NormalMinionSpawner();
Minion minion = normalMinionSpawner.GetMinion("근거리"); // 기본 근거리 미니언 생성
MinionSpawner winterMinionSpawner = new WinterMinionSpawner();
Minion winterMinion = winterMinionSpawner.GetMinion("근거리"); // 겨울 근거리 미니언 생성
무엇을 눈여겨 봐야 할까?
1. MinionSpawner의 GetMinion 메소드는 자신이 다루는 Minion의 종류를 알 필요가 없다.
천천히 살펴보자. GetMinion 메소드는 무조건 Minion 객체를 return 해야 한다.
이때 return 해야 할 Minion의 객체를 new 키워드로 새롭게 할당하는것이 아닌
CreateMinion 메소드에 type을 던져주어 반환된 Minion을 할당한다.
물론 CreateMinion 메소드에서는 당연히 new 키워드로 객체를 생성해 주어야 겠지만,
중요한점은 GetMinion은 Create된 Minion이 어떤 종류인지 알 필요가 없다.
그저 StartRunning과 StartAttacking만 해주면 된다.
따라서 MinionSpawner 클래스와 Minion 클래스는 서로 완전히 분리되어 있다고 볼 수 있다.
겨울 코스튬이든, 기본 코스튬이든 약속한 일만 해내면 되는것이다.
2. 어느순간 의존성이 뒤바꼈다.
본문의 맨 처음 코드이다.
class MinionSpawner
{
public Minion GetMinion(string type)
{
Minion minion;
if(type == "근거리")
minion = new ShortRangeMinion();
else if(type == "원거리")
minion = new LongRangeMinion();
else if(type == "대포")
minion = new CannonMinion();
minion.StartRunning();
minion.StartAttacking();
return minion;
}
}
MinionSpawner에서 type을 바탕으로 Minion의 종류를 결정한다.
이를 도식화하면 다음과 같다.
팩토리 패턴으로 바뀐 코드이다.
abstract class MinionSpawner
{
public Minion GetMinion(string type)
{
Minion minion = CreateMinion(type);
minion.StartRunning();
minion.StartAttacking();
return minion;
}
abstract Minion CreateMinion(string type); // CreateMinion 추상화
}
MinionSpawner는 type을 가지고 있긴 하지만 현재 다루고 있는 Minion을 먼저 알지 못한다.
CreateMinion으로 type에 맞는 Minion이 return 되어야 그 때부터 알 수 있다.
이를 도식화 하면 다음과 같다.
순서가 바뀐것을 볼 수 있다.
여기서 의존성이란, 어떤 클래스가 다른 클래스에게 얼마나 의존적이냐를 뜻하는것인데,
위의 그림들을 보면 쉽게 이해할 수 있을 것이다.
Dependency Inversion
Dependency Inversion(DI) : 구현 클래스(Minion)에 대한 의존성을 뒤집는다.
한번쯤은 들어봤을 단어로 Spring과 같은 프레임워크에서 중요하게 생각하는 원칙이다.
다른 말로는 추상화된 클래스가 아닌 구현된 클래스에게 의존하면 안되고, 추상화된 클래스에게
의존적인 코드를 짤 수록 좋다라는 뜻이다.
이렇게 의존성이 반대로 되면 어떤점이 좋을까?
위에 예시로 보여줬듯이, MinionSpawner는 Minion의 종류에 대해 알 필요없이 추상화된 클래스인
Minion클래스로 묶어서 미니언이라면 당연히 해야될일만 처리한다.(걷기, 공격하기)
결론적으로는 코드간의 결합도를 떨어트려 가독성이 좋아지며 유지보수하기에 편해진다.
'Programming > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 6. 커맨드 패턴 (0) | 2022.03.22 |
---|---|
[디자인 패턴] 5. 싱글턴 패턴 (0) | 2022.03.22 |
[디자인 패턴] 3. 데코레이터 패턴 (0) | 2022.03.17 |
[디자인 패턴] 2. 옵저버 패턴 (0) | 2022.03.10 |
[디자인 패턴] 1. 스트래티지 패턴 (0) | 2022.03.10 |