Project Detail
SMAT-mi Ver.2
SMAT-mi Ver.2 — Modern Inverter Console
C#, WPF MVVM 구조와 Modbus RS-485 통신을 결합해 멀티 인버터를 동시에 제어하고, 실시간 차트, 게이지 데이터를 시각화하는 현장 도구를 만들었습니다.
- WPF 기반 윈도우 프로그래밍
- MVVM 패턴 기반 UI 구축
- 스레드 최적화
- Modbus RS-485 기반 통신 구축
- 인버터 모터 이상징후 감지 및 알림
- 인버터 모터 구동 스케줄링
WPF 기반으로 완전히 재설계한 인버터 제어, 모니터링 프로그램입니다. MVVM 패턴과 최신 UI 라이브러리를 적용해 SMAT-mi Ver.1의 한계를 해결했습니다.
UI/UX
WPF MVVM 리뉴얼
구형 WinForm UI를 MVVM 패턴 위에서 재구성해 다중 창/패널 구조와 상태 관리를 체계화했습니다.
WPF, MVVM, Data Binding 개선
DASHBOARD
실시간 모니터링 대시보드
Live Charts, DevExpress Gauge로 전류/토크/온도 데이터를 실시간으로 시각화하고 다중 인버터 비교가 가능해졌습니다.
Live Charts, DevExpress Gauge, Theme
COMM
확장형 통신 인프라
Modbus RS-485 Half-duplex 통신에 스레드 신호 정리와 재시도 큐를 적용해 안정성과 확장성을 확보했습니다.
NModbus4, Mutex sync, Retry Queue
SMAT-mi v2 — 실시간 2D 차트 모니터링
1 / 7프로젝트 개요
아주 오래된 도구 WinForms를 기반으로 만들어진 구 버전 SMAT-mi Ver.1은 최신 UI를 적용하기에 제한이 많아 심미적으로 좋지 않은 UI를 제공해야 했습니다. 이뿐만 아니라 프로젝트에 구조화된 패턴 또는 아키텍처가 적용되지 않아 UI와 Model 간 제한 없이 사방에서 서로 호출하는 코드 또한 산재해 있었습니다. 이러한 단점들을 보완하기 위해 새롭게 리뉴얼 한 프로그램이 바로 SMAT-mi Ver.2입니다. 버전 2는 비교적 최근 출시한 WPF 도구이고 최신 UI 라이브러리가 많아 선택지가 다양해 인버터 프로그램 성격에 맞는 UI를 선별해 적용했습니다. 또한, WPF 제작사인 마이크로소프트는 기본적으로 MVVM 패턴을 적용하는 것을 권고하므로 WPF MVVM 패턴 Best Practice를 참고하여 프로그램을 설계하고 구축했습니다.
기술 스택
개발에 활용한 언어는 C#이며, .NET 프레임워크 기반 WPF 도구를 통해 UI(XAML)와 비즈니스 로직을 구현했습니다. 모드버스 통신은 NModbus4, 그래픽 차트는 Live Charts, 그래픽 게이지는 DevExpress, 레이아웃은 AvalonDock, 로깅은 log4net 으로 진행했습니다.
주요 책임 및 성과
- 오픈소스 프로젝트 AvalonDock을 적용해 도킹 패널 시스템 구현 및 UX 개선
- Live Charts(2D 실시간 차트 라이브러리), DevExpress(UI 툴)를 활용한 UI/UX 개선
- 스레드 풀을 활용해 UI의 부드러운 동작 보장
- MVVM 패턴을 적용해 코드 구조 개선
- 통신 클래스에 스레드 신호정리를 적용해 일관성 있는 통신을 수행하도록 하여 신뢰할 수 있는 데이터 제공을 보장
- 전역에서 단일 개체로 다뤄야 하는 클래스들에 Singleton Pattern 적용
- Observer Pattern을 적용해 외부 통신 응답 시 특정 View UI 업데이트 기능 구현
- 모드버스 통신 클래스의 enqueue 작업에 스레드 신호정리를 적용해 일관성과 신뢰성 있는 통신 보장
배운 점
디자인 패턴 적용 경험(Observer, Singleton)
신입으로서 처음으로 디자인 패턴을 적용해 볼 수 있었습니다. 패턴 중 Observable과 Singleton을 적용시켜 보았습니다. Observable Pattern을 적용시킨 이유를 먼저 말씀 드리자면, PC 프로그램을 개발하면 네트워크 통신 객체를 통해 들어오는 패킷 데이터를 여러 윈도우 창에 분산돼 있는 UI에 전달해 일괄 업데이트 해야 했던 상황이 있었습니다. 이때 단순히 if-else 문을 통해 각 윈도우 창을 찾아가며 데이터가 전달되도록 하는 방식 보다는, 어떻게 하면 가독성도 좋고 유지보수하기 좋은 방식으로 구현할 수 있을까 고민하며 구글 검색을 열심히 하던 찰나 Observable Pattern이라는 것을 발견했습니다. 처음에는 잘 이해가 가지 않았지만, 데모 형식으로 작게나마 구현을 해보니 구조가 이해가 가기 시작했고, 스택오버플로우에서 베스트 프랙티스로 보이는 사례를 찾아 적용해 보았고, 이를 통해 데이터가 들어오면 이벤트를 발생시켜 Observer에게 데이터를 전달하도록 하는 방식으로 구현했습니다. 결국 맨 처음 생각하던 if-else 방식의 무식한 코드에 비해 코드의 가독성과 유지보수성이 향상된 것을 머리털 나고 처음 경험해 보았습니다. 다음은 Observer들에게 이벤트를 배포하는 코드입니다.
1else if (item.Type == ModbusFuncType.monitRead) 2{ 3 /* 4 * Type = (int)mode, // ModbusFuncType.monitRead: 모니터링 모드 5 * SlavesAddress = slaveAddress, // 슬레이브들의 주소 6 * StartAddress = 40160, // 파라미터 시작 주소 7 * NumberOfPoints = 8, // 읽을 파라미터 갯수 8 */ 9 10 ConcurrentDictionary<byte /* slave id */, float[]/* result */> monitDic = new ConcurrentDictionary<byte, float[]>(); 11 foreach (byte slave in item.SlavesAddress) 12 { 13 ushort[] rxData = await ReadHoldingRegistersAsync(slave, item.StartAddress, item.NumberOfPoints); 14 float[] finalResult = new float[item.NumberOfPoints]; 15 16 finalResult = ReproduceRxDataArr(rxData, ParamCategory.inverterStatus); 17 18 monitDic.TryAdd(slave, finalResult); 19 } 20 21 // 입력받은 모든 슬레이브를 읽어들여 monitDic에 저장 후 MonitoringDocumentViewModel에 DataEventType.MonitoringValueChanged 타입으로 notify 22 // Destination: MonitoringDocumentViewModel (Chart, Linear Gauge, 각종 요소에 출력), MonitoringSettingViewModel (DataGrid에 출력) 23 MonitoringEventManagerWrapper.NotifyObservers(new DataEventDefinition 24 { 25 EventName = nameof(DataEventType.MonitoringValueChanged), 26 ReceiverType = new List<Type>(new Type[] { typeof(MonitoringDocumentViewModel), typeof(MonitoringSettingViewModel) }), 27 EventType = DataEventType.MonitoringValueChanged, 28 Param = monitDic 29 }); 30}
MonitoringEventManagerWrapper.NotifyObservers 호출부를 보시면 인버터 모터와 통신하여 받아온 데이터를 MonitoringDocumentViewModel, MonitoringSettingViewModel 뷰 모델에 전달하는 모습입니다.
1internal class MonitoringDocumentViewModel : PaneViewModel, IDocumentViewModel, IObserver<DataEventDefinition>, IDisposable 2{ 3 4 ...생략... 5 6 public void OnNext(DataEventDefinition node) 7 { 8 // ModbusCommunicator로부터 모니터링 값 변동 감지 9 if ((DataEventType)node.EventType == DataEventType.MonitoringValueChanged) 10 { 11 // ModbusCommmunicator로부터 전달받은 모니터링 결과를 Chart에 실시간 반영 12 ConcurrentDictionary<byte, float[]> chartBucket = new ConcurrentDictionary<byte, float[]>(); 13 ConcurrentDictionary<byte, float[]> gaugeBucket = new ConcurrentDictionary<byte, float[]>(); 14 foreach (var item in monitDic) 15 { 16 chartBucket.TryAdd(item.Key, item.Value); 17 gaugeBucket.TryAdd(item.Key, item.Value); 18 } 19 20 // 차트 업데이트 21 UpdateChart(chartBucket); 22 23 // 게이지 업데이트 24 UpdateGauge(gaugeBucket); 25 } 26 } 27 28 public void Subscribe(IObservable<DataEventDefinition> provider) 29 { 30 // DataEventManager 구독 일 경우 31 if (provider is DataEventManager) 32 { 33 _unsubscriberDataEvent = provider.Subscribe(this); 34 } else if (provider is MonitoringEventManager) // MonitoringEventManager 구독 일 경우 35 { 36 _unsubscriberMonitEvent = provider.Subscribe(this); 37 } 38 } 39 40 public void OnCompleted() 41 { 42 // 두 Subscribtion 구독 해지 43 _unsubscriberDataEvent.Dispose(); 44 _unsubscriberMonitEvent.Dispose(); 45 } 46 47 public void OnError(Exception error) 48 { 49 System.Windows.MessageBox.Show(error.ToString()); 50 } 51}
OnNext 메서드에서 DataEventDefinition을 받아 처리합니다. 발행자로부터 받아온 데이터를 차트와 게이지에 실시간으로 반영하는 코드입니다. Subscribe 메서드에서는 발행자를 구독하고, OnCompleted 메서드에서는 구독을 해지합니다. 이처럼 Observer Pattern을 적용해 보면서 과거의 많은 개발자들이 겪는 문제가 어느 정도는 공통 분모가 있다는 것과, 그 문제를 적절한 방식으로 해결하기 위해 탄생한 것이 디자인 패턴이라는 것을 알게 되었습니다.
여러 컴포넌트에서 네트워크 패킷을 내보낼 때, 이를 한 곳에서 관리할 통신 객체와 토스트 메시지를 통합적으로 제어할 관리 객체가 필요한 상황이 있었습니다. 토스트 메시지 관리 객체에 대한 내용은 제 블로그 [WPF] 팝업 알림창 MVVM 패턴으로 구현하기에 이미 정리해 두었으므로, 여기서는 통신 객체에 대해 자세히 말씀드리겠습니다. 예를 들어 여러 뷰 모델에서 인버터에 메시지를 전달해야 하는 상황이 있을 수 있습니다. 이 경우 뷰 모델마다 통신 객체를 Dependency Injection으로 주입해 쓸 수도 있겠지만, 저는 Singleton Pattern을 적용해 정적(static) 객체로 구현했습니다. 다음은 제가 작성한 Singleton 클래스 정의입니다.
1internal class ModbusCommunicatorWrapper : IDisposable 2{ 3 #region fields 4 private static ModbusCommunicatorWrapper _instance; 5 private ModbusCommunicator Origin { get; set; } 6 private static readonly object lockObj = new object(); 7 8 public static WorkSpaceViewModel _vm = null; 9 private bool disposedValue; 10 11 /// <summary> 스레드 흐름제어 신호 객체. </summary> 12 private static readonly ManualResetEvent _signalMutex = new ManualResetEvent(false); 13 #endregion fields 14 15 #region ctors 16 /// <summary> 단 한 번만 초기화. </summary> 17 public ModbusCommunicatorWrapper() 18 { 19 if (_vm != null) 20 { 21 Origin = new ModbusCommunicator(_vm); 22 23 Origin.FinishedEvent += FinishedEventHandler; 24 } 25 else 26 throw new System.Exception("ModbusCommunicatorWrapper에서 WorkSpaceViewModel을 inject 받지 못했습니다."); 27 } 28 #endregion ctors 29 30 #region properties 31 /// <summary> 32 /// Singleton Instance for Modbus 33 /// 여러 스레드에서 싱글톤 객채에 접근할 수 밖에 없으므로 필요에 따라 Double-Checking Locking 활용 34 /// </summary> 35 private static ModbusCommunicatorWrapper Instance 36 { 37 get 38 { 39 if (_instance == null) 40 { 41 lock (lockObj) 42 { 43 if (_instance == null) 44 { 45 _instance = new ModbusCommunicatorWrapper(); 46 } 47 } 48 } 49 50 return _instance; 51 } 52 } 53 #endregion properties 54 55 ...생략... 56 57 public static void sendTx(byte slaveAddress, ushort startAddress, ushort numberOfPoints) 58 { 59 Instance.Origin.sendTx(slaveAddress, startAddress, numberOfPoints); 60 } 61 62 public static void sendTx(byte slaveAddress, ushort startAddress, ushort numberOfPoints, ParamDataType dataType) 63 { 64 Instance.Origin.sendTx(slaveAddress, startAddress, numberOfPoints, dataType); 65 } 66}
ModbusCommunicatorWrapper가 ModbusCommunicator 인스턴스를 단 하나만 만들어 유지하는 래퍼 클래스입니다. 이 클래스를 통해 ModbusCommunicator 인스턴스를 생성하고, 다른 뷰 모델에서 이 클래스에 직접적인 접근은 막아두었고, sendTx 와 같은 통신에 필요한 메서드만 public static으로 열어 외부에서 사용할 수 있도록 했습니다. Singleton 특성상 여러 외부 요인이 접근 가능하므로 ModbusCommunicatorWrapper 내부에는 상태를 정의하거나 수정할 수 있는 상황을 배제시키고, 허용되는 메서드만을 노출시키도록 했습니다.
오픈소스(다중 윈도우 도킹 관련) 적용 경험
디자인 패턴 적용 경험도 좋았지만, 처음으로 오픈소스 라이브러리를 활용해 개발을 진행해볼 기회가 있었고, 그 안에서 다양한 코딩 기법을 만나볼 수 있었던 것도 좋았습니다. 개발을 진행하면서 가장 어려운 순간이기도 했지만(어려웠던 점에서 설명드리겠습니다.), 많은 개발자들이 만든 코드를 분석하고 받아들이는 습관을 기를 수 있었으며, 이것이 저의 최대 장점이자 무기가 됐습니다. 이후부터는 "이것을 적용하면 얻는 장점과 단점은 무엇일까?" 혹은 "정말 이게 최선일까?" 등의 비판적 사고를 갖고 트레이드오프를 고려하게 되었고, 차선책 적용 후 현재 상황에 안주하지 않고 코드 또는 인프라를 지속해서 더 나은 방향으로 개선하기 위해 노력하는 자세를 유지하고 있습니다.
멀티 스레드 프로그래밍 경험
그 다음으로 배운 점은 바로 스레드에 대한 이해입니다. WPF나 Winforms로 윈도우 프로그래밍을 하려면 멀티 스레딩에 대해 잘 알아야 합니다. 물론 저는 경험이 적었고, 학교에서도 스레드를 간단한 게임이나 VR 시뮬레이터 등을 만들 때 빼곤 본격적으로 접해본 적이 없어서 상당히 어려웠던 기억이 납니다. 무엇이 어려웠냐면, Main 스레드(UI 스레드)와 Worker 스레드의 분리와 적절한 사용이 어려웠습니다. 그리고 OS 특성상 프로그램이 실행될 때 Main 스레드를 하나 할당받습니다. Main 스레드는 UI를 그리고(Drawing) 이벤트를 처리하는 역할을 합니다. 그런데 저는 이러한 개념이 없어서 Main 스레드에서 모든 일을 처리하다가 UI가 멈추는 현상을 겪었습니다. 이를 해결하기 위해 열심히 공부해보니 Worker 스레드의 존재를 알게 되었고, Worker 스레드를 만들어 Main 스레드에서 처리해야 할 일을 Worker 스레드로 옮겨 처리하도록 해 UI가 멈추는 현상은 겪지 않게 되었습니다. 또한, 무조건 Worker 스레드를 새로 만들어 처리하는 것 보다는, Thread Pool을 사용해 기존 스레드를 재활용하도록 하여 자원을 효율적으로 사용할 수 있었습니다. 이러한 경험들을 통해 멀티 스레딩은 어떻게 처리해야 하고, 어떤 방식으로 돌아가는지 이해가 생겼고, 이후에도 멀티 스레딩을 사용해야 하는 상황이 생길 때마다 적절한 스레드를 사용하도록 노력하고 있습니다.
멀티 스레드 신호정리 경험
마지막으로 배운 점은 스레드의 신호정리가 필요하다는 것입니다. 통신 요청을 보내기 위해 Singleton 통신 객체에 여러 스레드가 접근할 때 동시에 큐에 쌓도록 하면 다른 요청이 겹쳐 누락되는 경우가 발생할 수 있습니다. 이를 방지하기 위해 BlockingCollection 클래스를 사용해 외부 스레드가 큐에 enqueue 요청을 보냈을 때 Mutex 락을 걸고, 다른 스레드가 접근하지 못하도록 막은 후 정상적으로 enqueue를 마치면 다른 스레드가 접근 가능하도록 했습니다. 코드는 다음과 같습니다.
1// [A] 2/// <summary> 3/// 모드버스 Tx 요청 4/// </summary> 5/// <param name="slaveAddress">Tx 목적지 인버터 주소</param> 6/// <param name="mode">Modbus Protocol 함수 타입</param> 7public static void sendTx(List<byte> slaveAddress, ModbusFuncType mode) 8{ 9 Instance.Origin.AddItemToMonitoring(slaveAddress, mode); 10 11 _signalMutex.WaitOne(); 12} 13 14// [B] 15finally 16{ 17 // 작업 마침 알림 18 Finished = true; 19 20 // Thread 신호대기 해제 21 FinishedEvent?.Invoke(this, new ModbusCommunicatorEventArgs()); 22} 23 24/// <summary> 25/// Mutex 신호 개방 콜백 메서드 26/// </summary> 27/// <param name="sender">콜백 호출자</param> 28/// <param name="e">콜백 이벤트</param> 29private void FinishedEventHandler(object sender, ModbusCommunicatorEventArgs e) 30{ 31 // WorkNode enqueue 완료 시 콜백을 통해 Mutex 신호 개방 32 _signalMutex.Set(); 33}
[A] Singleton 객체의 sendTx 메서드를 외부 스레드가 호출하면 Mutex 신호를 닫아 다른 스레드가 접근하지 못하도록 하고, [B] 모드버스 통신 객체에서 enqueue를 마치면 Mutex 신호를 다시 개방해 다른 스레드가 접근할 수 있도록 했습니다. 이를 통해 동시성에서 오는 문제를 해결하고, 안정적인 모드버스 통신을 할 수 있었습니다.
Semaphore가 아닌 Mutex를 사용한 이유는 요청이 잦지 않아 1개를 초과하는 스레드가 접근할 필요는 없었고, 디버깅 시 API 호출자 추적 또한 Semaphore보다 수월했으며, 미미하긴 하지만 Semaphore는 카운팅 로직으로 인해 약간의 성능 손실이 상대적으로 존재했기 때문에 Mutex를 사용하도록 결정했습니다.
어려웠던 점
WPF 관련 지식에 대한 미숙함
첫 번째로 어려웠던 점은 XAML 기반 웹 UI 컴포넌트 개발입니다. WPF에서는 UI 컴포넌트를 WPF Control이라고 하는데, 이 컨트롤마다 속성과 기능이 모두 다 달라 학습에 어려움을 겪었습니다. 레이아웃 구성과 UI 배치가 쉬운 편이라 HTML 코드처럼 간단하게 태그를 나열해 화면 구성이 가능하긴 했지만 다른 문제들이 있었습니다. 예를 들어 Visual Tree(UI로 실제 렌더링 되는 요소) 혹은 Logical Tree(데이터 바인딩 처리)에 포함되지 않는 컨트롤이라면 ViewModel DataContext에 접근조차 하지 못하는 문제가 있었습니다. 이런 경우 View에서 보여줘야 할 데이터를 보여주지 못하게 되는 상태가 됩니다. 따라서 Freezable 이라는 추상 클래스를 상속받는 클래스를 구현해 그 클래스를 Control에 붙여 약간의 우회하는 방법으로 DataContext에 접근할 수 있도록 해야만 했습니다. Freezable 추상 클래스는 DataContext를 상속받을 수 있게 해주기 때문에 이러한 동작이 가능하다고 합니다. 이처럼 기존의 HTML 개발 프로세스와는 아예 다른 방식으로 접근해야 했기 때문에 XAML 기반 UI 개발에서 많은 어려움을 겪었습니다.
MVVM 패턴에 대한 미숙함
두 번째로 어려웠던 점은 MVVM 패턴 자체에서 오는 러닝 커브입니다. 나중에 다른 사람과 협업해야할 일이 생기거나 후임자가 생기면 코드를 구조화된 형태로 바라볼 수 있도록 MVVM 패턴을 도입했었습니다. WPF가 기본적으로 MVVM 패턴을 위해 탄생한 만큼, 프레임워크 개발팀 자체에서 사용을 권유하는 편이지만, 1인 개발을 하는데 굳이 이 패턴을 써야 하느냐, 오버헤드가 너무 크다는 시각도 인터넷 상에서 일부 존재했습니다. 그러나 저는 구조화 되지 않은 프로젝트에 새로운 협업자가 다수 들어왔을 때, 어떤 잘 정형화 된 팀 코드 컨벤션이 제대로 구축되어 있지 않은 경우 결국 스파게티 코드가 될 수밖에 없다고 생각해 이 패턴을 도입하게 되었습니다. 처음엔 러닝 커브가 매우 높아 초반에 개발 속도가 많이 떨어졌지만, MVVM 패턴의 룰을 이해하고 조금씩 적용시키며 진행하다보니 나중가서는 확장에 열린 개발을 손쉽게 할 수 있게 되었습니다. 이 과정을 통해 왜 개발자들이 패턴을 도입하고, 팀과 팀원들이 시간을 써가면서 규칙과 규제를 만들어 나아가려는지 몸소 깨닫게 되었습니다. 이를 통해 어떠한 패턴이든 궁금증이 생기면 일단 직접 만들어보고, 어떤 장단점이 있는지 알아보려고 하는 것이 습관이 되었습니다.
복잡한 오픈소스의 적용
세 번째로 어려웠던 점은 UI에서 Docking & Floating Window를 다룰 때의 어려움이였습니다. 대부분의 WPF 애플리케이션을 보면 보통 하나의 윈도우에 모든 기능이 Sticky 하게 정적으로 붙어서 움직이지 않는 형태로 개발되어 있습니다. 하지만 이번 프로젝트는 윈도우 안에 윈도우를 고정하고, 고정 해제해 독립적으로 움직일 수 있도록 하는 Docking System을 도입했습니다. 도입 이유는 임베디드 개발자 분과 상의 결과 여러 인버터 모터의 데이터를 동시에 비교하기 위해서는 여러 윈도우가 필요하다는 의견이 나왔기 때문입니다. 따라서 오픈소스 라이브러리인 AvalonDock을 활용하기로 했습니다. 이 라이브러리는 훌륭한 테마와 각종 기능을 보일러 플레이트로 가지고 있기 때문에 초기 환경 설정에 큰 어려움 없이 바로 개발을 진행할 수 있었습니다. 다만, 이 라이브러리의 각종 클래스 예를 들어 LayoutDocumentPane, LayoutAnchorablePane, LayoutDockablePane 등 이들이 구현한 개념들을 익혀야만 했습니다. 이 과정에서 개발 속도가 다시 느려지긴 했지만, 문서 정리가 잘 되어 있어 금방 적응할 수 있었습니다. 이 오픈소스 라이브러리를 통해 UI에 날개를 달아 많은 확장 가능성을 부여할 수 있었습니다. 이처럼 오픈소스 프로젝트를 활용해 개발을 진행한 경험을 계기로 다른 오픈소스에 많은 관심을 기울일 수 있게 되었고, 많은 오픈소스 코드들을 분석하고 받아들이는 습관이 생겨 미래에 더욱 성장할 수 있는 발판이 되었다고 생각합니다.
메모리 누수에 대한 이해도와 경험 부족
마지막으로 어려웠던 점은 전역 객체 관리에 대한 이해와 경험 부족이었습니다. 어느날 요구사항에 사용자에게 알림 피드를 주는 기능이 추가돼 프로그램 우측 상단에 토스트 알림 메시지를 띄우는 컴포넌트를 개발해야 했습니다. 이 토스트 메시지 기능을 하는 객체는 전역으로 생성되고 관리되어야 했습니다. 따라서 싱글톤 패턴으로 관리하였습니다. 하지만 경험미숙으로 이 과정에서 프로그램을 종료 해도 프로세스가 죽지 않는 문제가 발생했습니다. 원인은 프로그램이 닫힐 때 전역 객체를 명시적으로 Dispose 하지 않았기 때문이었습니다. 전역으로 등록된 객체는 프로그램이 생성될 때 같이 생성되는데, 이는 가비지 컬렉터의 관리 대상에서 벗어나게 됩니다. 당시 가비지 컬렉터가 어느 데이터를 수집하고 관리하는지 자세히 몰랐기 때문에 헤맸던 기억이 있습니다. 추후 저는 문제가 있음을 파악하게 되었고, Eager, Lazy 어떠한 방식으로 생성되는 간에 프로그램 생명주기가 종료되는 시점 즉, Closing 핸들러가 호출될 때 명시적으로 Dispose 하도록 하여 해결했습니다.
Tech Stack
WPF MVVM 구조를 기반으로 실시간 차트, 게이지 UI와 안정적인 Modbus 통신을 통합했습니다.
ARCH
MVVM Desktop Architecture
WPF, MVVM 기반으로 View, ViewModel, Service 레이어를 분리해 유지보수성과 테스트성을 높였습니다.
C#.NET FrameworkWPFMVVM Toolkitlog4net
VIS
Realtime Visualization
Live Charts와 DevExpress Gauge로 인버터 상태를 차트, 게이지로 시각화하고 테마 커스터마이징을 제공했습니다.
Live ChartsDevExpress GaugeTheme Resource DictionaryDynamic Palette
COMM
Modbus Communication Layer
RS-485 환경에 맞는 동기/비동기 큐와 Mutex 기반 신호 정리로 안전한 패킷 전송을 구현했습니다.
NModbus4RS-485 Half-duplexMutex SignalRetry QueueObserver Pattern