[Python] 기존에 작성된 코드를 쉽게 수정하기! (상속)
소프트웨어 개발에서 기존 코드를 수정하는 것과 확장하는 것 사이에는 중요한 차이가 있습니다.
실제 프로젝트에서 남들이 작성한, 혹은 기존에 작성된 클래스를 확장하여
로깅 기능을 추가한 사례를 통해 코드 유지보수와 확장의 베스트 프랙티스를 살펴보겠습니다.
상속을 통한 코드 확장의 이점
코드를 수정할 때 기존 클래스를 직접 변경하는 대신 상속을 통해 새로운 기능을 추가하면 다음과 같은 이점이 있습니다:
- 원본 코드 보존 - 라이브러리나 프레임워크의 코드를 직접 수정하지 않아 업데이트 충돌을 방지합니다.
- 명확한 기능 추가 - 어떤 기능이 추가되었는지 명확하게 알 수 있습니다.
- 유연한 확장 - 필요에 따라 특정 메서드만 오버라이드하여 기능을 확장할 수 있습니다.
- 쉬운 롤백 - 문제 발생 시 원래 클래스로 쉽게 되돌릴 수 있습니다.
실제 구현 사례: 로깅 기능 추가
https://github.com/juanmc2005/diart 에 올라온 기본 예제코드입니다.
from diart import SpeakerDiarization
from diart.sources import MicrophoneAudioSource
from diart.inference import StreamingInference
from diart.sinks import RTTMWriter
logger.info("Starting diart pipeline")
pipeline = SpeakerDiarization()
mic = MicrophoneAudioSource(device=7) # 이 클래스의 메스드를 수정하고 싶은 경우
inference = StreamingInference(pipeline, mic, do_plot=True)
inference.attach_observers(RTTMWriter(mic.uri, "./output/output.rttm"))
prediction = inference()
이 코드가 정확히 어떻게 동작하는지 살펴보기 위해 로그를 추가하려고합니다.
MicrophoneAudioSource 클래스의 특정 메서드에도 로그를 추가하려고 하는데 직접 이 코드를 수정할 수 도 있지만, 이후에 어느 부분을 수정했는지 바로바로 파악하기가 불편해집니다.
그러므로 우리가 원하는 부분만 오버라이딩을 통해 수정을 하면 코드를 깔끔하게 유지할 수 있게됩니다.
메서드 오버라이딩을 통한 로깅 기능 구현
from diart import SpeakerDiarization
from diart.sources import MicrophoneAudioSource
from diart.inference import StreamingInference
from diart.sinks import RTTMWriter
class LogMicrophoneAudioSource(MicrophoneAudioSource):
# 상속으로 통해 원하는 메서드 쉽게 수정
def _read_callback(self, samples, *args):
logger.info(f"samples: {samples}")
self._queue.put_nowait(samples[:, [0]].T)
pipeline = SpeakerDiarization()
mic = LogMicrophoneAudioSource(device=7)
inference = StreamingInference(pipeline, mic, do_plot=True)
inference.attach_observers(RTTMWriter(mic.uri, "./output/output.rttm"))
prediction = inference()
로그와 관련된 코드를 코드 앞쪽에 추가하고
오버라이딩을 하기 위해 자식 클래스(LogMicrophoneAudioSource)를 정의합니다.
_read_callback 이라는 메서드는 원래 다음과 같이 작성되어있었습니다.
로깅 결과 예시
확장된 클래스를 사용하면 오디오 스트림 처리 과정이 로그 파일에 기록됩니다:
(_read_callback 메서드가 호출되면 로그파일에 sample 이 출력됩니다.)
마이크로서비스 아키텍처에서의 코드 확장 중요성마이크로서비스 아키텍처에서는
다음과 같은 이유로 상속을 통한 코드 확장 전략이 더욱 중요합니다:
1. 서비스 독립성 유지
마이크로서비스는 각 서비스가 독립적으로 개발, 배포, 확장될 수 있어야 합니다.
기존 라이브러리나 공통 코드를 직접 수정하는 대신 상속을 통해 확장하면:
- 공통 라이브러리의 기능은 그대로 유지하면서 각 서비스에 맞는 기능을 추가할 수 있습니다
- 공통 코드의 업데이트가 있어도 확장된 클래스에 영향을 최소화할 수 있습니다
- 서비스별 맞춤형 기능을 구현하면서도 기본 인터페이스를 유지할 수 있습니다
2. 분산 로깅과 모니터링 강화
마이크로서비스 환경에서는 분산 시스템의 로깅과 모니터링이 매우 중요합니다. 위 예시에서처럼 로깅 기능을 확장함으로써:
class LoggingMicrophoneAudioSource(MicrophoneAudioSource):
def _read_callback(self, samples, *args):
logger.info(f"Device {self.device} received audio: shape={samples.shape}")
# 원래 기능 유지
self._queue.put_nowait(samples[:, [0]].T)
- 각 서비스에서 필요한 상세 로깅을 추가할 수 있습니다
- 트러블슈팅 및 성능 모니터링이 용이해집니다
- 서비스 간 통신 및 데이터 흐름을 추적할 수 있습니다
3. 점진적 기능 개선 및 마이그레이션
마이크로서비스 아키텍처에서는 시스템 전체를 한 번에 업그레이드하기보다 점진적으로 개선하는 전략이 중요합니다:
- 상속을 통한 확장으로 새 기능을 점진적으로 도입할 수 있습니다
- A/B 테스팅을 쉽게 구현할 수 있습니다 (일부 서비스는 확장된 클래스 사용, 일부는 원래 클래스 사용)
- 문제 발생 시 빠르게 롤백하거나 기존 구현으로 전환할 수 있습니다
4. 공통 인터페이스 유지를 통한 서비스 간 통합 용이성
마이크로서비스 환경에서는 서비스 간 통합과 상호작용이 중요합니다:
# 서비스 A에서는 기본 클래스 사용
audio_source_a = MicrophoneAudioSource(device=1)
# 서비스 B에서는 확장된 클래스 사용하지만 동일한 인터페이스 유지
audio_source_b = LoggingMicrophoneAudioSource(device=2)
# 두 서비스 모두 동일한 파이프라인에 연결 가능
inference_a = StreamingInference(pipeline, audio_source_a)
inference_b = StreamingInference(pipeline, audio_source_b)
- 공통 인터페이스를 유지하면서도 각 서비스에 맞는 맞춤형 구현이 가능합니다
- 서비스 간 통합이 용이해집니다
- 각 서비스 팀이 독립적으로 발전시킬 수 있습니다
결론
상속을 통한 기존 클래스의 확장은 코드의 안정성과 유지보수성을 높이는 효과적인 방법입니다. 원본 코드는 그대로 유지하면서 새로운 기능을 추가할 수 있어 기능의 분리와 책임의 명확한 구분이 가능합니다. 이는 특히 마이크로서비스 패턴을 적용한 시스템에서 각 서비스의 독립성과 확장성을 유지하는 데 중요한 전략입니다.코드 확장성을 고려한 설계는 단기적으로는 약간의 추가 작업이 필요할 수 있지만, 장기적으로는 유지보수 비용을 크게 줄이고 시스템의 안정성을 향상시키는 투자입니다.