2025-08-08 00:26
Tags:
단일 책임 원칙 (SRP) 핸드북
1. 만들어진 이유: 소프트웨어의 썩어가는 냄새를 막기 위해
소프트웨어는 살아있는 유기체와 같습니다. 처음에는 깨끗하고 잘 정돈되어 있지만, 시간이 지나고 기능이 추가되면서 점점 복잡해지고 얽히게 됩니다. 코드를 조금만 수정해도 예상치 못한 곳에서 오류가 터져 나오는 현상, 바로 **‘소프트웨어의 부패’**가 시작되는 것입니다.
2000년대 초, 로버트 C. 마틴(Robert C. Martin, Uncle Bob)은 이러한 문제에 깊이 고민했습니다. 그는 수많은 프로젝트가 실패하는 근본적인 원인이 잘못된 ‘설계’에 있다고 보았고, 좋은 객체 지향 설계를 위한 5가지 원칙, SOLID를 제시했습니다.
**단일 책임 원칙(Single Responsibility Principle, SRP)**은 바로 이 SOLID의 첫 번째 글자 ‘S’를 담당하는, 가장 근본적이면서도 강력한 원칙입니다. SRP의 탄생 목표는 명확했습니다. “소프트웨어 구성 요소(클래스, 모듈, 함수 등)의 변경 이유를 단 하나로 제한하여, 시스템의 복잡성을 낮추고 유지보수성을 극대화하자.”
이는 마치 잘 정리된 공구함과 같습니다. 망치를 찾고 싶을 때, ‘타격 도구’ 서랍만 열어보면 되는 것처럼, 특정 기능의 변경이 필요할 때 코드의 단 한 부분만 살펴보면 되도록 만드는 것이 SRP의 핵심 철학입니다.
2. 그래서, 단일 책임 원칙이 정확히 무엇인가?
“하나의 클래스는 단 하나의 변경 이유만을 가져야 한다.”
(A class should have only one reason to change.)
이것이 로버트 C. 마틴이 SRP를 정의한 문장입니다. 하지만 ‘이유’라는 단어는 다소 추상적으로 들릴 수 있습니다. 그래서 그는 이 정의를 더욱 구체화했습니다.
“하나의 모듈은 오직 하나의 액터(Actor)에 대해서만 책임져야 한다.”
(A module should be responsible to one, and only one, actor.)
여기서 **액터(Actor)**란 ‘시스템의 변경을 요청하는 사람이나 집단’을 의미합니다. 예를 들어, ‘인사팀’, ‘재무팀’, ‘개발팀’ 등이 각각의 액터가 될 수 있습니다.
예를 들어, Employee
라는 클래스가 있다고 가정해 보겠습니다.
-
재무팀은 직원의 급여를 계산하는
calculatePay()
메서드의 변경을 요청할 수 있습니다. -
인사팀은 직원의 근무 시간을 기록하는
reportHours()
메서드의 변경을 요청할 수 있습니다. -
**개발팀(DBA)**은 직원의 정보를 데이터베이스에 저장하는
save()
메서드의 변경을 요청할 수 있습니다.
만약 이 세 가지 메서드가 모두 Employee
클래스 안에 있다면, 이 클래스는 재무팀, 인사팀, 개발팀이라는 세 명의 다른 액터에 대한 책임을 지게 됩니다. 재무팀의 요청으로 calculatePay()
를 수정했는데, 이 수정이 reportHours()
에 영향을 주어 인사팀의 업무에 오류를 발생시킬 수 있습니다. 이것이 바로 SRP를 위반한 전형적인 사례입니다.
따라서 SRP는 단순히 ‘한 클래스는 한 가지 일만 해야 한다’는 막연한 개념이 아니라, **‘클래스의 변경을 유발하는 액터(혹은 비즈니스 관심사)가 오직 하나여야 한다’**는 명확한 지침입니다.
3. 구조와 사용법: 어떻게 적용하는가?
SRP를 적용하는 것은 **‘책임을 분리하는 과정’**입니다. 여러 책임을 가진 거대한 클래스를 발견했다면, 각 책임에 맞는 작은 클래스들로 나누어야 합니다.
SRP 위반 사례 (Bad Case)
Java
// SRP 위반: 재무, 인사, 데이터베이스 관련 책임이 모두 섞여 있음
public class Employee {
public Money calculatePay() {
// 급여 계산 로직 (재무팀 관심사)
// ...
}
public String reportHours() {
// 근무 시간 보고서 생성 로직 (인사팀 관심사)
// ...
}
public void save() {
// 데이터베이스 저장 로직 (DBA 관심사)
// ...
}
}
위 Employee
클래스는 세 명의 다른 액터(재무팀, 인사팀, DBA)를 만족시켜야 하므로, 세 가지 변경 이유를 가집니다.
SRP 적용 사례 (Good Case)
이 문제를 해결하기 위해 각 액터의 책임에 따라 클래스를 분리합니다.
-
PayCalculator (재무팀 책임): 급여 계산만을 담당합니다.
Java
public class PayCalculator { public Money calculatePay(EmployeeData employeeData) { // 급여 계산 로직 // ... } }
-
HourReporter (인사팀 책임): 근무 시간 보고만을 담당합니다.
Java
public class HourReporter { public String reportHours(EmployeeData employeeData) { // 근무 시간 보고서 생성 로직 // ... } }
-
EmployeeRepository (DBA 책임): 데이터 영속성(저장, 조회 등)만을 담당합니다.
Java
public class EmployeeRepository { public void save(EmployeeData employeeData) { // 데이터베이스 저장 로직 // ... } }
-
EmployeeData (데이터 구조): 이제
Employee
클래스는 순수하게 직원의 데이터만을 담는 역할(DTO 또는 POJO)을 하거나, 각 책임 클래스들을 조합하여 사용하는 퍼사드(Facade) 역할을 수행할 수 있습니다.Java
public class EmployeeData { // 직원의 데이터 (이름, 시급 등) private String name; private double hoursWorked; // ... getters and setters }
이렇게 책임을 분리하면, 재무팀의 급여 정책이 변경되어도 PayCalculator
클래스만 수정하면 됩니다. HourReporter
나 EmployeeRepository
코드는 전혀 영향을 받지 않아 시스템의 안정성이 크게 향상됩니다.
4. 심화: 언제나 분리하는 것이 정답일까?
SRP는 강력하지만, 맹목적으로 적용해서는 안 됩니다. 모든 것을 아주 작은 단위로 분리하는 것이 항상 최선은 아닙니다.
-
조기 최적화의 함정: 아직 변경의 이유가 명확하지 않은 초기 단계부터 모든 것을 분리하면, 오히려 클래스 수가 불필요하게 늘어나고 구조가 복잡해져 개발 속도를 저해할 수 있습니다. 처음에는 하나의 클래스로 시작하더라도, 두 번째 책임이 생기는 시점에 분리하는 것(Rule of Two)이 더 실용적일 수 있습니다.
-
퍼사드 패턴(Facade Pattern)의 활용: 책임이 분리된 여러 클래스를 사용하는 것이 번거로울 때, 이들을 감싸는 간단한 인터페이스를 제공하는 ‘퍼사드’ 클래스를 만들 수 있습니다. 이 퍼사드 클래스는 직접적인 비즈니스 로직을 가지지 않고, 각 책임 클래스에 작업을 위임하는 역할만 수행합니다.
-
응집도(Cohesion)와의 관계: SRP는 **높은 응집도(High Cohesion)**라는 개념과 깊은 관련이 있습니다. 응집도는 ‘모듈의 구성 요소들이 얼마나 밀접하게 관련되어 있는가’를 나타내는 척도입니다. SRP를 잘 지킨 클래스는 단일 목적을 위해 밀접하게 연관된 데이터와 메서드만 가지므로, 자연스럽게 응집도가 높아집니다.
결론적으로, 단일 책임 원칙은 코드의 특정 부분에 변경이 필요할 때, 우리가 수정해야 할 코드가 단 한 곳에만 존재하도록 이끄는 나침반입니다. 이 원칙을 통해 우리는 변화에 유연하고, 이해하기 쉬우며, 테스트하기 용이한 견고한 소프트웨어를 만들어나갈 수 있습니다.
이 핸드북이 단일 책임 원칙을 이해하는 데 도움이 되었기를 바랍니다. 혹시 SRP를 실제 프로젝트에 적용하면서 겪었던 어려움이나 궁금한 점이 있으신가요?