소프트웨어 엔지니어링에서 코드 구조의 중요성은 아무리 강조해도 지나치지 않습니다. 특히 규모가 큰 프로젝트를 진행할 때, 하나의 파일에 너무 많은 코드가 집중되면 유지보수가 어려워지고 가독성이 떨어집니다. 오늘은 C# 프로젝트에서 코드 구조를 개선하는 방법에 대해 논의하고자 합니다.
모놀리식 코드의 문제점
소프트웨어 개발 초기 단계에서 많은 개발자들이 기능 구현에 집중하다 보면, 단일 파일에 모든 기능을 구현하는 ‘모놀리식’ 코드를 작성하게 됩니다. 이런 접근 방식은 단기적으로는 빠른 개발을 가능하게 하지만, 장기적으로는 다음과 같은 문제를 초래합니다:
- 가독성 저하: 한 파일에 수천 줄의 코드가 있으면 특정 기능을 찾기 어렵습니다.
- 유지보수 어려움: 코드의 특정 부분을 수정할 때 의도치 않은 사이드 이펙트가 발생할 가능성이 높아집니다.
- 협업 제한: 여러 개발자가 동시에 같은 파일을 수정하면 버전 충돌이 자주 발생합니다.
- 코드 재사용 어려움: 단일 파일에 여러 기능이 얽혀 있으면 기능을 분리하여 재사용하기 어렵습니다.
객체지향 프로그래밍의 핵심 원칙: SRP
객체지향 프로그래밍(OOP)의 핵심 원칙 중 하나인 ‘단일 책임 원칙(Single Responsibility Principle, SRP)’은 “클래스는 변경해야 할 이유가 오직 하나뿐이어야 한다”는 개념을 강조합니다. 이 원칙을 적용하면 각 클래스는 명확히 정의된 하나의 책임만 갖게 되어, 코드의 모듈성과 재사용성이 높아집니다.
모듈화를 통한 코드 개선 사례: 컴퓨터 비전 애플리케이션
제가 최근 리팩토링한 프로젝트를 예로 들어보겠습니다. 이 프로젝트는 YOLOv8 객체 탐지 모델을 사용하여 카메라 피드에서 실시간으로 객체를 감지하는 Windows Forms 애플리케이션입니다.
원래 구조: 모놀리식 Form1.cs
초기 구현에서는 모든 코드가 Form1.cs 파일에 집중되어 있었습니다. 이 파일에는 다음과 같은 여러 기능이 혼합되어 있었습니다:
- UI 구성요소 및 이벤트 핸들러
- 카메라 디바이스 제어 로직
- YOLO 모델 초기화 및 추론 로직
- 이미지 전처리 및 후처리 로직
- 객체 탐지 결과 시각화 로직
이로 인해 Form1.cs는 수백 줄의 코드를 가진 복잡한 파일이 되었고, 단일 기능을 수정하기 위해 전체 파일을 이해해야 하는 상황이 발생했습니다.
개선된 구조: 모듈화된 클래스 분리
코드를 리팩토링하여 다음과 같이 기능별로 분리했습니다:
- Form1.cs: UI 관련 코드와 이벤트 핸들러만 포함
- YoloDetector.cs: YOLO 모델 초기화, 추론, 시각화 로직
- CameraManager.cs: 카메라 장치 검색, 프레임 캡처, 리소스 관리
이렇게 구조를 개선하면서 다음과 같은 이점을 얻었습니다:
1. 명확한 책임 분리
각 클래스는 명확한 하나의 책임을 가집니다:
Form1
클래스: 사용자 인터페이스 관리YoloDetector
클래스: 객체 탐지 로직CameraManager
클래스: 카메라 하드웨어 제어
2. 코드 재사용성 향상
특히 YoloDetector
와 CameraManager
는 독립적인 클래스로 분리됨으로써 다른 프로젝트에서도 쉽게 재사용할 수 있게 되었습니다. 예를 들어, 향후 WPF나 UWP로 UI를 변경하더라도 이 두 클래스는 거의 수정 없이 재사용할 수 있습니다.
3. 유지보수성 개선
기능 별로 코드가 분리되어 있기 때문에 특정 부분만 수정할 때 다른 기능에 미치는 영향을 최소화할 수 있습니다. 예를 들어, 카메라 관련 버그를 수정할 때는 CameraManager
클래스만 집중적으로 살펴보면 됩니다.
4. 협업 개선
팀 개발 환경에서는 각 개발자가 서로 다른 클래스를 담당하여 병렬적으로 작업할 수 있게 되었습니다. 예를 들어, 한 개발자는 UI를 개선하고, 다른 개발자는 객체 탐지 알고리즘을 최적화하는 작업을 동시에 진행할 수 있습니다.
구현 세부 사항
1. YoloDetector 클래스
public class YoloDetector
{
private InferenceSession session;
private float confidenceThreshold = 0.2f;
public bool IsInitialized { get; private set; } = false;
public bool Initialize()
{
// YOLO 모델 초기화 로직
}
public void ProcessFrame(Mat frame, int displayWidth, int displayHeight)
{
// 프레임 처리 및 객체 탐지 로직
}
private DenseTensor<float> PreprocessImage(Mat frame)
{
// 이미지 전처리 로직
}
private List<Prediction> PostprocessPredictions(Tensor<float> output, ...)
{
// 예측 결과 후처리 로직
}
}
2. CameraManager 클래스
public class CameraManager
{
private VideoCapture capture;
public bool IsRunning { get; private set; } = false;
public List<string> FindCameraDevices()
{
// 카메라 장치 검색 로직
}
public bool StartCamera(int deviceIndex)
{
// 카메라 시작 로직
}
public void StopCamera()
{
// 카메라 중지 및 리소스 정리 로직
}
public Mat GetFrame()
{
// 프레임 캡처 로직
}
}
3. Form1 클래스
public partial class Form1 : Form
{
private bool isFlipX = false;
private YoloDetector yoloDetector;
private CameraManager cameraManager;
public Form1()
{
InitializeComponent();
yoloDetector = new YoloDetector();
cameraManager = new CameraManager();
}
private async Task ProcessCameraFrames()
{
// UI 스레드와 비동기 통신하는 로직
}
// 이벤트 핸들러들
}
모듈화 과정에서의 주의점
1. 네임스페이스 충돌 해결
코드를 모듈화하는 과정에서 발생할 수 있는 한 가지 문제는 네임스페이스 충돌입니다. 예를 들어, 이 프로젝트에서는 System.Drawing.Point
와 OpenCvSharp.Point
클래스 간의 충돌이 발생했습니다.
이런 충돌은 다음과 같이 네임스페이스 별칭(alias)을 사용하여 해결할 수 있습니다:
using OpenCvPoint = OpenCvSharp.Point;
using OpenCvSize = OpenCvSharp.Size;
그리고 코드에서 명시적으로 어떤 타입을 사용할지 지정합니다:
Cv2.Rectangle(frame, new OpenCvPoint(x, y), ...);
2. 클래스 간 통신 설계
클래스를 분리할 때는 클래스 간 통신 방식을 신중하게 설계해야 합니다. 이 프로젝트에서는 다음과 같은 패턴을 적용했습니다:
- 명확한 API 설계: 각 클래스는 외부에서 필요한 기능만 공개 메서드로 노출합니다.
- 상태 캡슐화: 각 클래스는 자신의 상태를 내부에서 관리하고, 필요한 정보만 속성(Property)을 통해 노출합니다.
- 의존성 주입: Form1 클래스는 생성자에서 YoloDetector와 CameraManager 인스턴스를 생성하고 관리합니다.
모듈화의 추가 확장 가능성
현재 구현에서도 충분히 모듈화가 이루어졌지만, 더 복잡한 프로젝트에서는 다음과 같은 추가 개선을 고려할 수 있습니다:
- 인터페이스 도입:
IObjectDetector
,ICameraDevice
등의 인터페이스를 정의하여 구현 교체 가능성을 높입니다. - 의존성 주입 프레임워크 사용: Microsoft.Extensions.DependencyInjection과 같은 프레임워크를 도입하여 의존성 관리를 자동화합니다.
- 이벤트 기반 통신: 클래스 간 직접 참조 대신 이벤트를 통한 느슨한 결합(loose coupling)을 구현합니다.
- MVVM 패턴 적용: 특히 WPF로 마이그레이션할 경우, Model-View-ViewModel 패턴을 적용하여 UI 로직과 비즈니스 로직을 더 명확히 분리할 수 있습니다.
결론
코드 모듈화는 소프트웨어 엔지니어링의 중요한 측면입니다. 단일 책임 원칙을 따르는 작은 클래스들로 코드를 구성하면 가독성, 유지보수성, 재사용성, 테스트 용이성 등 여러 측면에서 이점을 얻을 수 있습니다.
이 사례에서 보았듯이, Form1.cs 하나의 파일에 모든 코드를 작성하는 대신 기능별로 클래스를 분리하여 코드의 품질을 크게 향상시킬 수 있었습니다. 이러한 접근 방식은 규모가 작은 프로젝트에서도 초기부터 적용하는 것이 바람직하며, 장기적으로는 개발 속도와 코드 품질 모두에 긍정적인 영향을 미칩니다.
객체지향 설계의 핵심은 적절한 추상화 수준을 찾는 것입니다. 너무 세분화하면 불필요한 복잡성이 증가하고, 너무 크게 묶으면 유지보수가 어려워집니다. 프로젝트의 규모와 복잡성에 맞는 적절한 모듈화 수준을 찾는 것이 중요합니다.
마지막으로, 코드 구조 개선은 일회성 작업이 아닌 지속적인 리팩토링 과정입니다. 새로운 기능이 추가되거나 요구사항이 변경될 때마다 코드 구조를 재평가하고 필요에 따라 조정해 나가는 것이 중요합니다. “작동하는 코드”보다 “좋은 코드”를 작성하는 습관을 들이면, 장기적으로 더 효율적이고 즐거운 개발 경험을 할 수 있을 것입니다.
답글 남기기