
Mediator 2. Mediator 와 Visitor 의 조합
이전 Mediator 패턴 글에서는 간단한 채팅 시스템을 구축하여 string 타입의 메시지를 주고받는 예제를 다뤘습니다. 이번 글에서는 Visitor 패턴을 도입하여 다양한 타입의 데이터를 주고받을 수 있는 시스템으로 업그레이드해보려고 합니다. 이를 통해 Mediator 패턴과 Visitor 패턴의 결합이 얼마나 강력한 조합인지 확인할 수 있습니다.
Visitor 패턴을 Mediator 시스템에 적용하기
Visitor 패턴은 객체의 구조는 변경하지 않고, 새로운 연산을 추가하는 데 유용한 디자인 패턴입니다. 이 패턴을 활용하면 다양한 타입의 데이터를 Mediator 시스템을 통해 주고받을 수 있습니다.
코드 예시
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
27
28
29
30
31
32
33
34
35
public interface IVisitor
{
void Visit(IVisitable visitable);
}
public abstract class Payload<TPayload> : IVisitor
{
public abstract TPayload Content {get;}
public abstract void Visit(IVisitable visitable);
public Payload(TPayload content)
{
this.Content = content;
}
}
public class StringPayload : Payload<string>
{
public IVisitable Sender {get;}
public override string Content {get;}
public override void Visit(IVisitable visitable)
{
// 타입 검사 필요
if(visitable is Agent agent)
{
agent.Receive(Content);
}
}
public StringPayload(IVisitable sender, string content) : base(content)
{
this.Sender = sender;
}
}
위 코드는 Payload 클래스를 정의해 Visitor 패턴의 역할을 수행하게 하고, 이를 통해 string 타입의 데이터를 전달하는 StringPayload 클래스를 구현한 모습입니다.
Mediator 시스템에 Visitor 패턴 적용
이제 Visitor 패턴을 기반으로 한 Mediator 시스템을 구성해보겠습니다. Entity와 Agent 클래스는 IVisitable 인터페이스를 구현하여 Visitor가 방문할 수 있도록 합니다.
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public interface IVisitable
{
void Accept(IVisitor visitor);
}
public abstract class Entity : IVisitable
{
protected IMediator mediator;
public Entity(IMediator mediator)
{
this.mediator = mediator;
}
public abstract void Accept(IVisitor visitor);
}
public class Agent : Entity
{
public string Name { get; }
public Agent(IMediator mediator, string name) : base(mediator)
{
this.Name = name;
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
public void Receive(string message)
{
Console.WriteLine($"{Name} received message: {message}");
}
public void Send(string message)
{
Console.WriteLine($"{Name} sends message: {message}");
mediator.Send(this, new StringPayload(this, message));
}
public void Send(IVisitor visitor)
{
mediator.Send(this, visitor);
}
}
Mediator 클래스
Mediator 클래스는 메시지를 중재하며, Visitor 패턴을 통해 객체 간의 상호작용을 관리합니다.
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
public interface IMediator
{
void Send(Entity sender, string message);
}
public class Mediator : IMediator
{
private List<Entity> entities = new List<Entity>();
public void Register(Entity entity)
{
entities.Add(entity);
}
public void Send(Entity sender, IVisitor visitor)
{
foreach (var entity in entities)
{
if (entity != sender)
{
entity.Accept(visitor);
}
}
}
}
시스템 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MediatorOpening
{
void Start()
{
Mediator mediator = new Mediator();
Agent alice = new Agent(mediator, "Alice");
Agent bob = new Agent(mediator, "Bob");
Agent charlie = new Agent(mediator, "Charlie");
mediator.Register(alice);
mediator.Register(bob);
mediator.Register(charlie);
alice.Send("Hello everyone!");
bob.Send("Hi Alice!");
}
}
Generic 기반의 Visitor Mediator 시스템
위에서 구현한 Mediator 시스템은 다양한 타입의 데이터를 처리할 수 있지만, 타입 안정성에 취약한 부분이 있습니다. 이를 해결하기 위해 Generic 기반의 Mediator 시스템으로 업그레이드할 수 있습니다. Generic Mediator는 더 강력한 타입 검사를 통해 런타임 에러를 줄이고, 더 명확한 구조를 제공합니다.
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
27
public interface IVisitor<T> where T : IVisitable
{
void Visit(T visitable);
}
public abstract class Payload<TPayload, T> : IVisitor<T> where T : IVisitable
{
public abstract TPayload Content {get;}
public abstract void Visit(T visitable);
}
public class StringPayload : Payload<string, Agent>
{
public Agent Sender {get;}
public override string Content {get;}
public StringPayload(Agent sender, string content)
{
this.Sender = sender;
this.Content = content;
}
public override void Visit(Agent visitable)
{
visitable.Receive(Content);
}
}
이제 Mediator 클래스도 제네릭으로 설계하여, 특정 타입에만 Mediator 시스템을 적용할 수 있습니다.
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
public abstract class Mediator<T>
{
protected readonly List<T> entities = new();
public void Register(T entity)
{
entities.Add(entity);
}
public abstract void Send(T sender, IVisitor visitor);
}
public class AgentMediator : Mediator<Agent>
{
public override void Send(Agent sender, IVisitor visitor)
{
foreach(var entity in entities)
{
if(!entity.Equals(sender))
{
entity.Accept(visitor);
}
}
}
}
또한, Agent 클래스는 Agent 타입의 Mediator 와 상호작용할 수 있도록 구성합니다.
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
27
28
29
30
31
32
33
34
35
36
public interface IVisitable
{
void Accept(IVisitor visitor);
}
public class Agent : IVisitable
{
public string Name {get;}
Mediator<Agent> mediator;
public Agent(Mediator<Agent> mediator, string name)
{
this.Name = name;
this.mediator = mediator;
}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
public void Receive(string message)
{
Console.WriteLine($"{Name} received message: {message}");
}
public void Send(IVisitor visitor)
{
mediator.Send(this, visitor);
}
public void Send(string message)
{
Console.WriteLine($"{Name} sends message: {message}");
mediator.Send(new StringPayload(this, message));
}
}
두 버전의 차이점
- 설계 방식의 차이
- 첫 번째 버전은 인터페이스 기반으로 더 높은 유연성을 제공하며, 다양한 엔티티가 상호작용할 수 있습니다.
- 두 번째 버전은 제네릭을 사용해 더 강력한 타입 안정성을 제공합니다.
- 유연성 대 타입 안전성
- 인터페이스 기반 설계는 여러 타입을 다룰 수 있는 유연성을 제공하지만, 런타임 타입 검사로 인해 복잡성이 증가할 수 있습니다.
- 제네릭 기반 설계는 컴파일 타임에 타입 안정성을 보장하여, 더 안전한 코드를 작성할 수 있습니다.
- 확장성과 유지보수성
- 인터페이스 기반은 다양한 엔티티를 처리할 수 있는 확장성이 있지만, 타입마다 새로운 메서드를 구현해야 하는 부담이 있습니다.
- 제네릭 기반은 더 안전하지만, 특정 타입에 한정되어 유연성이 떨어질 수 있습니다.
그럼에도 interface 기반의 코드 작성은 설계 단계에서 쓸만하다
1. 유연한 설계 및 빠른 변경
- 인터페이스 기반 설계는 구체적인 구현을 강제하지 않기 때문에, 유연하게 여러 타입의 객체를 다룰 수 있습니다. 이를 통해 프로토타입 단계에서 다양한 시나리오를 실험하고, 변경이 필요한 부분을 쉽게 수정할 수 있습니다.
- 예를 들어,
IVisitable
,IVisitor
같은 인터페이스는 다양한 객체들이 서로 동적 관계를 형성할 수 있도록 하여, 초기 설계에서 기능과 구조를 자유롭게 조정할 수 있습니다.
2. 구현 클래스 변경 용이성
- 인터페이스 기반 설계는 객체의 구현 클래스를 교체하는 것이 매우 쉽습니다. 즉, 특정 객체가 상호작용하는 방식이나 내부 동작을 쉽게 수정하거나 교체할 수 있기 때문에, 여러 가지 방식을 실험하고 테스트하기에 적합합니다.
- 예를 들어,
IMediator
인터페이스를 기반으로 다양한 Mediator 구현 클래스를 만들고, 실시간으로 이 구현 클래스들을 바꾸면서 성능이나 구조의 적합성을 평가할 수 있습니다.
3. 기능 확장 테스트에 유리
- 프로토타입에서는 종종 새로운 기능을 추가하거나, 기존 기능을 확장하는 요구가 발생합니다. 인터페이스 기반의 설계는 기능 추가가 용이하며, 다른 객체들과의 상호작용을 빠르게 시도해볼 수 있습니다.
- 각 객체가 인터페이스만을 참조하기 때문에, 구체적인 구현에 의존하지 않아 다양한 방식으로 확장 테스트가 가능합니다.
4. 코드가 직관적이고 단순
- 인터페이스 기반 설계는 일반적으로 단순하고 직관적이기 때문에 프로토타입 작성 시 빠르게 구조를 잡고, 기능을 구현할 수 있습니다.
- 런타임 타입 검사나 유연한 객체 처리를 통해 구조를 빠르게 잡을 수 있으므로, 전체 시스템의 큰 틀을 구성하는 데 유리합니다.
5. 복잡성 증가 요소를 나중에 도입 가능
- 초기에는 타입 안정성이나 구체적인 구현의 복잡성을 고려하지 않고, 인터페이스 기반으로 전체 구조와 기본적인 흐름을 빠르게 잡을 수 있습니다.
- 이후 실제 프로젝트로 전환하거나 프로토타입을 다듬는 과정에서 타입 안전성을 강화하기 위해 Generic 방식으로 전환하거나, 타입 검사를 줄이는 방향으로 개선할 수 있습니다.
마무리
두 버전 모두 각각의 장단점이 있으며, 인터페이스 기반은 더 유연한 설계를 제공하지만, 제네릭 기반은 더 안전하고 간결한 코드를 작성할 수 있게 합니다. 타입 안정성을 보장하고자 할 때는 제네릭 기반의 Mediator 설계가 적합하며, 설계 단계나 다양한 객체 간의 상호작용이 필요한 경우에는 인터페이스 기반이 더 유리할 수 있습니다.