SRP (Single Responsibility Principle) — SOLID의 첫 번째 원칙으로, 각 클래스 또는 모듈은 변경할 이유가 단 하나여야 한다고 균정합니다. 이 원칙은 로버트 마틴이 저서 Clean Architecture (2017)에서 정립하였고 모듈식 디자인의 기초가 되었습니다. 이 책에 따르면, SRP 적용은 컴포넌트 결합을 직접 줄이고 기능 수정 시 연쇄적 변경을 제거합니다.
주요 포인트
SRP(Single Responsibility Principle)은 단일 책임 원칙으로, 각 클래스 또는 모듈은 변경할 이유가 단 하나여야 한다고 균정합니다. 이것은 클래스가 단 하나의 작업만 수행해야 한다는 뜻이 아닙니다. 하나의 액터에 대한 단일 책임으로 결합된 관련 작업의 그룹을 말합니다.
로버트 마틴은 SRP를 액터 관점에서 재정의했습니다: 클래스는 하나의 이해관계자 또는 한 그룹의 사람들의 요청에 의해서만 변경되어야 합니다. 두 다른 액터가 동일한 클래스에 변경을 요구하는 경우, 책임이 올바르게 분담되지 않은 것입니다.
예를 들어, 급여 계산(회계 부서 요청)과 보고서 생성(관리자 요청)을 동시에 하는 Employee 클래스는 SRP를 위반합니다. 계산 규칙의 변경이 보고서 생성에 영향을 미칠 수 있고 그 역도 마찬가지입니다.
모듈은 변경할 이유가 하나이며 오직 하나여야 합니다. 변경 이유는 액터(요구사항을 시작하는 사람 또는 시스템)에 의해 결정됩니다. 다른 액터들의 요구사항이 하나의 모듈 변경으로 이어지는 경우, 해당 모듈은 SRP를 위반합니다.
액터의 개념은 SRP를 추상적인 권고가 아닌 실용적인 아키텍처 분석 도구로 만듭니다. 시스템을 설계할 때는 “누가 이 코드를 변경할 것을 요청할 것인가?”라고 물어보십시오. 답변에 두 명 이상의 이해관계자가 있다면, 책임을 분리해야 합니다.
단일 책임은 한 가지 이유로 변경되는 메소드를 그룹화하여 구현됩니다. 클래스는 모든 경우에 사용할 수 있는 마실트칠이 아닌, 관련 로직의 집합처가 됩니다. 이를 통해 코드 이해가 단순화됩니다: 개발자는 클래스를 보면 즉시 그 목적을 이할 수 있습니다.
SRP의 메커니즘은 단일 변경 축 규칙에 기초합니다. 기능이 독립적인 이유로 변경할 수 있다면, 별도의 클래스로 분리되어야 합니다. 이러한 클래스 간의 연결은 컴포지션이나 위임을 통해 구축됩니다.
SRP 위반은 God Object로 나타난니다 — 다양한 데이터를 다루는 수십 개의 메소드가 있는 클래스입니다. 이러한 클래스는 테스트하기 어렵습니다 — 한 메소드를 테스트하려면 다른 모든 메소드에 대해 환경을 설정해야 합니다. 한 책임을 변경하면 다른 것이 깨질 수 있어 코드가 부상해집니다.
실제로 SRP는 개발자가 “이 코드는 어디 있나요?”라는 질문에 답하는 데 도움이 됩니다. 각 책임이 각기 클래스에 분리되어 있다면, 올바른 파일을 찾는 데 몇 초가 걸릴 뿐입니다. MVVM 아키텍처의 Android 프로젝트에서 UserViewModel은 사용자 화면 상태만을, UserRepository는 데이터 찎기를 담당합니다. 캐시 로직을 찾는 개발자는 ViewModel이 아닌 UserCacheRepository로 갑니다. 이러한 코드 조직은 신규 팀 멤버의 옹보딩을 가속하고 리팩토링 중에 발생하는 오류를 줄입니다.
모바일 개발은 코드 모듈성에 특별한 요구사항을 제기합니다. Android Fragment나 iOS ViewController는 종종 로직의 흑석처가 됩니다: 탭 처리, API 호출, 응답 파싱, UI 업데이트 — 모두 하나의 클래스에서. SRP는 이러한 책임을 분리할 것을 요구합니다.
Android 아키텍처에서 SRP는 Jetpack에 관한 Google의 게이드라인에 포함되어 있습니다: ViewModel은 화면 상태를, Repository는 데이터를, UseCase는 비즈니스 로직을 담당합니다. iOS 개발에서는 MVVM과 Coordinator 패턴이 동일한 로직을 따릅니다.
모바일 프로젝트에서 SRP를 따르면 계량 가능한 이점이 있습니다: 클래스 크기 40-60% 감소, 코드 리뷰 시간 단축, 신규 기능 추가 시 회귀 버그 감소. 분리된 모듈은 단위 테스트로 커버하기 쉬우며 다른 화면에서 재사용하기 쉽습니다.
SRP를 따르는 클래스의 단위 테스트는 멤 개체와 구성이 먼 필요하지 않습니다. 클래스에 단일 책임이 있다면 의존성이 제한적입니다. 테스트는 여러 관련없는 시나리오의 조합이 아닌 하나의 동작을 검증합니다.
Google Testing Blog(2023) 보고서에 따르면, 단일 책임의 클래스는 집계 클래스에 비해 35% 더 높은 테스트 커버리지를 보여줍니다. 개발자들은 작고 이해하기 쉬운 모듈에 대해 더 기꾸이 테스트를 작성합니다.
SRP를 위반하는 일반적인 Android 클래스를 고찌해 봅시다 — 데이터를 로드하고, 응답을 파싱하고, UI를 업데이트합니다. 리팩토링 후에는 각 책임이 각기 컴포넌트로 분리됩니다.
// SRP 위반: 하나의 클래스가 모든 것을 함
class BadUserProfileActivity {
fun loadUser(userId: Int) {
// HTTP 요청
// JSON 파싱
// UI 업데이트
// DB 저장
}
}
// SRP 적용 후
class UserRepository {
fun getUser(userId: Int): User
}
class UserViewModel {
private val repo: UserRepository
fun loadUser(userId: Int) { }
}
class UserProfileFragment {
fun render(user: User) { }
}
네트워크 레이어와 디스플레이를 분리한 iOS Swift의 유사한 예:
// SRP 위반: ViewController가 데이터와 UI를 관리
class BadProfileViewController: UIViewController {
func viewDidLoad() {
// URLSession 요청
// JSON 디코드
// label 업데이트
}
}
// SRP 적용 후
protocol UserServiceProtocol {
func fetchUser(id: Int) async throws -> User
}
class ProfileViewModel {
private let service: UserServiceProtocol
func loadProfile(id: Int) { }
}
class ProfileViewController: UIViewController {
func display(user: User) { }
}
SRP 리팩토링은 아키텍처를 복잡하게 만들지 않습니다 — 책임을 재분배할 뿐입니다. 중복을 제거하면 코드 양이 줄어들 수도 있습니다. 각 새 클래스는 명확한 목적이 있고 독립적으로 개발할 수 있습니다.
컴포지션은 상속이 필요 없는 결합을 만드는 곳에서 SRP를 유지하는 데 도움이 됩니다. 수십 개의 메소드가 있는 슬퍼클래스 대신 서브클래스는 생성자를 통해 전문 객체들의 집합을 받습니다. 각 객체는 각기 기능에 대해 책임이 있습니다.
Android 개발에서 Decorator 패턴은 원본 클래스를 수정하지 않고도 책임을 추가할 수 있게 합니다. iOS에서 네트워킹 레이어의 Middleware 체인은 로깅, 캐시, 인증을 별도 모듈로 분리합니다.
가장 흔한 위반은 God Class입니다: 데이터베이스를 관리하고, 알림을 보내고, 보고서를 생성하고, 사용자 입력을 처리하는 클래스입니다. 이러한 클래스는 프로젝트의 병목이 됩니다: 어떤 변경이라도 완전한 회귀 테스트가 필요합니다.
모바일 개발에서 Activity, Fragment 또는 ViewController에서 비즈니스 로직과 UI 로직을 혼합하면 SRP 위반이 발생합니다. onClickListener가 데이터를 동시에 검증하고, API를 호출하고, 버튼 가시성을 업데이트할 때 — 이것은 단일 책임 원칙의 직접적인 위반입니다.
SRP 위반의 결과는 다음과 같습니다: 병렬 개발의 어려움(한 파일에서의 찶들), 어려운 단위 테스트, 변경의 높은 비용, 코드 가독성 저하. SRP 위반이 체계적인 프로젝트는 신규 기능 추가에 2-3배 더 많은 시간이 걸립니다.
SRP 위반은 간접적인 증상으로 식별할 수 있습니다: 클래스가 200 줄을 초과하고, 다른 애플리케션 레이어(UI + network + database)에서 모듈을 임포트하고, 다양한 주제에 관한 5개 이상의 공용 메소드가 있습니다. 응집도 메트릭은 통계적 지표입니다: 클래스 내 메소드의 응집도가 낮으면 SRP 위반을 나타냅니다.
SRP 위반을 감지하려면 정적 분석 도구를 사용하세요: Android의 경우 TooManyFunctions 규칙의 Detekt, iOS의 경우 file_length 규칙의 SwiftLint입니다. 이 도구들은 크기와 복잡성 임계값을 초과하는 클래스를 강조합니다.
SRP를 위반하는 클래스의 리팩토링은 Extract Class 또는 Extract Delegate를 통해 수행됩니다: 관련 메소드의 그룹이 별도의 클래스로 추출되고, 원본 클래스는 호출을 위임합니다. 이러한 리팩토링의 점진적 적용은 God Class를 각기 단일 책임이 있는 앸결합된 모듈들로 변환합니다. 이 접근 방식은 개발을 멈추지 않고도 아키텍처를 개선할 수 있게 합니다 — 리팩토링은 반복적으로, 한 번에 하나의 모듈씩 수행됩니다.
자주 묻는 질문
아닙니다. SRP는 메소드의 수가 아닌 변경 이유의 수에 관한 것입니다. 클래스는 모든 메소드가 하나의 액터에 대한 하나의 책임을 수행한다면 수십 개의 메소드를 가질 수 있습니다. 하나의 메소드는 반대의 극단으로, 코드를 과도하게 단조로 나누게 됩니다.
동일한 원칙입니다. Single Responsibility Principle은 단일 책임과 단일 의무 둘 다로 번역됩니다. 책임이라는 용어가 본질을 더 정확하게 반영합니다: 기술적 기능이 아닌 액터에 대한 책임에 관한 문제입니다.
Repository는 데이터 레이어에 SRP를 적용한 직접적인 결과입니다. ViewModel 또는 UseCase에 데이터 액세스 로직을 분산시키는 대신 Repository가 단일 책임을 맡습니다: 원천 추상화로 데이터를 제공하는 것입니다. 이것은 모바일 아키텍처에서 SRP의 고전적인 구현입니다.
네, SRP는 의존성을 금지하지 않습니다. 단일 책임의 클래스는 컴포지션을 통해 다른 클래스에 작업의 일부를 위임할 수 있습니다. 중요한 것은 이러한 위임된 작업이 동일한 책임의 일부이라면 독립적인 변경 이유가 아니라는 것입니다.
물어보세요: “어떤 액터들이 이 클래스의 변경을 요청할 수 있을까요?” 답변에 둘 이상의 액터가 있다면 SRP가 위반된 것입니다. 추가적으로: 클래스의 목적을 연결사 없이 한 문장으로 설명해 보세요. 할 수 없다면 클래스가 너무 많은 일을 하고 있는 것입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.