200번 리팩토링으로 확장성 있는 알람 서비스 만들기

dasd412·2025년 12월 25일

실무 문제 해결

목록 보기
25/27

상황

프로젝트 초기에 합류하면서 전체 시스템과 DB 스키마를 한 번에 파악하기는 쉽지 않은 상태였다. 기존 알람 서비스는 단일 회사, 단일 알람 유형을 기준으로 설계되어 있었고, 메시지 채널 역시 SMS와 Slack 정도로 한정되어 있었다.

코드를 살펴보며 몇 가지 개선이 필요하다고 느낀 점들이 있었다.

  • DB 모델이 하나의 파일에 모여 있어 협업 시 Git 충돌이 잦았다.
  • 하나의 서비스 파일이 여러 책임을 동시에 맡고 있어 코드의 역할을 파악하기 어려웠다.
  • 로그 형식이 통일되어 있지 않아 운영 환경에서 추적이 쉽지 않았다.
  • 재시도 로직이 존재하긴 했지만, 유사한 코드가 여러 곳에 반복되고 있었다.
  • 객체 생성이 내부에서 직접 이루어져 확장이나 테스트에 제약이 있었다.

이런 상황에서 다음 요구사항을 만족해야 했다.

  • 알람 종류 추가
  • 이메일 채널 추가
  • 회사별로 서로 다른 알람/채널 구성이 가능해야 함
  • 향후 알람, 채널이 더 늘어나더라도 구조 변경이 최소화될 것

해결

1. 기존 코드 정리

1) 모델 분리

반복적이고 기계적인 작업은 Claude Code의 도움을 받아 모델 파일을 역할 단위로 분리했다. 덕분에 이후 변경 시 영향 범위를 줄일 수 있었다.

2) 함수 추출 중심의 리팩토링

기존 로직을 작은 단위의 메서드나 클래스로 나누고, 상위 레벨에서는 흐름만 읽을 수 있도록 위임 구조로 정리했다. 세부 구현을 보지 않아도 메서드 이름만으로 동작을 이해할 수 있게 하는 것이 목표였다.

3) 로그 형식 통일

운영 환경에서 로그를 기준으로 문제를 추적하는 경우가 많기 때문에, 로그 포맷과 메시지 규칙을 정하고 코드 전반에 적용했다.

4) 재시도 데코레이터 도입

파이썬 데코레이터를 활용해 비동기 함수의 재시도 로직을 공통화했다. 이를 통해 중복 코드를 줄이고, 재시도 정책을 한 곳에서 관리할 수 있게 되었다.

@async_retry(max_retries=2, delay=30)
async def send_message():
    ...

2. 기존 구조를 유지한 채 확장

1) 필요한 정보에 집중하기

전체 시스템을 한 번에 이해하려 하기보다는, 이번 작업에 필요한 입력(In)과 출력(Out) 에만 집중했다. 백엔드 개발자로서 어떤 데이터를 받아야 하고, 어떤 데이터를 제공해야 하는지에 초점을 맞췄다.

이는 인터페이스를 먼저 정의하고 구현을 진행하는 방식과 유사했고, 불필요한 복잡도를 줄이는 데 도움이 되었다.

2) 안정적인 동작을 우선

기존 기능이 그대로 동작하는지, 새로 추가한 기능이 정상적으로 동작하는지를 최우선으로 두었다. 그 결과 파일 수는 다소 늘어났지만, 기한 내에 기능을 추가하고 배포하는 것이 가능했다.


3. 확장 후 구조 정리

기능 추가 이후, 늘어난 파일과 코드 구조를 다시 정리하는 단계에 들어갔다.

1) 네이밍 규칙과 파라미터 정리

파일명과 클래스명에 prefix/postfix를 붙여 역할이 드러나도록 정리했다. 또한 메서드 파라미터가 많아지는 부분은 DTO로 묶어 가독성을 개선했다.

파이썬의 TypedDict를 활용해 키 접근 시 IDE의 도움을 받을 수 있도록 했다.

class FooParam(TypedDict):
    pass

2) 개념과 위계 정립

구조를 정리하면서 실행 흐름에 대한 개념도 자연스럽게 정리되었다.

  • Entry: 실행 시작점
  • Executor: 알람 실행 단위
  • Channel: SMS / Email / Slack
  • Sender: 실제 전송 구현체

3) Protocol 도입

파이썬의 Protocol을 활용해 인터페이스를 정의했다. 상속 기반 구조보다 구현 의존성을 줄일 수 있고, 협업 시에도 시그니처만 보고 구현이 가능하다는 장점이 있었다.

class Sender(Protocol):
    @async_retry
    async def send_message(self, message: dict) -> bool: ...

4) 의존성 주입과 팩토리

객체 생성은 팩토리에서 담당하고, 실행 로직에서는 의존성 주입을 통해 필요한 구현체를 전달받도록 구성했다. 이 구조는 테스트 시에도 유연하게 활용할 수 있었다.

환경 설정은 ConfigMap 등을 통해 주입받아 회사별로 필요한 Executor만 생성하도록 했다.

5) 아키텍처


위 이미지는 AI가 그린거라 아주 약간 부정확하다. 각 채널은 자신만의 Sender와 Formatter를 갖는다. 그리고 Executor는 여러 개다.


결과

  • 모델과 책임 단위로 코드 분리
  • 네이밍 규칙을 통한 가독성 개선
  • Executor → Channel → Sender 구조 도입
  • 재시도 로직 공통화
  • 의존성 주입 기반 설계
  • 환경 설정 기반의 유연한 구성
  • Protocol과 디자인 패턴을 활용한 확장성 확보

디자인 패턴에 대해

이번 작업에서 특정 디자인 패턴을 의식하고 적용하기보다는, 문제를 해결하는 과정에서 자연스럽게 패턴에 가까운 구조가 만들어졌다.

개인적으로는 패턴 이름을 먼저 정해두기보다, 필요에 의해 구조를 만들다 보면 결과적으로 패턴이 되는 접근을 선호한다. 기술이나 패턴은 목적이 아니라 수단이라는 점을 다시 한 번 느낀 작업이었다.


profile
아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다.

0개의 댓글