스택큐힙리스트

친구 추천 서비스, 왜 무너졌을까? SRP 위반 리팩터링 실전 해부 본문

개발

친구 추천 서비스, 왜 무너졌을까? SRP 위반 리팩터링 실전 해부

스택큐힙리스트 2025. 7. 13. 19:58
반응형

“친구 추천 코드를 뜯어보니 추천·랭킹·알림·로깅까지 한 클래스에 다 들어 있었다.”

SNS 서비스에서 친구 추천은 트래픽을 좌우하는 핵심 기능입니다. 그런데 출시 1년 만에 버그가 폭발하고 배포가 두려워진 팀이 있었습니다. 원인은 단 하나, SRP(단일 책임 원칙) 위반이었습니다. SRP란 “클래스가 변경돼야 할 이유는 하나여야 한다”는 원칙이죠 이번 글에서는 실제로 벌어졌던 ‘친구 추천 서비스’의 SRP 위반 패턴을 해부하고, 단계별 분리 전략을 살펴봅니다.


1. 실패의 전조 – 거대 FriendRecommendationService

초기 구현은 다음과 같았습니다.

  • 데이터 수집: 사용자 그래프·활동 로그·팔로잉 기록 한꺼번에 조회
  • 후보 생성 + 랭킹: 협업 필터링 점수 계산 후 랭킹 적용
  • 알림 전송: 푸시·이메일 발송
  • 모니터링/로깅: 추천 결과·클릭률을 로그로 저장

네 가지 책임이 한 클래스에 얽히면서 “새 랭킹 알고리즘” 한 줄을 바꾸려 할 때마다 알림 로직과 로그 스키마까지 동시에 깨지는 참사가 발생했습니다. 대용량 트래픽을 받는 추천 시스템은 “후보 생성 단계·랭킹 단계로 나눠라”는 것이 정석인데 이를 무시한 결과였습니다.


2. 코드 냄새 체크리스트로 본 SRP 위반

리팩터링 전에 SRP 코드 냄새를 스캔했습니다 

  • if/else 지옥: ‘ABTestType’에 따라 알고리즘이 분기
  • UnsupportedOperationException: 채널이 없는 테스트 환경 때문에 예외 던짐
  • 300줄 메서드: recommend() 내부에서 그래프 DB 쿼리 → 점수 계산 → 푸시 발송까지 처리

모두 “변경 이유가 여럿”이라는 SRP 위반 신호였습니다.


3. 5단계 분리 가이드

  1. 역할 라벨링
    클래스·메서드 옆에 포스트잇을 붙여 “데이터 추출”, “랭킹”, “알림”처럼 역할을 명시했습니다. 두 개 이상이면 분리 대상.
  2. 인터페이스 추출
    • CandidateGenerator
    • ScoreCalculator
    • NotificationSender
    • RecommendationLogger
      각각을 작은 클래스로 분리하고 전략 패턴으로 교체 가능하게 설계.
  3. 의존성 주입(DI)
    스프링 Bean으로 구현체를 주입해 고수준 서비스가 구체 클래스에 직접 의존하지 않도록 역전(DIP) 시켰습니다.
  4. 계약 테스트 추가
    상위 타입에 대한 Mock 테스트를 먼저 작성해 하위 구현을 안전하게 교체.
  5. 점진적 배포 + Feature Toggle
    새 구조를 기능 토글로 감싸 트래픽 10% → 100%로 단계적 전환.

4. 분리 후 얻은 효과

  • 릴리스 주기 2주 → 3일: 알림 채널 추가도 코드 수정 없이 구현체만 추가
  • 장애율 –40%: 랭킹 알고리즘 실험이 다른 모듈에 영향 없음
  • 테스트 커버리지 +25%: Mock 주입이 쉬워져 빠른 단위 테스트 가능

무엇보다 “추천 품질 팀”과 “알림 팀”이 서로의 코드에 간섭하지 않게 되면서 조직 생산성이 눈에 띄게 상승했습니다.


5. 실무 팁

  • 메트릭으로 감시: 클래스 크기(Lines of Code)·메서드 복잡도를 SonarQube 알람으로 설정.
  • 로그 스키마 버전 관리: 로거를 분리하면 스키마 변경을 독립적으로 배포 가능.
  • A/B 테스트 자동화: 후보 생성기·점수 계산기를 전략 패턴으로 바꿔 실험 스위치를 손쉽게 켜기.
반응형
Comments