단일 책임 원칙 (SRP: Single Responsibility Principle) - 유지보수성을 높이는 설계 원칙

SRP단일 책임 원칙
avatar
2025.03.11
·
6 min read

단일 책임 원칙(SRP)이란?

클래스나 모듈은 단 하나의 변경 이유만 가져야 한다.

SRP는 하나의 클래스 또는 모듈이 어려 개의 역할을 담당하면 안 되며, 변경해야 하는 이유가 하나여야 한다는 의미를 가진다. 즉, 클래스는 하나의 명확한 목적을 가져야 하며, 하나의 책임만 수행해야 한다.

쉽게 말해, 하나의 클래스는 하나의 역할만 수행해야 한다. 여러 개의 기능을 한 클래스에서 처리하면 유지보수성이 떨어진다.

SRP가 중요한 이유

  • 코드 유지보수가 쉬워짐: 특정 기능을 변경할 때, 관련 없는 코드에 영향을 주지 않음.

  • 재사용성이 높아짐: 특정 역할을 하는 클래스는 다양한 곳에서 쉽게 재사용할 수 있음.

  • 디버깅 및 테스트가 용이해짐: 하나의 책임만 처리하므로, 단위 테스트가 간결해지고 명확해짐.

  • 협업이 쉬워짐: 여러 사람이 개발할 때, 특정 역할에 대한 코드만 수정하면 되므로 충돌이 줄어듦.

SRP 위반 사례

여러 책임을 가진 클래스

아래 UserManager 클래스는 사용자 권리와 데이터 저장 두 가지 책임을 동시에 수행하고 있다.

class UserManager {
    func createUser(name: String, email: String) {
        print("사용자 생성: \(name), 이메일: \(email)")
    }
    
    func saveUserToDatabase(user: User) {
        print("데이터베이스에 사용자 저장: \(user.name)")
    }
}

문제점:

  • UserManager는 사용자 생성과 데이터 저장이라는 두 가지 책임을 가지고 있음.

  • 사용자를 생성하는 로직이 변경되면, 데이터 저장 로직도 함께 영향을 받을 가능성이 있음.

  • 데이터 저장 방식을 변경할 경우, UserManager도 함께 수정해야 함.

사용자 관리와 데이터 저장을 분리하여 해결할 수 있다.

SRP 적용 방법

역할을 분리한 클래스 구조

SRP를 적용하여 사용자 관리(UserService)와 데이터 저장(UserRepository)를 분리하면, 각각의 클래스가 하나의 역할만 담당하도록 만들 수 있다.

// 사용자 생성만 담당하는 클래스
class UserService {
    func createUser(name: String, email: String) -> User {
        print("사용자 생성: \(name), 이메일: \(email)")
        return User(name: name, email: email)
    }
}

// 데이터 저장만 담당하는 클래스
class UserRepository {
    func saveUser(user: User) {
        print("데이터베이스에 사용자 저장: \(user.name)")
    }
}
  • UserService: 사용자 생성과 관련된 로직만 관리함.

  • UserRepository: 데이터 저장과 관련된 로직만 관리함.

  • 역할이 명확해지고, 각 클래스의 변경 이유가 하나만 존재하게 됨.

SRP를 적용하는 일반적인 패턴

SRP를 적용하는 몇 가지 일반적인 패턴을 살펴보자.

MVC 패턴에서 SRP 적용

  • Model: 데이터 및 비즈니스 로직 처리

  • View: UI 관련 로직 처리

  • Controller: 사용자 입력을 받아 Model과 View를 연결

각 요소가 하나의 역할만 담당하므로 SRP를 자연스럽게 적용할 수 있음.

서비스(Service)와 저장소(Repository) 분리

  • Service: 비즈니스 로직을 수행하는 역할

  • Repository: 데이터를 저장/조회하는 역할

비즈니스 로직과 데이터 저장을 분리하여 유지보수성을 높일 수 있음.

Delegate 패턴 활용

하나의 클래스가 여러 기능을 담당하는 경우, Delegate를 사용하여 역할을 분리할 수 있음.

protocol AuthenticationDelegate: AnyObject {
    func didAuthenticate(user: User)
}

class Authenticator {
    weak var delegate: AuthenticationDelegate?
    
    func authenticateUser(email: String, password: String) {
        // 로그인 로직 수행
        let user = User(name: "John Doe", email: email)
        delegate?.didAuthenticate(user: user)
    }
}

SRP를 유지하면서도 클래스 간의 결합도를 낮출 수 있음.

SRP를 언제 적용해야 할까?

  • 클래스가 여러 개의 기능을 담당하고 있을 때

  • 한 클래스의 변경이 여러 이유에서 발생할 때

  • 테스트 코드 작성이 어려워질 때

  • 코드가 길어지고 가독성이 떨어질 때







- 컬렉션 아티클