SOLID > SRP (단일 책임 원칙)

본 글은 로버트 C.마틴 저자(송준이 엮음)의 [클린 아키텍처: 소프트웨어 구조와 설계의 원칙] 도서를 참고하였습니다.


1. 의미

  • Single responsibility principle.
  • 한 클래스는 하나의 책임만 가져야 한다.
  • 따라서 각 소프트웨어 모듈은 변경의 이유가 단 하나여야만 한다.

2. 원칙 위반 예제

image

  • 직원 정보를 담당하는 Employee 클래스에는 4가지의 메서드가 있음
    • calculatePay() : 회계팀에서 급여를 계산하는 메서드
    • reportHours() : 인사팀에서 근무시간을 계산하는 메서드
    • saveDababase() : 기술팀에서 변경된 정보를 DB에 저장하는 메서드
    • calculateExtraHour() : 초과 근무 시간을 계산하는 메서드 (회계팀과 인사팀에서 공유하여 사용)
  • 각 메서드들은 각 팀에서 필요할 때마다 호출해 사용하고 있음

    • 예시 코드에서는 calculatePay() 메서드와 reportHours() 메서드에서 초과 근무 시간을 계산하기 위해 calculateExtraHour() 메서드를 공유하여 사용하고 있음
    class Employee {
        String name;
        String positon;
    
        Employee(String name, String position) {
            this.name = name;
            this.positon = position;
        }
    
    	// * 초과 근무 시간을 계산하는 메서드 (두 팀에서 공유하여 사용)
        void calculateExtraHour() {
            // ...
        }
    
        // * 급여를 계산하는 메서드 (회계팀에서 사용)
        void calculatePay() {
            // ...
            this.calculateExtraHour();
            // ...
        }
    
        // * 근무시간을 계산하는 메서드 (인사팀에서 사용)
        void reportHours() {
            // ...
            this.calculateExtraHour();
            // ...
        }
    
        // * 변경된 정보를 DB에 저장하는 메서드 (기술팀에서 사용)
        void saveDababase() {
            // ...
        }
    }
    
  • 회계팀에서 급여를 계산하는 기존의 방식을 새로 변경하여, 코드에서 초과 근무 시간을 계산하는 메서드 calculateExtraHour()의 알고리즘 업데이트가 필요해짐
    • 개발팀에서 회계팀의 요청에 따라 calculateExtraHour() 메서드를 변경하였는데, 이에 따른 파급 효과로 인해 의도치 않게 인사팀에서 사용하는 reportHours() 메서드에도 영향을 주게 되버림
  • 이렇게 Employee 클래스에서 회계팀, 인사팀, 기술팀 이렇게 3개의 액터에 대한 책임을 한꺼번에 가지고 있기 때문에 SRP에 대해 위배되는 상황

3. 수정한 예제

image

// * 통합 사용 클래스
class EmployeeFacade {
    private String name;
    private String positon;

    EmployeeFacade(String name, String position) {
        this.name = name;
        this.positon = position;
    }

    // * 급여를 계산하는 메서드 (회계팀 클래스를 불러와 에서 사용)
    void calculatePay() {
        // ...
        new PayCalculator().calculatePay();
        // ...
    }

    // * 근무시간을 계산하는 메서드 (인사팀 클래스를 불러와 에서 사용)
    void reportHours() {
        // ...
        new HourReporter().reportHours();
        // ...
    }

    // * 변경된 정보를 DB에 저장하는 메서드 (기술팀 클래스를 불러와 에서 사용)
    void EmployeeSaver() {
        new EmployeeSaver().saveDatabase();
    }
}

// * 회계팀에서 사용되는 전용 클래스
class PayCalculator {
    // * 초과 근무 시간을 계산하는 메서드
    void calculateExtraHour() {
        // ...
    }
    void calculatePay() {
        // ...
        this.calculateExtraHour();
        // ...
    }
}

// * 인사팀에서 사용되는 전용 클래스
class HourReporter {
    // * 초과 근무 시간을 계산하는 메서드
    void calculateExtraHour() {
        // ...
    }
    void reportHours() {
        // ...
        this.calculateExtraHour();
        // ...
    }
}

// * 기술팀에서 사용되는 전용 클래스
class EmployeeSaver {
    void saveDatabase() {
        // ...
    }
}
  • 각 책임(기능 담당)에 맞게 클래스를 분리하여 구성함
  • 회계팀, 인사팀, 기술팀의 기능 담당은 PayCaculator, HourReporter, EmployeeSaver 각기 클래스로 분리, 이를 통합적으로 사용하는 클래스인 EmployeeFacade 클래스를 만듦
    • EmployeeFacade 클래스의 메서드는 사실상 아무런 로직이 없고, 생성자로 인스턴스를 생성하고 각 클래스의 메서드를 사용하는 역할만 수행
  • EmployeeFacade 클래스는 어떠한 액터도 담당하지 않게 됨
    • 변경 사항이 생겨도 각각의 분리된 클래스에서만 수정하면 되기 때문에 EmployeeFacade 클래스는 냅둬도 기능을 이용하는데 아무런 문제가 생기지 않음

4. SRP 원칙 적용 주의점

4.1. 클래스명은 책임의 소재를 알 수 있게 작명

  • 클래스가 하나의 책임을 가지고 있다는 것을 나타내기 위해, 클래스명을 어떠한 기능을 담당하였는지 알 수 있게 작명하는 것이 좋음
  • 즉, 각 클래스는 하나의 개념을 나타내게 구성하는 것

4.2. 책임을 분리할 때 항상 결합도, 응집도 따지기

  • 결합도와 응집도를 따져가며 클래스를 분리해야 함
    • 응집도 : 한 프로그램 요소가 얼마나 뭉쳐있는가를 나타내는 척도
    • 결합도 : 프로그램 구성 요소들 사이가 얼마나 의존적인지를 나타내는 척도
  • 좋은 프로그램이란 응집도는 높게, 결합도는 낮게 설계하는 것
    • 여러가지 책임으로 나눌 때는 각 책임간의 결합도를 최소로 하도록 코드를 구성해야 함

Reference