프로젝트 초기에 합류하면서 전체 시스템과 DB 스키마를 한 번에 파악하기는 쉽지 않은 상태였다. 기존 알람 서비스는 단일 회사, 단일 알람 유형을 기준으로 설계되어 있었고, 메시지 채널 역시 SMS와 Slack 정도로 한정되어 있었다.
코드를 살펴보며 몇 가지 개선이 필요하다고 느낀 점들이 있었다.
이런 상황에서 다음 요구사항을 만족해야 했다.
반복적이고 기계적인 작업은 Claude Code의 도움을 받아 모델 파일을 역할 단위로 분리했다. 덕분에 이후 변경 시 영향 범위를 줄일 수 있었다.
기존 로직을 작은 단위의 메서드나 클래스로 나누고, 상위 레벨에서는 흐름만 읽을 수 있도록 위임 구조로 정리했다. 세부 구현을 보지 않아도 메서드 이름만으로 동작을 이해할 수 있게 하는 것이 목표였다.
운영 환경에서 로그를 기준으로 문제를 추적하는 경우가 많기 때문에, 로그 포맷과 메시지 규칙을 정하고 코드 전반에 적용했다.
파이썬 데코레이터를 활용해 비동기 함수의 재시도 로직을 공통화했다. 이를 통해 중복 코드를 줄이고, 재시도 정책을 한 곳에서 관리할 수 있게 되었다.
@async_retry(max_retries=2, delay=30)
async def send_message():
...
전체 시스템을 한 번에 이해하려 하기보다는, 이번 작업에 필요한 입력(In)과 출력(Out) 에만 집중했다. 백엔드 개발자로서 어떤 데이터를 받아야 하고, 어떤 데이터를 제공해야 하는지에 초점을 맞췄다.
이는 인터페이스를 먼저 정의하고 구현을 진행하는 방식과 유사했고, 불필요한 복잡도를 줄이는 데 도움이 되었다.
기존 기능이 그대로 동작하는지, 새로 추가한 기능이 정상적으로 동작하는지를 최우선으로 두었다. 그 결과 파일 수는 다소 늘어났지만, 기한 내에 기능을 추가하고 배포하는 것이 가능했다.
기능 추가 이후, 늘어난 파일과 코드 구조를 다시 정리하는 단계에 들어갔다.
파일명과 클래스명에 prefix/postfix를 붙여 역할이 드러나도록 정리했다. 또한 메서드 파라미터가 많아지는 부분은 DTO로 묶어 가독성을 개선했다.
파이썬의 TypedDict를 활용해 키 접근 시 IDE의 도움을 받을 수 있도록 했다.
class FooParam(TypedDict):
pass
구조를 정리하면서 실행 흐름에 대한 개념도 자연스럽게 정리되었다.
파이썬의 Protocol을 활용해 인터페이스를 정의했다. 상속 기반 구조보다 구현 의존성을 줄일 수 있고, 협업 시에도 시그니처만 보고 구현이 가능하다는 장점이 있었다.
class Sender(Protocol):
@async_retry
async def send_message(self, message: dict) -> bool: ...
객체 생성은 팩토리에서 담당하고, 실행 로직에서는 의존성 주입을 통해 필요한 구현체를 전달받도록 구성했다. 이 구조는 테스트 시에도 유연하게 활용할 수 있었다.
환경 설정은 ConfigMap 등을 통해 주입받아 회사별로 필요한 Executor만 생성하도록 했다.

위 이미지는 AI가 그린거라 아주 약간 부정확하다. 각 채널은 자신만의 Sender와 Formatter를 갖는다. 그리고 Executor는 여러 개다.
이번 작업에서 특정 디자인 패턴을 의식하고 적용하기보다는, 문제를 해결하는 과정에서 자연스럽게 패턴에 가까운 구조가 만들어졌다.
개인적으로는 패턴 이름을 먼저 정해두기보다, 필요에 의해 구조를 만들다 보면 결과적으로 패턴이 되는 접근을 선호한다. 기술이나 패턴은 목적이 아니라 수단이라는 점을 다시 한 번 느낀 작업이었다.