개발
친구 추천 서비스, 왜 무너졌을까? 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단계 분리 가이드
- 역할 라벨링
클래스·메서드 옆에 포스트잇을 붙여 “데이터 추출”, “랭킹”, “알림”처럼 역할을 명시했습니다. 두 개 이상이면 분리 대상. - 인터페이스 추출
- CandidateGenerator
- ScoreCalculator
- NotificationSender
- RecommendationLogger
각각을 작은 클래스로 분리하고 전략 패턴으로 교체 가능하게 설계.
- 의존성 주입(DI)
스프링 Bean으로 구현체를 주입해 고수준 서비스가 구체 클래스에 직접 의존하지 않도록 역전(DIP) 시켰습니다. - 계약 테스트 추가
상위 타입에 대한 Mock 테스트를 먼저 작성해 하위 구현을 안전하게 교체. - 점진적 배포 + Feature Toggle
새 구조를 기능 토글로 감싸 트래픽 10% → 100%로 단계적 전환.
4. 분리 후 얻은 효과
- 릴리스 주기 2주 → 3일: 알림 채널 추가도 코드 수정 없이 구현체만 추가
- 장애율 –40%: 랭킹 알고리즘 실험이 다른 모듈에 영향 없음
- 테스트 커버리지 +25%: Mock 주입이 쉬워져 빠른 단위 테스트 가능
무엇보다 “추천 품질 팀”과 “알림 팀”이 서로의 코드에 간섭하지 않게 되면서 조직 생산성이 눈에 띄게 상승했습니다.
5. 실무 팁
- 메트릭으로 감시: 클래스 크기(Lines of Code)·메서드 복잡도를 SonarQube 알람으로 설정.
- 로그 스키마 버전 관리: 로거를 분리하면 스키마 변경을 독립적으로 배포 가능.
- A/B 테스트 자동화: 후보 생성기·점수 계산기를 전략 패턴으로 바꿔 실험 스위치를 손쉽게 켜기.
반응형