회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
LG CNS, KT DS도 이용하는 대표 IT프로젝트 플랫폼
2000년도 중후반, .NET Windows Form(이하 WinForm)이 공장자동화 계열에서 MFC, VB와 같은 윈도우 프로그램을 대체하기 시작했다. 어느덧 시간이 흘러, 이제 WinForm을 다루는 개발자들은 무언가 새로운 것을 개발하는 업무는 거의 없는 상황에 놓였다. 그보다 묵을 대로 묵은 옛 선배들의 레거시 코드를 리팩토링하거나 이를 참고해 기능을 확장하는 경우가 많다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
2000년도 중후반, .NET Windows Form(이하 WinForm)이 공장자동화 계열에서 MFC, VB와 같은 윈도우 프로그램을 대체하기 시작했다. 어느덧 시간이 흘러, 이제 WinForm을 다루는 개발자들은 무언가 새로운 것을 개발하는 업무는 거의 없는 상황에 놓였다. 그보다 묵을 대로 묵은 옛 선배들의 레거시 코드를 리팩토링하거나 이를 참고해 기능을 확장하는 경우가 많다.
일반적으로 개발 분야에서 리팩토링이란 “결과의 변경 없이 코드의 구조를 재조정함”을 의미한다. WinForm에는 GUI 디자이너, Win32 컨트롤, Form 등 독자적인 클래스 라이브러리가 있으므로 이와 같은 환경에 적용할 리팩토링 방법을 이해해야 한다.
이 글은 스마트팩토리 분야에서 가장 많이 쓰이는 .NET WinForm의 리팩토링 방법을 다룰 예정이다. 예제와 함께 필자가 스마트팩토리 분야에서 직접 WinForm 개발을 해오면서 중요하다고 생각한 몇 가지 전략을 소개하고자 한다.
리팩토링 예제는 입고 검사 프로그램으로, 자재가 입고되면 측정한 중량이 바코드에 표시된 중량에서 벗어나는지 판정하는 역할이다. 바코드 스캐너와 중량계는 직렬로 수신받고 실적은 HTTP로 전송한다. 오래된 프로그램 기준에 맞추면서도 비교적 상위 버전인 닷넷프레임워크 4.7로 개발했다.
이 프로그램의 원본 코드는 깃허브에 올려두었다. 10년 전, 스마트팩토리 분야 1년 차 개발자가 작성했을 법한 스타일로 코딩하려 노력했다. 물론 프로그램은 매우 잘 작동한다. 하지만 코드는 정리되지 않은 엉망진창 코드다.
왜 이토록 엉망진창인 코드가 오래 보존될까? 스마트팩토리 분야에서는 흔한 상황이다. 이 업계에서는 디바이스 연동도, 단위 프로그램의 수도 많아 저관리 프로그램이 자주 발생한다. 이 프로그램들은 가치가 낮으므로 리팩토링 비용이 비쌀 수밖에 없다. 따라서 상황이 나아지지 않고 오래 유지된다.
문제는 이런 저관리 프로그램을 주로 주니어 개발자가 맡는다는 점이다. 좋은 프로그램이 나올 리 없고 좋은 개발자가 나올 리 없다. 게다가 너무 오래도록 리팩토링하지 않은 프로그램이 있다면, 회사의 품질 관리 문제가 생길 수 있다.
그렇다면 프로젝트에서 리팩토링은 어떤 시기에 필요할까? 앞서 말했듯 스마트팩토리 분야는 디바이스 연동이 많다. 그래서 개발과 단위 테스트를 잘하고 가더라도 결국 실제 통신 시에 코드를 고칠 일이 자주 발생한다. 스마트팩토리에서 통합 테스트 기간은 비교적 넉넉하게 잡기 때문에, 급히 문제를 해결하지 않도록 그 기간에 리팩토링은 진행하는 것을 권장한다.
먼저 예제 코드에 대한 안내를 하고자 한다. 예제 항목들은 아래 세 가지 목적을 가지고 있다.
각 항목 모두 기존 코드에서 출발해 Git 브랜치를 새로 만들어 리팩토링을 진행했다. 따라서 순서에 상관이 없이 읽을 수 있다. 이에 맞춰 깃허브에 올려둔 소스들도 여러 브랜치로 커밋했다. 직접 리팩토링 연습을 하고 싶다면 master 브랜치를 포크 받으면 된다.
입고 검사 프로그램의 기존 코드에서는 ReceiveInspection 클래스가 바코드 스캐너, 중량계, REST API 연결에 대한 설정과 전송 및 수신 처리를 모두 수행한다. 클래스 하나가 여러 책임을 지는 데다 서로 의존성이 큰 탓에 변경이 일어날 때 관련 없는 곳까지 영향이 갈 수 있어 관리가 어렵다. 이런 문제의 해결 방안은 모듈화로, 책임을 각 모듈에 할당하는 것이다.
리팩토링 시에는 우선 코드를 분석하고 이해하는 것이 먼저다. 기존 코드의 기능들을 적당히 배치해 보면 1) 바코드 스캐너, 2) 중량계, 3) 작업 실적 세 가지로 나눌 수 있다. 물론 단일책임으로 분리하는 공사는 품이 많이 들기 때문에 신중하게 결정하고 진행해야 한다.
모듈화는 코드의 구조적 변경이 필요한 경우가 많다. Git이나 SVN을 쓴다면, 브랜치를 하나 만들어 작업하는 걸 권장한다. 나는 Git에서 refactorer_1 브랜치를 만들었다.
또한 모듈을 새로 만들어야 한다면 테스트 주도 개발(TDD)로 시작할 것을 권하고 싶다. 다만 디바이스 연동과 의존하는 모듈이라면, 테스트를 만들 때 모킹(모의 객체)하느라 시간이 더 걸리므로 정말 효율적인지 판단하고 결정할 필요가 있다.
글에서는 바코드 스캐너 기능의 모듈화 과정만을 설명하고자 한다. 다음과 같이 스캐너 연결을 시작하고 연결이 OK가 되는 테스트 코드를 작성했다. (프로젝트는 MSTest이고 모킹 라이브러리는 Moq를 사용했다)
현재는 모듈을 작성한 것이 없기 때문에 컴파일 에러가 난다. 다음의 코드를 작업해 주자.
코드를 간단히 설명하자면, XerialPort는 직렬 통신을 위한 SerialPort 라이브러리에 의존성을 주입하고자 만들었다. 이제 BarcodeScanner 클래스는 생성 시점부터 XerialPort 객체를 파라미터로 참조한다. 스캐너의 연결 상태는 열거자 BarcodeScannerStatus로 알 수 있게 했다. 이 정도만 해도 테스트를 통과할 수 있다.
다음 단계는 스캐너 가동 기능을 테스트하는 것이다. ConnectStart 메소드를 호출하고 시작된 상태인지 체크한다.
컴파일 에러가 안 나도록 ConnectStart 메소드를 BarcodeScanner 클래스에 추가한다. 이 메소드가 호출되면 다른 스레드에서 매 5초마다 확인해서 연결시킨다. 기존 코드를 확인해 보자.
이 코드를 긁어서 BarcodeScanner 클래스에 방금 추가한 ConnectStart 메소드에 붙여 넣었다.
물론 컴파일 에러가 난다. serialPort1, StatusMessageShow 등이 기존 클래스의 자원들이기 때문이다. 이를 해결하려면 serialPort1은 BarcodeScanner의 멤버인 _port로 변경해줄 수 있다. StatusMessageShow 메소드는 창 하단에 메시지를 출력하는 기능인데, 귀찮지만 대리자를 거쳐 외부에서 해당 Form의 UI 자원에 전달할 수 있도록 처리하면 된다.
먼저 Program.cs로 가서 다음과 같이 대리자를 추가해 주고 해당 Form의 메소드를 참조할 수 있다. 이 코드로 Program.statusMessageShow는 f.StatusMessageShow의 함수 포인터로 동작한다.
다시 옮겨온 코드는 다음과 같이 대리자를 호출하도록 바뀐다.
마지막 작업이다. 기존 코드로 가서 바코드 스캐너 객체를 생성하고 ConnectStart 메소드를 호출하도록 변경한다.
이렇게 단일 책임을 위한 리팩토링 일부가 끝났다. 핵심은 꾸준히 테스트를 만들면서 기존 코드를 모듈화하는 작업 방식이다. 모듈화는 코드 구조를 변경하는 작업인 만큼 돌다리도 두드리며 건넌다는 생각으로 임하는 게 좋다. 테스트가 쌓일수록 튼튼한 코드가 나온다.
판정 시 OK와 NG 여부를 화면에 출력하는 방식이 있다. 이 코드를 확인해 보니 조건에 따라 해당 레이블의 색깔을 직접 설정하는 방식임을 알 수 있다.
그러나 주석 없이 색상 변경 코드만 보면 이게 무슨 기능인지 의미를 파악하기 어렵다. 이를 사용자 정의 컨트롤을 통해 아래처럼 코드 한 줄로 만들면, 간단하고 의미도 명확한 방식이 될 것이다.
기존 코드에서 refactor_2usc 브랜치를 만들고 작업을 진행했다. 우선 사용자 정의 컨트롤을 만들기 전에 기존의 컨트롤들이 어찌 배치되었는지 분석해 봐야 한다.
OK와 NG를 출력하는 영역은 Label 컨트롤 두 개로 기능한다. 이제 사용자 정의 컨트롤을 추가해 두 레이블을 긁어다 붙여 놓으려고 한다. 이 사용자 정의 컨트롤의 이름은 코파일럿이 추천한 ResultHighlighter로 정했다. 이처럼 리팩토링 단계에서 네이밍이 고민이면 지체 말고 인공지능에 물어보는 것이 좋다. 매우 빠르게 적절한 이름을 알려준다.
이처럼 사용자 정의 컨트롤을 만들고 빌드하면 디자이너 모드의 ‘도구 상자’에서 ResultHighlighter 컨트롤이 나타난다.
기존 화면에서 OK와 NG 컨트롤을 지우고 새로 만든 컨트롤로 바꿨다. 곧이어 다시 ResultHighlighter 클래스로 가서 다음과 같이 Result 프로퍼티를 만들어 기능까지 코딩해 줬다.
이제 기존 코드인 ReceiveInspection 클래스에서 이를 수정하면 이번 작업은 끝이 난다.
if-else는 매우 활용도가 높은 조건문이지만, else if가 지나치게 반복되거나 조건식이 길고 복잡하면, 분석하는 사람을 지치게 한다. 그런 만큼 if-else는 잘못 쓰면 가독성을 해치기 쉽다.
아래 invoke 구문을 보자. WinForm 개발을 하다 보면 다른 스레드에서 메인 스레드의 자원인 UI 컨트롤에 접근할 일이 무척 많다. 스레드 접근 시 컨트롤의 InvokeRequired가 true가 되기 때문에, 메인 스레드가 동기적으로 처리할 수 있도록 invoke에 대리자 메소드를 넘겨주는 식이다.
문제는 코드에서 나는 악취다. 단순히 컨트롤의 Text 멤버에 문자값을 설정하는 것일 뿐인데 이런 조건문이 자주 등장하여 읽기 어렵다. 낮은 가독성 탓에 자칫하면 근처에 있을지도 모를 중요한 코드가 묻힐 가능성도 높다.
모든 UI 컨트롤이 닷넷의 System.Windows.Forms.Control을 상속한다는 점에 착안해 해결 방법을 찾았다. 이 항목은 refactor_3ifelse 브랜치로 커밋했다.
먼저 대리자 TextSetThreadSafe로 스레드 안전한 기능을 만들어 보겠다.
앞서 InvokeRequired 조건문의 반복으로 악취가 났던 코드로 돌아가, 대리자를 추가했다.
전반적으로 바코드 스캐너 수신 이벤트의 전체 기능이 한눈에 잘 들어오는 모습이다. 그와 함께 TextSetThreadSafe 라는 메소드 이름만으로도 이것이 스레드 안전한 방식의 설정임을 알 수 있게 했다.
이 과정으로 알 수 있는 사실이 있다. 반복되는 조건문이나 복잡한 조건식 탓에 악취가 나는 코드는 메소드로 대체하는 정도만으로 깔끔해지는 경우가 많다는 점이다.
또 다른 예시로 표준중량과 측정중량을 가지고 판별하는 조건식을 가져왔다. 조금 얕지만, 마찬가지로 냄새가 나는 코드다.
허용 오차가 바뀌면 두 개의 하드코딩 값을 변경해 줘야 하는 귀찮은 코드다. 이 코드가 무슨 일을 하는지 역시 신경 써서 봐야 파악할 수 있다. 보기 쉽게 바꾸려면 이를 메소드로 대체하는 것으로 충분하다. 아래 메소드를 만들었다.
허용 오차 범위인지 아닌지 결과를 반환하는 메소드가 나왔다. 앞서 본 조건문을 이 메소드로 대체하고 하드코딩된 값도 상수로 관리하게 되었다. 곧 아래와 같이 의미가 명확한 조건문을 만들 수 있었다.
모든 예제 브랜치를 병합하고 정리한 코드는 refct_close 브랜치에 올려 두었다. 물론 필자의 작업이 마음에 안 들 수 있으며 그 외에도 리팩토링이 필요한 거리를 발견할 수도 있다. 이는 독자들의 몫으로 남기겠다.
만약 오래도록 리팩토링을 무시하면 어떤 일이 벌어질까? 우리가 본 엉망진창 코드가 레거시 코드가 되어 버리면 결국 리팩토링 부채에 빠지고 만다. 리팩토링 비용이 시간이 지날수록 비싸진다는 의미다. 먼 미래에는 차라리 새로운 코드를 만드는 게 나은 상황이 생긴다. 물론 언젠가는 이런 시기가 반드시 온다는 걸 부정하기 어렵다. 하지만 리팩토링은 이러한 시기를 늦출 수 있다.
이 글이 적어도 개발사에 있어 리팩토링을 장려할 기회가 되었길 바란다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.