WPF 독학 중입니다. 소켓 통신 관련 된 부분을 어떻게 처리하는 것이 좋은 방법일까요.

GPT와 함께 매일 조금씩 WPF 독학 중에 있습니다.

mvvm 적용하면서 소켓 생성/종료, 데이터 송수신 등을 뷰모델에서 작성하려다 뷰모델이 너무 비대해지는 것 같아 어떻게 처리하는 것이 좋은 방법일지 고민이 됩니다.

자료를 찾다보면 종속성 주입으로 보통 모델 또는 뷰를 서비스를 통해 사용하는게 자주 보입니다.
소켓도 뷰모델에서 종속성 주입으로 주입 받아 사용하는 방법도 괜찮을지요?

주변에 사수나 스승님 들이 없어 막연함에 질문을 올려봅니다.
있다면 이것저것 물어볼텐데 없네요.

3개의 좋아요

이 문제는 종속성 주입보다는 책임 분리 관점에서 접근하는 법을 트레이닝 해보시는 게 좋을 것 같습니다.

MVVM 패턴을 처음 접할 때 자주 하는 실수가 View에 있어야 할 것도 ViewModel에 넣거나, 반대로 Model이나 Service에 있어야 할 것들을 ViewModel에 넣는 것이라고 생각하는데요. 저도 그랬고요…

이 경우 후자의 케이스입니다. 뷰 모델에는 뷰의 상태와 명령을 추상화한 로직을 제외하고 다른 로직이 포함되지 않아야 합니다.

당장 떠오르는 것은 소켓 통신 로직을 별도의 클래스로 분리하고, 뷰모델에는 원칙대로 상태 변화를 표시하거나 명령을 트리거하는 로직만 남기는 것입니다.

예를 들어 소켓 통신에 관한 로직을 분리한다고 가정하면, 인터페이스를 대충 다음과 같이 정의할 수 있을 것입니다.

public interface ISocketClient
{
    event EventHandler<byte[]>? DataReceived;

    bool IsConnected { get; }

    Task ConnectAsync(string host, int port, CancellationToken cancellationToken = default);

    Task DisconnectAsync(CancellationToken cancellationToken = default);

    Task SendAsync(byte[] data, CancellationToken cancellationToken = default);
}

그렇다면 뷰모델에서는 UI 입력에 따라 SocketClient 객체의 메서드를 호출하고 상태 변화만 반영해주면 됩니다.

public partial class SocketViewModel : ObservableObject
{
    private readonly ISocketClient _client;

    [ObservableProperty]
    private bool _isConnected;

    [ObservableProperty]
    private string? _host;

    [ObservableProperty]
    private int _port;

    public SocketViewModel()
    {
        _client = new MySocketClient();

        _client.DataReceived += OnDataReceived;
    }

    public ObservableCollection<string> Messages { get; } = [];

    [RelayCommand]
    private async Task ConnectAsync(CancellationToken cancellationToken = default)
    {
        await _client.ConnectAsync(Host, Port, cancellationToken);
        IsConnected = _client.IsConnected;
    }

    [RelayCommand]
    private async Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        await _client.DisconnectAsync(cancellationToken);
        IsConnected = _client.IsConnected;
    }

    [RelayCommand]
    private async Task SendAsync(string message, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(message) || !IsConnected)
            return;
        var data = Encoding.UTF8.GetBytes(message);
        await _client.SendAsync(data, cancellationToken);
        Messages.Add($"Sent: {message}");
    }

    private void OnDataReceived(object? sender, byte[] e)
    {
        // 필요 시 프로토콜 구현에 따라 e를 파싱
        string message = Encoding.UTF8.GetString(e);
        Messages.Add($"Received: {message}");
    }
}

뷰모델 코드를 보시면 알겠지만 종속성을 주입하지 않았습니다. 단지 생성자에서 ISocketClient의 구현체인 MySocketClient의 객체를 초기화해 변수에 할당했을 뿐입니다. 그럼에도 불구하고 필수 코드만 남아 어떤 동작을 하는지 한 눈에 확인할 수 있습니다.

9개의 좋아요

이 문제에서 종속성 주입은, '뷰 모델이 내가 원하는 통신 방식이 TCP냐 UDP냐, 혹은 나중에 추가될 프로토콜이냐에 관계없이 소켓 통신을 처리하게 만들 거야’를 해결하기 위해 도입된다고 보시면 됩니다.

7개의 좋아요

Socket 이라는 것 자체가 외부 자원(통신 I/O)에 대한 접근이라서, MVVM 중에 하나가 아니고, 이들이 의존하는 "서비스"로 취급합니다.

