• Feed
  • Explore
  • Ranking
/
/
    💻 Dev

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

    SRP단일 책임 원칙
    지
    지성
    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를 언제 적용해야 할까?

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

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

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

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







    - 컬렉션 아티클