[인턴] CH02. 효율적인 설계의 첫걸음: 디자인 패턴 도입기
디자인 패턴 도입
기존에 회사에 존재하던 코드인 LSTM 예측 코드는 프로젝트의 다양한 요구사항 중 하나에 해당할 뿐이기 때문에 새로운 요구사항을 충족하기 위해서는 단순히 기능을 구현하는 것 뿐만 아니라 프로젝트 설계를 해야했다. 즉, 요구사항을 파악했으니 이를 기반으로 설계와 구상을 진행해야했다.
이전 글에서도 언급했듯이 해당 프로젝트의 최종 목표는 상업화이기 때문에 복잡한 요구사항을 효율적으로 설계하고, 유지보수 가능한 구조를 만드는 것을 목표로 두었다. 왜냐하면 내가 회사를 떠나게 된다해도 이후에 다른 담당자가 코드를 쉽게 이해하고 수정할 수 있어야하기 때문이다. 그래서 나는 문서화와 함께 디자인 패턴 도입을 하기로 결정했다.
이 중 이번 글에서는 도입한 디자인 패턴과 왜 이 패턴을 선택했는지에 대해 설명하고자 한다.
많은 디자인 패턴 중 왜 퍼사드(Facade) 패턴인가?
소프트웨어 설계 문제를 해결하기 위해 다양한 디자인 패턴이 존재한다. 이 중에서도 퍼사드 패턴(Facade Pattern)을 선택하게 된 이유는 프로젝트의 본질적인 요구사항과 맞아떨어지기 때문이다.
퍼사드 패턴을 간단히 설명하자면 복잡한 서브시스템을 단일 인터페이스로 감싸는 디자인 패턴이다.
위의 이미지는 퍼사드 패턴을 이해하기 쉽게 해주는 이미지로, Facade 객체가 내부의 여러 클래스(기능)를 감싸고 있으며 외부(Client)에서는 Facade 객체를 통해 복잡한 기능을 단순하게 실행할 수 있다. 즉, 클라이언트는 내부의 복잡한 동작을 의식하지 않고 단순화된 인터페이스를 통해 시스템을 사용할 수 있다는 것이다.
코드 관점에서 보면, 코드 상에 어떠한 복잡한 로직의 코드가 존재한다면 main 함수에서 모두 실행하는 것이 아니라 각 로직을 독립적인 함수나 클래스로 분리하여 관리 및 main 함수의 코드를 간단하게 구성하는 것을 객체 지향 프로그래밍 관점으로 치환한 것이 퍼사드 패턴이다.
그럼 퍼사드 패턴이 왜 프로젝트의 요구사항과 맞아 떨어지는 걸까? 이거에 대한 답변은 다음과 같다.
- 다양한 서브시스템 통합의 필요성
프로젝트는 데이터 송/수신, 이상치 검출, LSTM 학습 및 예측 등 다양한 독립적인 기능들로 구성되어 있기 때문에 이를 하나의 통합된 파이프라인으로 연결하기 위해서는 단순하고 효율적인 접근 방식이 필요함 - 사용성 / 접근성 고려
클라이언트는 시스템 관리자 혹은 마지막에 임베디드 기기 연결을 담당하는 타 업체 담당자로, 내부의 복잡한 동작을 이해할 필요가 없기 때문에 단순화된 인터페이스를 통한 간단한 명령어로 프로세스를 실행할 수 있음 - 유지보수와 확장성 고려
서브시스템의 세부 구현은 외부에 감추고, 인터페이스를 통해 유지보수가 용이하며 새로운 기능이 추가되더라도 새로운 기능을 퍼사드에 연결하면 됨 - 전체 시스템 빠르게 이해
퍼사드 패턴을 통해 내부 구현을 추상화했기 때문에 다음 담당자에게 복잡한 서브시스템의 세부 동작을 모두 설명할 필요 없이 객체와 메서드의 역할, 호출 순서를 알면 빠르게 전체 시스템을 이해할 수 있는 구조임
정리하자면 복잡한 기능들을 직관적으로 통합하고, 단순한 사용성과 접근성을 제공하며, 유지보수와 확장성을 보장 및 시스템에 대한 이해를 높이기 위해 이를 만족하는 퍼사드 패턴을 도입했다.
구현 방식
위에서 개념을 설명했으니 이제 프로젝트에 코드 상으로 어떻게 구현하였는지 설명하겠다!
1. 디렉토리 구조
프로젝트의 전체적인 디렉토리 구조를 간략히 소개하자면 src에 존재하는 서브시스템(기능)들을 scheduling.py가 Facade 역할을 하여 통합하는 방식으로 구성했다.
project_root/
├── main.py # 단일 진입점
├── src/ # 스크립트 일부만 표시
│ ├── scheduling.py # Facade 역할: 서브시스템 통합
│ ├── transform.py # 데이터 추출 및 전처리
│ ├── outlier_detection.py # 이상치 탐지
│ ├── lstm_prediction.py # LSTM 예측
2. main.py: 단일 진입점
main.py는 단일 진입점으로 동작하며 SchedulerManager 클래스의 run 메서드를 호출하는 역할만 수행한다. 클라이언트는 해당 스크립트를 명령어(python main.py)로 실행함으로써 전체 기능을 수행할 수 있는데, 이는 복잡한 데이터 처리 로직은 내부적으로 처리되어있어 클라이언트가 간단하게 시스템을 실행할 수 있다.
from src.scheduling import SchedulerManager
if __name__ == "__main__":
# SchedulerManager 인스턴스 생성
manager = SchedulerManager()
# 스케줄러 시작
manager.run()
3. scheduling.py: 퍼사드
scheduling.py는 퍼사드 패턴의 핵심으로 퍼사드 역할을 담당하며, 서브시스템을 통합하고 실행 순서를 정의한다.
scheduling.py에서 SchedulerManager 클래스는 퍼사드로서 각 서브시스템(Transform, OutierDetection, LstmPrediction)을 통합한다. 코드를 확인해보면 실행 순서가 명확히 정의되어 있고, 새로운 기능 추가 시 서브시스템을 구현한 후 SchedulerManager에 통합하면 된다.
# src/scheduling.py
from src.transform import Transform
from src.outlier_detection import OutlierDetection
from src.lstm_prediction import LstmPrediction
class ScheduleManager:
def __init__(self):
self.transform = Transform()
self.outlier_detection = OutlierDetection()
self.prediction = LstmPrediction()
def run(self):
print("Start")
self.transform.extract_data()
self.outlier_detection.detect_outliers()
4. 서브시스템
각 서브시스템은 독립적으로 구현되어 있으며 특정 역할을 수행한다. 예를 들어, 데이터 추출 및 전처리를 담당하는 Transform 클래스는 독립적인 모듈로 유닛 테스트가 가능하며, 오류가 발생할 시 해당 메서드만 수정하면 된다.
# src/transform.py
class Transform:
def extract_data(self):
print("Extracting and preprocessing data")
# 데이터 추출 및 전처리 로직
디자인 패턴 도입 후기
처음부터 설계를 직접 진행하고, 디자인 패턴을 적용한 것은 이번이 처음이었다. 단순히 기능 구현에 그치는 것이 아니라 복잡한 요구사항을 구조화하고 효율적으로 관리할 수 있는 설계를 고민한 끝에 퍼사드 패턴을 적용하게 되었다.
사실 코드를 작성한 나조차도 프로젝트 초반에 구현 도중 흐름을 놓치는 경우가 종종 있었지만, 디자인 패턴 도입이 흐름을 빠르게 잡게 도와주었다. 팀장님께 코드를 설명할 때도 구조가 명확하게 드러나기 때문에 설명하기가 쉬웠고, 이 덕분에 다른 사람이 코드를 볼 때도 흐름 파악이 수월할 것이라고 판단했다
이번 경험은 단순히 동작하는 코드를 넘어, 팀과 사용자에게 가치를 제공하는 코드를 작성하는 것의 중요성을 깨닫게 해주었다. 엔지니어의 본질은 프로덕트를 잘 만드는 사람이라는 말처럼 이 경험은 나에게 더 나은 엔지니어로 성장하는 발판이 된 것 같다.