이벤트로 레거시 코드 풀기



레거시 코드는 일반적으로 다양한 개념과 추상화 수준을 혼합하는 긴 방법이 특징입니다. 이러한 맥락에서 동작을 적절한 모듈로 추출하는 것은 어려울 수 있습니다. 모든 것이 모든 것에 의존하는 것처럼 느껴지고 전체 코드베이스를 대대적으로 점검해야 하기 때문입니다. 이 문서에서는 기존 코드를 추출하거나 새 코드를 추가하기 위해 레거시 코드와 적절하게 바인딩된 다른 모듈 간의 결합을 줄이기 위해 이벤트를 사용할 것을 제안합니다.

UserManager 메서드를 특징으로 하는 일반적인 CreateUser 클래스가 있다고 가정해 보겠습니다. 사용자가 생성되면 비밀번호를 설정하고 어딘가에서 캐시를 지우기 위해 환영 이메일을 보낼 수 있습니다. 초기 코드는 다음과 같습니다.

public class UserManager {
    // ...

    public async Task CreateUser(string username) {
        var user = new User { Username = username };
        int userId = await _repository.Add(user);
        await ClearCacheForUser(userId);
        await SendWelcomeEmail(userId, user);
    }

    private async Task ClearCacheForUser(int userId) {
        // ...
    }

    private async Task SendWelcomeEmail(int userId, User user) {
        // ...
    }
}


첫 번째 단계는 ClearCacheForUserSendWelcomeEmail를 전용 클래스로 추출하는 것입니다. 그러나 근본적으로 문제를 바꾸지는 않을 것입니다. 실제로 CreateUser 메서드는 여전히 캐시를 지우고 이메일을 보내고 사용자 생성 시 발생해야 하는 다른 모든 프로세스를 알아야 합니다. 따라서 커플 링은 여전히 ​​존재합니다.

이 결합을 제거하는 간단한 솔루션은 이벤트를 사용하는 것입니다. 눈살을 찌푸리기 전에 이벤트를 사용하는 것이 반드시 이벤트 스토어 등으로 완전한 이벤트 소싱 아키텍처를 구현하는 것은 아니라는 점에 유의해야 합니다(이벤트 기반 및 이벤트 소싱은 two separate things ). 사실, 매우 간단한 프로세스 내 이벤트 버스로 시작하면 레거시 코드를 푸는 데 큰 도움이 될 수 있습니다.

다음 코드는 사용할 수 있는 프로세스 내 이벤트 버스의 완전한 구현입니다.

public abstract class MyEvent {}

public interface IEventBus
{
    Task Publish<T>(T @event) where T : EpEvent;

    void Subscribe<T>(IEventHandler<T> handler) where T : MyEvent;
}

public interface IEventHandler<in T> where T : MyEvent
{
    Task Handle(T @event);
}

public class InProcessEventBus : IEventBus
{
    private readonly IDictionary<Type, IList<object>> _subscriptions = new Dictionary<Type, IList<object>>();

    public async Task Publish<T>(T @event) where T : MyEvent
    {
        if (_subscriptions.TryGetValue(typeof(T), out IList<object> handlers))
        {
            foreach (IEventHandler<T> handler in handlers.OfType<IEventHandler<T>>())
            {
                    await handler.Handle(@event);
            }
        }
    }

    public void Subscribe<T>(IEventHandler<T> handler) where T : MyEvent
    {
        if (!_subscriptions.ContainsKey(typeof(T)))
        {
            _subscriptions[typeof(T)] = new List<object>();
        }

        _subscriptions[typeof(T)].Add(handler);
    }
}


이 최소한의 이벤트 버스를 사용하여 CreateUser 메서드를 다음과 같이 다시 작성할 수 있습니다.

public class UserCreatedEvent : MyEvent {
    public int UserId { get; }
    public string Username { get; }

    public UserCreatedEvent(int userId, string username) {
        UserId = userId;
        Username = username;
    }
}

public class UserManager {
    // ...
    private readonly IEventBus _eventBus;

    public async Task CreateUser(string username) {
        var user = new User { Username = username };
        int userId = await _repository.Add(user);

        await _eventBus.Publish(new UserCreatedEvent(userId, username));
    }
}


이제 핸들러를 생성하여 이를 처리하고UserCreatedEvent 필요한 작업을 수행할 수 있습니다.

public class ClearUserCacheEventHandler : IEventHandler<UserCreatedEvent>
{
    public ClearUserCacheEventHandler(IEventBus eventBus)
    {
        eventBus.Subscribe(this);
    }

    public Task Handle(UserCreatedEvent @event)
    {
        // Clear cache
    }
}

public class SendWelcomeEmailEventHandler : IEventHandler<UserCreatedEvent>
{
    public SendWelcomeEmailEventHandler(IEventBus eventBus)
    {
        eventBus.Subscribe(this);
    }

    public Task Handle(UserCreatedEvent @event)
    {
        // Send welcome email
    }
}


이 이벤트 버스는 이벤트가 동기식으로 처리되고 Publish 메서드는 모든 이벤트가 처리된 후에만 반환하므로 확장성에 도움이 되지 않습니다. 그러나 코드베이스 내에서 결합을 크게 줄일 수 있으며 아키텍처를 보다 이벤트 중심으로 만들기 위한 디딤돌 역할을 할 수 있습니다. 또한 더 나아가야 하는 경우(예: 이벤트를 통해 통신하는 두 서비스) out-of-process 구현을 위해 InProcessEventBus를 전환할 수 있는 추상화가 이미 있습니다.

좋은 웹페이지 즐겨찾기