Socket 은 통신 I/O 의 기본 객체이기 때문에, HttpCient, WebClient, DatabaseConnection 등에서 사용되기도 합니다. 이 객체를 사용해보셔서 아시겠지만, 이 객체들은 Socket 을 외부로 노출하지 않습니다. 이와 유사하게, Socket 을 직접 노출하는 것 보다, 그 용도에 맞게 캡슐화하는 것이 좋습니다. 예를 들면,

public interface IMachineStatusService {}
public class SokcetBasedMachineStatusService : IMachineStatusService { }
class MyViewModel(IMachineStatusService service) { }

IMachineStatusService 는 직접 생성할 수도 있고,

partial class MyView
{
   public MyView()
   {
       DataContext = new MyViewModel(new SokcetBasedMachineStatusService());

서비스 컨테이너를 통해 자동으로 주입되도록 할 수도 있습니다.

services.AddTransient<IMachineStatusService, SokcetBasedMachineStatusService>();
services.AddTransient<MyView>();

이 서비스를 반드시 뷰모델이 소비해야 하는 것은 아닙니다.
보통은 뷰가 소비하는 것이 자연스럽니다.

왜냐하면 뷰는 보통 생애 주기 메서드를 제공하는데, time-consuming (비동기) 작업의 호출 장소로 최적이기 때문입니다. (생성자에서 이런 작업을 수행하는 것은 좋지 않습니다.)

이 경우, 뷰모델은 서비스가 제공하는 데이터만 주입받는 구조가 됩니다.

class MyViewModel(IEnumerable<MachineStatus> stata) { }

데이터를 주입받을 때는 MyViewModel 은 서비스 컨테이너에 등록하는 것이 자연스럽지 않습니다.

8개의 좋아요

저는 WPF를 입문할 단계에 MVVM을 적용해서 입문하는 것은 반대하는 편이긴 합니다. 적어도 프로젝트를 2개 정도는 MVVM 아닌걸로 시작해보고 하는걸 좋다고 생각하는 편입니다.

WPF를 MVVM으로 시작하기에는 WPF 자체적인 기능을 놓칠 때가 있습니다. VisualStateManager 라던지 Visual Tree랑 Logical Tree의 차이는 뭔지 같은 WPF의 기본 Control 상속 구조입니다.

Dependency Property는 뭐고, DispatcherObject는 뭐고 같은 이런 Visual 자체의 기능을 제쳐두고 MVVM부터 시작하는 것은 오히려 나중에 고생할 수 있는 부분이라고 생각합니다.

이 답변은 질문과 무관한 답변이지만 다른 분들이 좋은 답변들은 올려주셨기에 참고하시라고 의견만 전달드립니다.

6개의 좋아요

저는 @Vincent 님과 비슷한 관점에서 WPF를 막 시작한 상태에서 MVVM을 도입하는 것은 나쁘지 않다고 생각합니다.

대신 DI를 적용하는 건 추천하지 않습니다. DI는 단순히 객체 생성을 컨테이너에 맡기는 기술이 아니라, 객체지향 설계 원칙과 의존성 역전 같은 것들에 대한 이해를 전제로 하기 때문에 이걸 제대로 고민할 수 있는 능력이 없다면 DI는 그저 형식(형태 흉내내기)만 남고 코드 복잡도만 늘어납니다.

WPF 학습 초반에는 억지로 인터페이스를 만들거나 분리하는 대신, MVVM 구조 자체에 집중 하는 것이 우선일 듯합니다. 지금과 같이 ViewModel이 복잡해질 조짐이 보일때 서비스로 분리하는 방법 은 고민하는 단계로 넘어가는 적절한 시기인 것이죠.
지금도 처음부터 추상화에 집착하기보다, 구체 클래스를 통해 먼저 구조를 안정시키고 이후 확장 가능성이 뚜렷해질 때 필요한 만큼만 추상화를 적용하는 편이 바람직할 것 같습니다.

6개의 좋아요

뷰가 생애 주기 메서드를 제공 이라는게 와닿지는 않는데 검색 및 공부를 조금 더 해보겠습니다.
감사합니다.

4개의 좋아요

WPF 뿐만 아니라, 닷넷의 모든 UI 프레임워크(와 다른 언어의 프레임워크)는 생애 주기 메서드 - 이벤트 핸들러를 제공합니다.

각 프레임워크가 추구하는 방향에 따라 약간 씩 차이가 있을 뿐입니다.

Object lifetime events - WPF | Microsoft Learn

App lifecycle - .NET MAUI | Microsoft Learn

ASP.NET Core Razor component lifecycle | Microsoft Learn

4개의 좋아요