이 문서의 과거 버전 (r1)을 보고 있습니다. 수정일: 2026.02.14 23:09
SRP(Single Responsibility Principle)는 객체 지향 프로그래밍과 소프트웨어 설계에서 중요한 5가지 원칙인 SOLID 원칙 중 첫 번째 원칙이다. 이 원칙은 "하나의 클래스는 단 하나의 변경 이유만을 가져야 한다"는 핵심 명제로 요약된다.
SRP는 로버트 C. 마틴에 의해 명확히 정의되고 널리 알려졌다. 이 원칙의 근본 목적은 소프트웨어 구성 요소의 응집도를 높이고 결합도를 낮추어, 시스템의 변경과 유지보수를 용이하게 하는 것이다. 하나의 모듈이 하나의 명확한 책임만을 진다면, 그 모듈에 대한 변경은 해당 책임과 관련된 요구사항의 변화에서만 비롯된다.
개발 실무에서 SRP는 클래스뿐만 아니라 메서드, 모듈, 마이크로서비스와 같은 다양한 수준의 소프트웨어 구성 요소에 적용될 수 있는 기본 철학이다. 이 원칙을 준수함으로써 코드는 더욱 이해하기 쉬워지고, 수정과 테스트가 간편해지며, 다른 시스템과의 재사용성이 향상된다. 따라서 SRP는 견고하고 유연한 소프트웨어 아키텍처를 구축하기 위한 초석이 된다.
단일 책임 원칙은 객체 지향 프로그래밍과 소프트웨어 설계에서 사용되는 핵심 원칙 중 하나이다. 이 원칙은 로버트 C. 마틴에 의해 명확히 정의되었으며, SOLID 원칙의 첫 글자를 차지한다.
이 원칙의 핵심은 "하나의 클래스는 단 한 가지 변경 이유만을 가져야 한다"는 것이다. 여기서 '변경의 이유'는 클래스가 담당하는 하나의 명확한 책임을 의미한다. 즉, 하나의 모듈, 클래스 또는 함수는 하나의 액터(사용자, 이해관계자, 시스템)에 대해서만 책임져야 한다는 개념이다. 이는 기능적 관점이 아닌, 변경이 발생하는 주체(액터)의 관점에서 책임을 분리하는 것을 강조한다.
책임의 분리는 변경으로 인한 영향을 최소화하는 데 목적이 있다. 서로 다른 이유로, 서로 다른 시점에 변경될 수 있는 두 가지 책임이 하나의 클래스에 결합되어 있다면, 한 책임의 변경이 다른 책임과 무관한 코드에 영향을 미쳐 부작용을 초래할 수 있다. 따라서 SRP는 이러한 변경의 축을 분리함으로써 시스템의 응집도는 높이고 결합도는 낮추는 설계를 지향한다.
단일 책임 원칙은 객체 지향 프로그래밍과 소프트웨어 설계에서 사용되는 핵심 원칙 중 하나이다. 이 원칙은 로버트 C. 마틴에 의해 명확히 정립되었으며, SOLID 원칙의 첫 글자를 차지한다.
원칙의 핵심 명제는 "하나의 클래스는 단 하나의 변경 이유만을 가져야 한다"는 것이다. 여기서 '책임'은 '변경의 이유'로 해석된다. 즉, 하나의 클래스가 담당하는 기능 영역이 변경을 유발하는 하나의 액터(사용자, 시스템, 정책 등)에 대해서만 응집되어야 함을 의미한다. 이는 클래스가 오직 하나의 일만을 수행해야 한다는 단순한 의미보다 더 깊은 설계 철학을 담고 있다.
실제 적용에서 이 원칙은 클래스의 응집도를 높이고 결합도를 낮추는 방향으로 이끈다. 만약 하나의 클래스가 서로 다른 이유로 변경된다면, 그 클래스는 여러 책임을 지니고 있을 가능성이 크다. 이러한 클래스는 수정 시 예상치 못한 부작용을 발생시키기 쉽고, 유지보수가 어려워진다. 따라서 SRP는 변경으로 인한 파급 효과를 최소화하기 위해 책임의 경계를 명확히 구분하도록 요구한다.
변경의 이유는 단일 책임 원칙을 적용하는 핵심적인 기준이 된다. 이 원칙에 따르면, 하나의 클래스는 변경되어야 하는 단 하나의 이유만을 가져야 한다. 여기서 '변경의 이유'란 해당 클래스가 담당하는 하나의 명확한 책임이나 역할을 의미한다. 예를 들어, 사용자 정보를 관리하는 클래스가 사용자 데이터의 유효성 검사와 데이터베이스 저장 로직을 모두 처리한다면, 이 클래스는 검증 규칙 변경과 저장소 기술 변경이라는 두 가지 서로 다른 이유로 변경될 수 있다. 이는 원칙 위반에 해당한다.
책임의 분리는 이러한 변경의 이유를 기준으로 수행된다. 하나의 클래스가 여러 변경 이유를 가진다는 것은 여러 책임이 뒤섞여 있음을 나타낸다. 이러한 클래스는 특정 책임과 관련된 요구사항이 변경될 때, 다른 책임과 무관한 코드까지 영향을 받고 수정되어야 할 위험이 있다. 따라서 각 책임은 변경의 이유가 하나인 별도의 클래스나 모듈로 분리되어야 한다. 이는 시스템의 한 부분을 수정할 때 다른 부분에 미치는 예기치 않은 영향을 최소화하는 데 목적이 있다.
변경의 이유에 따른 책임 분리는 다음과 같은 이점을 제공한다.
분리 기준 | 분리 전 문제점 | 분리 후 기대 효과 |
|---|---|---|
데이터 관리 vs. 비즈니스 로직 | 저장소 변경 시 로직 코드가 영향을 받음 | 저장소 구현체만 교체 가능 |
표현 계층 vs. 도메인 계층 | UI 변경이 핵심 비즈니스 규칙을 훼손할 위험 | UI 변경이 도메인 모델에 영향을 주지 않음 |
외부 서비스 통신 vs. 내부 처리 | API 형식 변경 시 전체 처리 흐름을 수정해야 함 | 통신 어댑터만 수정하면 됨 |
결론적으로, '변경의 이유'는 책임을 식별하고 분리하는 구체적인 척도 역할을 한다. 이 접근법은 단순히 기능을 작은 단위로 나누는 것이 아니라, 변화에 대한 응집력 있는 모듈을 설계하도록 이끈다. 이를 통해 각 구성 요소는 자신의 주요 책임에만 집중하고, 다른 이유로 인한 변경으로부터 보호받을 수 있다.
SRP를 준수하면 코드의 유지보수성이 크게 향상된다. 각 클래스가 하나의 변경 이유만을 가지므로, 특정 기능 수정이나 요구사항 변경 시 해당 책임을 가진 단 하나의 클래스만 수정하면 된다. 이는 변경의 영향을 국소화하여 시스템 전체의 안정성을 높이고, 수정 과정에서 의도치 않은 부작용이 발생할 위험을 줄인다.
또한, 명확하게 정의된 단일 책임을 가진 클래스는 그 자체로 응집력이 높은 모듈이 되어 재사용 가능성이 높아진다. 다른 맥락에서 동일한 책임이 필요할 때, 해당 클래스를 쉽게 가져와 사용하거나 조합할 수 있다. 이는 코드 중복을 방지하고 개발 생산성을 높이는 데 기여한다.
테스트 용이성도 중요한 장점이다. 책임이 분리된 작은 클래스는 테스트 대상이 명확하고 의존성이 단순해 단위 테스트를 작성하기 훨씬 수월하다. 복잡한 설정이나 많은 모의 객체 없이 핵심 로직에 집중한 테스트 케이스를 구성할 수 있으며, 이는 테스트 커버리지와 소프트웨어 신뢰도를 향상시킨다.
요약하면, SRP는 시스템을 이해하고 예측 가능하게 만드는 기초를 제공한다. 변경이 쉬우며, 재사용성이 높고, 테스트하기 좋은 구조는 장기적으로 소프트웨어의 생명주기 비용을 절감하고 품질을 유지하는 데 결정적인 역할을 한다.
단일 책임 원칙(SRP)을 준수하면 코드의 유지보수성이 크게 향상된다. 이는 소프트웨어의 가장 중요한 품질 속성 중 하나로, 시스템을 쉽게 이해하고 변경하며 확장할 수 있는 능력을 의미한다. SRP는 각 클래스가 하나의 변경 이유만을 가지도록 강제함으로써, 특정 변경 요구사항이 발생했을 때 수정해야 할 코드의 범위를 최소화한다. 결과적으로 한 부분을 변경할 때 다른 독립적인 기능에 부작용을 끼칠 가능성이 줄어든다.
구체적으로, 유지보수성 향상은 다음과 같은 측면에서 나타난다. 첫째, 코드의 가독성이 높아진다. 각 클래스가 명확하고 단일한 책임을 가지므로, 클래스의 이름과 목적을 쉽게 이해할 수 있다. 둘째, 변경의 영향 범위가 국소화된다. 예를 들어, 보고서 생성 로직의 형식만 변경되어야 할 경우, 해당 책임만을 가진 클래스만 수정하면 된다. 데이터베이스 연결 방식이나 계산 로직을 담당하는 다른 클래스는 전혀 건드릴 필요가 없다.
이러한 국소화는 디버깅과 버그 수정 과정에도 긍정적인 영향을 미친다. 문제가 발생했을 때, 해당 기능을 담당하는 특정 클래스에 집중하여 원인을 파악할 수 있다. 여러 책임이 뒤섞인 큰 클래스에서는 버그의 원인이 서로 다른 책임 영역에 걸쳐 흩어져 있어 추적이 어려울 수 있다. SRP를 적용하면 각 모듈의 경계가 명확해지므로, 시스템의 특정 부분에 대한 이해와 수정이 훨씬 수월해진다.
결과적으로, 장기적인 프로젝트에서 요구사항 변경이 빈번하게 발생하거나 여러 개발자가 협업할 때 SRP의 가치는 더욱 두드러진다. 변경이 예측 가능하고 안정적으로 이루어질 수 있어, 소프트웨어의 수명 주기 전반에 걸쳐 개발 및 유지보수 비용을 절감하는 효과를 가져온다.
SRP를 준수하면 각 클래스나 모듈이 하나의 명확한 책임만을 가지게 됩니다. 이는 해당 기능을 필요로 하는 다른 부분에서 해당 클래스를 쉽게 가져다 사용할 수 있게 만듭니다. 하나의 거대하고 다양한 책임을 가진 클래스는 특정 맥락에 너무 강하게 결합되어 있어, 다른 곳에서 재사용하기 어렵습니다. 반면, 단일 책임을 가진 작은 클래스는 특정 기능을 독립적으로 제공하므로, 새로운 컨텍스트에 통합하기가 훨씬 용이해집니다.
예를 들어, 사용자 데이터를 처리하는 시스템에서 User 클래스가 사용자 정보를 저장하는 책임과 사용자 정보를 데이터베이스에 저장하는 책임을 동시에 가진다면, 단순히 사용자 정보 객체가 필요한 다른 모듈(예: 리포트 생성 모듈)에서도 불필요한 데이터베이스 연결 로직을 함께 가져와야 합니다. SRP에 따라 User 클래스는 데이터를 표현하는 책임만을, UserRepository 클래스는 영속성 처리를 담당하게 분리하면, User 객체는 데이터베이스와 무관한 어떤 곳에서도 안전하게 재사용될 수 있습니다.
또한, 잘 정의된 단일 책임은 인터페이스를 명확하게 만들어, 의존성 주입과 같은 기법을 통해 구현체를 유연하게 교체할 수 있게 합니다. 이는 특정 기능을 다른 구현으로 대체해야 할 때, 해당 책임만을 담당하는 클래스만 새로 작성하거나 교체하면 되므로, 시스템 전반의 재사용성과 적응성을 높입니다.
재사용성 측면 | SRP 미적용 시 | SRP 적용 시 |
|---|---|---|
클래스의 독립성 | 낮음. 다른 책임과 강하게 결합됨. | 높음. 단일 기능으로 독립적임. |
다른 컨텍스트에서의 사용 난이도 | 높음. 불필요한 의존성을 제거해야 함. | 낮음. 필요한 기능만을 가져와 사용 가능. |
기능 교체/확장 | 어려움. 클래스 전체를 수정해야 할 수 있음. | 용이함. 특정 책임의 클래스만 교체하면 됨. |
따라서 SRP는 코드를 구성하는 단위의 응집도를 높여, 이를 소프트웨어 컴포넌트로서 더 가치 있게 만듭니다. 이는 궁극적으로 시스템의 아키텍처를 보다 모듈화되고 유연한 구조로 이끌어, 새로운 기능 개발 시 기존 코드를 재사용할 기회를 크게 늘립니다.
단일 책임 원칙을 준수하면 각 클래스나 모듈이 하나의 명확한 책임만을 가지게 된다. 이는 자연스럽게 해당 모듈의 입력과 출력, 그리고 내부 동작을 명확하게 정의하게 만든다. 결과적으로, 테스트하려는 대상의 범위가 좁고 구체적이기 때문에 단위 테스트를 작성하기가 훨씬 용이해진다.
하나의 클래스가 여러 책임을 가지면, 해당 클래스를 테스트하기 위해 다양한 시나리오와 복잡한 테스트 픽스처를 준비해야 한다. 예를 들어, 데이터를 처리하고 동시에 파일 입출력을 수행하며, 형식 변환까지 하는 클래스는 각 기능을 독립적으로 검증하기 어렵다. 반면, SRP에 따라 책임이 분리되면 각각의 작은 클래스는 상대적으로 간단한 의존성만을 가지게 되어, 모의 객체나 스텁을 이용한 격리 테스트가 쉬워진다.
또한, 책임이 분리되면 테스트 케이스의 수도 관리하기 쉬워진다. 하나의 책임 변경으로 인해 발생하는 테스트 실패는 해당 책임을 담당하는 모듈의 테스트에만 국한된다. 이는 회귀 테스트의 범위를 최소화하고, 테스트 스위트의 안정성을 높이는 데 기여한다. 결국, SRP는 테스트 주도 개발이나 자동화된 테스트를 효과적으로 지원하는 기반이 된다.
책임 과다 클래스(God Class)는 SRP를 위반하는 대표적인 사례이다. 이는 하나의 클래스가 여러 가지 이유로 변경될 수 있는, 즉 여러 책임을 지니고 있는 상태를 의미한다. 예를 들어, 사용자 정보를 관리하는 User 클래스가 데이터베이스 연결, 사용자 데이터 검증, 이메일 발송, 보고서 생성 등 서로 다른 변경 동기를 가진 기능을 모두 포함하는 경우가 해당한다. 이러한 클래스는 그 크기와 복잡성이 비대해지기 쉽다.
SRP 위반은 결합도를 증가시키고 여러 부작용을 초래한다. 하나의 책임 변경이 다른 책임과 무관한 코드 부분에까지 영향을 미칠 수 있어, 변경이 예측하기 어렵고 위험해진다. 또한, 특정 책임만 재사용하려 해도 불필요한 다른 코드까지 포함해야 하므로 재사용성이 떨어진다. 테스트 관점에서도 모든 책임이 얽혀 있으면 단위 테스트 작성이 복잡해지고, 한 기능을 테스트할 때 다른 기능의 부수 효과를 고려해야 하는 문제가 발생한다.
구체적인 문제점은 다음과 같은 표로 정리할 수 있다.
문제점 | 설명 |
|---|---|
취약한 설계 | 한 책임의 변경이 다른 책임에 의존하지 않는 코드를 망가뜨릴 수 있다. |
재사용 어려움 | 클래스 전체를 가져와야 하므로 원하는 기능만 분리하여 사용하기 힘들다. |
테스트 복잡성 | 단일 책임을 격리하여 테스트하기 어렵고, 테스트 설정이 과도하게 복잡해진다. |
이해도 저하 | 클래스의 주요 목적이 모호해지고, 코드를 읽고 이해하는 데 더 많은 시간이 소요된다. |
결국, SRP를 위반하는 설계는 소프트웨어의 유지보수성을 현저히 저하시키는 주요 원인이 된다. 변경이 두려운 코드베이스로 진화하며, 작은 수정조차 시스템 전반에 걸친 예기치 않은 오류를 발생시킬 위험이 커진다.
책임 과다 클래스는 단일 책임 원칙을 위반하는 대표적인 안티패턴이다. 이는 하나의 클래스가 너무 많은 책임을 지니고 있어, 서로 관련이 없거나 약하게 연관된 여러 기능을 포함하는 경우를 가리킨다. 이러한 클래스는 종종 '만능 클래스' 또는 '신 클래스'라고 불리며, 시스템 내에서 과도하게 많은 역할을 수행한다. 일반적으로 수천 줄에 달하는 방대한 코드를 포함하고, 다양한 이유로 변경이 발생할 수 있다.
책임 과다 클래스는 여러 심각한 문제를 초래한다. 첫째, 클래스가 변경되어야 할 이유가 여러 가지이기 때문에, 한 부분을 수정하면 의도하지 않은 다른 부분에 영향을 미칠 위험이 크다. 이는 결합도가 높아진 결과이다. 둘째, 클래스의 크기가 비대해지기 때문에 이해하기 어렵고, 다른 개발자가 코드를 파악하거나 수정하는 데 많은 시간이 소요된다. 셋째, 특정 책임만 재사용하고 싶어도 거대한 클래스 전체에 의존해야 하므로 재사용성이 현저히 떨어진다.
다음은 간단한 책임 과다 클래스의 예시이다.
책임 | 설명 | 변경 이유 |
|---|---|---|
데이터 관리 | 사용자 정보를 데이터베이스에 저장하고 조회한다. | |
비즈니스 로직 | 사용자 정보의 유효성을 검사하고 특정 규칙을 적용한다. | 비즈니스 규칙 변경 |
보고서 생성 | 사용자 데이터를 기반으로 PDF나 HTML 형식의 보고서를 생성한다. | 출력 형식 또는 레이아웃 변경 |
이메일 발송 | 특정 이벤트 발생 시 사용자에게 이메일을 발송한다. | 이메일 서비스 공급자 또는 템플릿 변경 |
위 표와 같이 하나의 클래스가 네 가지의 명확히 구분되는 책임을 지니고 있다. 이 중 하나의 책임(예: 보고서 생성 방식)을 변경하기 위해 클래스를 수정하면, 다른 책임(예: 이메일 발송)과 전혀 무관한 코드를 건드리게 되어 버그가 발생할 가능성이 높아진다. 이러한 클래스는 단위 테스트 작성도 매우 어렵게 만든다.
SRP 위반으로 인해 발생하는 주요 문제 중 하나는 결합도가 증가한다는 점이다. 하나의 클래스가 여러 책임을 지니게 되면, 서로 다른 이유로 변경되는 코드들이 강하게 엮이게 된다. 이는 한 책임 영역의 수정이 다른, 전혀 무관한 책임 영역에까지 영향을 미치는 부작용을 초래한다.
예를 들어, 사용자 정보를 관리하고 동시에 이메일 발송 로직을 처리하는 클래스가 있다고 가정하자. 이메일 서비스 제공업체의 API가 변경되어 발송 로직을 수정해야 할 경우, 이 변경은 사용자 정보 관리와는 전혀 관계없음에도 불구하고 해당 클래스를 수정하게 만든다. 반대로, 사용자 정보의 저장 방식을 변경할 때도 이메일 발송 로직을 포함한 전체 클래스를 다시 테스트하고 배포해야 할 위험이 생긴다. 이렇게 변경의 영향이 비대해지는 현상을 파급 효과라고 한다.
높은 결합도는 시스템의 유연성을 크게 저해한다. 특정 기능을 재사용하거나 다른 모듈로 교체하려 해도, 불필요하게 엮인 다른 책임 때문에 분리가 어려워진다. 또한 단위 테스트 작성도 복잡해지는데, 하나의 책임만을 고립시켜 테스트하기가 매우 어렵기 때문이다. 결국 시스템은 작은 변경에도 취약해지고, 유지보수 비용은 기하급수적으로 증가하게 된다.
책임을 식별하는 가장 효과적인 방법 중 하나는 클래스나 모듈이 변경되는 이유를 분석하는 것이다. 하나의 변경 이유는 일반적으로 하나의 책임에 해당한다. 예를 들어, 보고서를 생성하는 클래스가 데이터를 가져오는 로직과 PDF 형식으로 변환하는 로직을 모두 포함한다면, 이 클래스는 데이터 접근과 포맷 변환이라는 두 가지 이유로 변경될 수 있다. 따라서 이 두 책임을 분리해야 한다.
책임 분리를 위한 구체적인 기법으로는 인터페이스 분리 원칙(ISP)을 활용한 인터페이스 설계가 있다. 클라이언트가 필요로 하는 메서드들만으로 구성된 세분화된 인터페이스를 정의하면, 자연스럽게 책임이 분리된 구현체를 만들 수 있다. 또한, 컴포지션을 통한 역할 위임은 상속보다 유연한 책임 분리를 가능하게 한다. 하나의 클래스가 여러 책임을 수행해야 할 경우, 각 책임을 별도의 클래스로 추출하고 원본 클래스가 이들을 조합하여 사용하도록 설계한다.
적용 과정에서는 명확한 역할 분담이 중요하다. 예를 들어, Order 클래스는 주문 정보를 관리하는 책임만을 지고, 주문 계산 로직은 OrderCalculator 클래스로, 주문 영수증 출력 로직은 OrderPrinter 클래스로 분리할 수 있다. 이때 각 클래스의 이름은 그 단일 책임을 명확히 반영해야 한다. 너무 포괄적인 이름(예: OrderManager)은 여러 책임을 암시할 수 있다.
분리 기법 | 설명 | 예시 |
|---|---|---|
클래스 추출 | 하나의 클래스에서 특정 책임을 담당하는 부분을 별도의 클래스로 분리한다. |
|
인터페이스 분리 | 클라이언트별로 필요한 메서드만을 정의한 인터페이스를 생성하여 구현체의 책임을 제한한다. |
|
컴포지션 | 책임을 가진 독립적인 객체를 포함(has-a)하여 기능을 위임한다. |
|
마지막으로, 응집도를 높이고 결합도를 낮추는 방향으로 설계를 검토해야 한다. 하나의 클래스 내 메서드들이 모두 단일한 목표를 위해 협력한다면 응집도가 높은 것이며, 이는 SRP가 잘 적용되었음을 나타내는 지표가 된다.
책임 식별은 SRP 적용의 첫 단계이며, 가장 어려운 부분 중 하나이다. 일반적으로 클래스나 모듈의 이름을 분석하는 것으로 시작한다. 만약 클래스 이름에 "그리고(And)"나 "또는(Or)"과 같은 접속사가 포함되어 있다면, 이는 여러 책임을 암시하는 징후이다. 예를 들어, UserAuthenticationAndEmailService라는 클래스는 인증과 이메일 발송이라는 두 가지 명확한 책임을 내포하고 있다.
책임을 분리하는 구체적인 기법으로는 인터페이스 분리 원칙을 활용한 인터페이스 분할이 있다. 하나의 큰 인터페이스를 각 책임에 맞는 여러 개의 작은 인터페이스로 나누는 것이다. 또한, 의존성 주입을 통해 특정 책임을 외부 서비스나 컴포넌트에 위임할 수 있다. 데이터와 해당 데이터를 처리하는 로직이 한 클래스에 혼재되어 있다면, 정보 전문가 패턴을 적용하여 데이터와 행위를 적절한 클래스로 재배치한다.
식별 기법 | 설명 | 예시 |
|---|---|---|
명명법 분석 | 클래스/모듈 이름에 복합적 용어가 있는지 검토 |
|
변경 이유 추적 | 서로 다른 이유로 변경되는 메서드 그룹화 | 사용자 포맷 변경 vs. 리포트 저장 방식 변경 |
공통 폐쇄 원칙 | 동일한 이유로 변경되는 요소들을 같은 모듈로 묶음 | 모든 데이터 검증 로직을 하나의 |
실제 분리 작업은 리팩토링 기법을 통해 이루어진다. 가장 일반적인 방법은 추출 클래스 리팩토링이다. 관련된 필드와 메서드를 선택하여 새로운 클래스로 이동시키고, 원본 클래스는 새 클래스의 인스턴스를 사용하도록 수정한다. 만약 행위만 분리하면 될 경우, 추출 메서드를 통해 특정 기능을 독립된 메서드로 만든 후, 이를 별도의 클래스로 옮길 수 있다. 분리 후에는 클래스 간의 관계가 결합도가 낮은 방향으로 설정되었는지 확인해야 한다.
인터페이스는 SRP를 준수하는 설계를 실현하는 핵심 도구로 작동한다. 구체적인 구현체가 아닌, 클래스가 수행해야 하는 역할에 대한 계약을 정의함으로써 책임의 경계를 명확히 한다. 하나의 인터페이스는 하나의 명확한 역할 또는 책임만을 선언해야 하며, 이를 통해 클래스가 여러 책임을 암묵적으로 떠안는 것을 방지한다.
역할 분담은 인터페이스를 통해 추상화된 각 책임을 구체적인 클래스에 할당하는 과정이다. 예를 들어, ReportGenerator와 ReportSaver라는 두 개의 인터페이스를 정의하면, 보고서 생성과 저장이라는 두 가지 독립적인 변경 이유를 분리할 수 있다. 이후 PdfReportGenerator 클래스는 ReportGenerator 인터페이스를, DatabaseReportSaver 클래스는 ReportSaver 인터페이스를 구현하여 각자의 단일 책임을 수행한다.
인터페이스 역할 | 책임 | 구현 클래스 예시 |
|---|---|---|
| 데이터 조회 |
|
| 데이터 가공 |
|
| 데이터 출력 |
|
이러한 설계는 의존성 주입과 결합될 때 그 효과가 극대화된다. 클라이언트 코드는 구체적인 구현이 아닌 인터페이스에 의존하게 되어, 특정 역할을 담당하는 구현체를 필요에 따라 쉽게 교체하거나 확장할 수 있다. 결과적으로 시스템의 각 구성 요소는 명확히 정의된 하나의 역할에 집중하며, 이는 변경에 대한 격리와 모듈 간의 낮은 결합도를 보장한다.
SRP는 SOLID 원칙의 첫 번째 원칙으로, 다른 네 가지 원칙의 기초를 제공한다. SRP를 준수하는 설계는 자연스럽게 OCP, LSP, ISP, DIP를 적용하기 쉬운 구조를 만들기 때문이다.
구체적으로, 단일 책임을 가진 클래스는 변경 이유가 하나이므로, 기능 확장 시 기존 코드를 수정하지 않고도 새로운 클래스를 추가하는 방식(OCP)으로 대응하기 쉽다. 또한, 명확한 책임은 클래스의 행위를 예측 가능하게 만들어, 상위 타입을 하위 타입으로 대체해도 문제가 발생하지 않도록 보장하는 LSP를 지키는 데 도움을 준다. 책임이 많아 인터페이스가 비대해지는 것을 방지함으로써, 클라이언트가 필요하지 않은 메서드에 의존하지 않도록 하는 ISP의 실현에도 직접적으로 기여한다. 마지막으로, 잘 정의된 책임은 구체적인 구현보다는 추상화에 의존하도록 유도하여, DIP를 적용하는 데 유리한 조건을 만든다.
다음 표는 SRP가 다른 SOLID 원칙에 미치는 긍정적 영향을 요약한다.
다른 SOLID 원칙 | SRP와의 관계 |
|---|---|
OCP (개방-폐쇄 원칙) | 단일 책임으로 인해 변경이 국지화되어, 기존 코드 수정 없이 확장이 용이해진다. |
LSP (리스코프 치환 원칙) | 명확한 책임은 하위 클래스의 행위를 예측 가능하게 하여, 치환 시 문제를 줄인다. |
ISP (인터페이스 분리 원칙) | 하나의 책임은 좁고 집중된 인터페이스를 자연스럽게 만들어, 불필요한 의존성을 제거한다. |
DIP (의존관계 역전 원칙) | 구체적인 책임 구현보다는 역할(추상화)에 초점을 맞추게 되어, 고수준 모듈이 저수준 모듈에 의존하지 않도록 한다. |
결론적으로, SRP는 SOLID 원칙의 출발점이자 토대이다. SRP를 무시하고 설계하면 다른 원칙들을 적용하는 것이 구조적으로 어려워지거나 무의미해질 수 있다. 따라서 SOLID 원칙을 효과적으로 적용하려면 먼저 책임의 경계를 명확히 하는 데 집중해야 한다.
SRP는 다른 SOLID 원칙들의 기반이 되는 핵심 원칙이다. 특히 OCP와 밀접한 관계를 가진다. 단일 책임 원칙을 준수하여 각 클래스가 하나의 변경 이유만을 가질 때, 시스템의 한 부분을 수정하더라도 다른 부분에 영향을 미치지 않게 된다. 이는 시스템을 확장에는 열려 있고 수정에는 닫혀 있게 만드는 OCP를 실현하는 데 필수적인 전제 조건이다.
LSP와의 연계성은 간접적이지만 중요하다. SRP를 통해 명확하고 응집된 책임을 가진 클래스들이 구성되면, 이 클래스들의 하위 타입을 생성할 때 상위 타입의 행동을 깨뜨리지 않기 쉬워진다. 즉, 책임이 명확히 분리된 클래스 계층 구조는 LSP를 위반할 가능성을 줄여준다.
ISP는 SRP를 인터페이스 수준에서 적용한 원칙으로 볼 수 있다. SRP가 "클래스는 하나의 변경 이유만 가져야 한다"고 한다면, ISP는 "클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다"고 말한다. 하나의 거대한 인터페이스를 여러 개의 구체적인 인터페이스로 분리하는 것은, 결국 각 인터페이스가 하나의 명확한 책임을 가지도록 하는 SRP의 정신과 일치한다.
마지막으로 DIP와의 관계는 의존성의 방향과 관련이 있다. SRP에 따라 책임이 잘 분리된 고수준 모듈(정책)과 저수준 모듈(세부 사항)은 DIP를 통해 추상화에 의존하도록 구성될 때 그 진가를 발휘한다. 세부 사항이 변경되더라도 고수준의 정책은 안정적으로 유지될 수 있다. 따라서 SRP는 모듈을 적절한 크기와 책임으로 분해하고, DIP는 이렇게 분해된 모듈들을 유연하게 조립하는 역할을 한다.
원칙 | 약자 | SRP와의 주요 연계성 |
|---|---|---|
개방-폐쇄 원칙 | OCP | SRP는 변경 이유를 분리함으로써 OCP 달성을 위한 기반을 제공한다. |
리스코프 치환 원칙 | LSP | 명확한 책임을 가진 클래스는 상속 계층에서 예측 가능한 행동을 보장하기 쉬워 LSP 준수를 돕는다. |
인터페이스 분리 원칙 | ISP | SRP의 정신을 인터페이스 설계에 적용한 것으로, 클라이언트별로 세분화된 책임을 부여한다. |
의존관계 역전 원칙 | DIP | 책임이 분리된 모듈들을 추상화를 통해 결합시켜 유연한 아키텍처를 구성할 수 있게 한다. |
SRP를 실무에 적용할 때는 적절한 책임 분리의 기준을 설정하는 것이 중요하다. 일반적으로 '변경의 이유'가 하나여야 한다는 원칙을 따르지만, '변경의 이유'를 너무 세분화하면 오히려 클래스와 모듈의 수가 폭증하여 시스템의 복잡성을 증가시킬 수 있다. 실용적인 접근법은 밀접하게 연관된 책임들은 하나의 클래스에 유지하고, 변경 주기가 명확히 다른 책임들, 예를 들어 비즈니스 로직과 보고서 생성 로직, 또는 도메인 로직과 데이터베이스 접근 로직을 분리하는 것이다. 이는 응집도를 높이고 결합도를 낮추는 데 도움이 된다.
과도한 분리는 또 다른 문제를 야기한다. 책임이 지나치게 작은 단위로 쪼개지면 클래스 간의 의존 관계가 복잡해지고, 단순한 기능을 수행하기 위해 너무 많은 객체들이 협력해야 하는 상황이 발생할 수 있다. 이는 코드의 흐름을 이해하기 어렵게 만들고, 런타임 오버헤드를 증가시킬 수 있다. 따라서 설계자는 항상 '분리의 정도'에 대해 고민해야 하며, 현재의 요구사항과 예상되는 변경 사항, 팀의 규모와 경험 수준을 종합적으로 고려하여 결정해야 한다.
실무에서 흔히 접하는 사례로 사용자 관리 모듈을 들 수 있다. 초기에는 사용자 정보의 생성, 조회, 수정, 삭제(CRUD)와 인증 로직이 하나의 클래스에 몰려 있는 경우가 많다. SRP를 적용하면 User라는 도메인 모델 객체, UserRepository라는 데이터 접근 객체, AuthenticationService라는 인증 전담 서비스, 그리고 UserController나 UserPresenter 같은 표현 계층 객체로 책임을 분리할 수 있다. 이렇게 하면 데이터베이스 스키마 변경은 UserRepository만, 비밀번호 정책 변경은 AuthenticationService만 영향을 받게 되어 유지보수가 용이해진다.
적용 시 고려사항은 다음과 같이 정리할 수 있다.
고려사항 | 설명 | 주의점 |
|---|---|---|
변경 주기 | 변경 빈도와 시기가 동일한 책임은 함께 두고, 다른 것은 분리한다. | 너무 잦은 변경을 예측하여 과도하게 분리하지 않는다. |
팀 구조 | 서로 다른 팀이 담당하는 기능은 책임 분리의 좋은 경계가 된다. | 팀 간 의사소통 비용을 고려한다. |
아키텍처 계층 | 표현 계층, 비즈니스 로직 계층, 데이터 접근 계층 등 아키텍처적 경계를 활용한다. | 계층 간 명확한 인터페이스를 정의한다. |
도메인 개념 | 하나의 명확한 도메인 개념을 표현하는 책임은 응집도를 유지한다. | 도메인 전문가와의 소통을 통해 개념의 경계를 명확히 한다. |
결국 SRP는 맹목적으로 따르기보다는, 코드의 복잡성을 관리하고 변경에 유연하게 대응하기 위한 도구로 이해하고 상황에 맞게 적용해야 한다.
적절한 책임 분리의 기준은 SRP를 실무에 적용할 때 가장 어려운 부분 중 하나이다. 너무 추상적인 기준은 분리의 실효성을 떨어뜨리고, 너무 엄격한 기준은 시스템의 복잡성을 불필요하게 증가시킬 수 있다.
일반적으로 책임을 분리하는 기준은 "변경의 이유"에 초점을 맞춘다. 하나의 클래스가 변경되어야 하는 이유가 두 가지 이상이라면, 그 클래스는 둘 이상의 책임을 가지고 있을 가능성이 높다. 예를 들어, 사용자 데이터를 데이터베이스에 저장하는 클래스가 동시에 이메일 발송 로직을 포함한다면, 저장 방식이 바뀌거나 이메일 형식이 바뀔 때 모두 동일한 클래스를 수정해야 한다. 이는 명백한 책임 위반 사례로 볼 수 있다. 또 다른 실용적인 접근법은 클래스의 이름을 지을 때 "그리고(And)"라는 단어가 필요하다면, 두 개의 책임을 포함하고 있을 가능성을 점검해봐야 한다[1].
그러나 모든 기능을 원자 단위로 분리하는 것은 오히려 해가 될 수 있다. 변경의 빈도와 영향 범위를 함께 고려하는 것이 중요하다. 두 가지 책임이 항상 함께 변경되고, 서로 강하게 결합되어 있다면 하나의 클래스로 유지하는 것이 더 합리적일 수 있다. 또한, 시스템의 규모와 도메인 복잡성에 따라 적절한 추상화 수준을 선택해야 한다. 작은 규모의 애플리케이션에서는 몇 가지 관련 책임을 하나의 모듈에 묶는 것이 더 실용적일 수 있으나, 대규모 엔터프라이즈 시스템에서는 보다 세밀한 분리가 장기적인 유지보수에 유리하다.
고려 요소 | 설명 | 예시 |
|---|---|---|
변경의 이유 | 변경이 발생하는 요인(비즈니스 규칙, 프레젠테이션, 영속성 등)이 하나인가? | 보고서 생성 로직과 PDF 변환 로직은 별도로 변경될 수 있음 |
변동성 | 책임들이 함께 변경되는가, 아니면 독립적으로 변경되는가? | 인증 로직과 로깅 정책은 독립적으로 변경됨 |
도메인 개념 | 도메인 내에서 자연스럽게 분리되는 개념인가? |
|
재사용성 | 한 책임이 다른 맥락에서 독립적으로 재사용될 가능성이 있는가? | 이메일 발송 로직은 다양한 서비스에서 재사용 가능 |
결국 적절한 기준은 프로젝트의 맥락에 따라 달라진다. 이상적인 SRP 적용은 코드의 응집도를 높이고 결합도를 낮추는 방향으로, 동시에 과도한 설계로 인한 복잡성 폭발을 방지하는 균형점을 찾는 과정이다.
SRP를 적용할 때 가장 흔히 발생하는 함정은 책임을 지나치게 세분화하여 과도한 수의 작은 클래스나 모듈을 생성하는 것이다. 이는 오히려 시스템의 복잡성을 증가시키고 전체적인 이해를 어렵게 만들 수 있다.
적절한 책임 분리의 기준은 "변경의 이유"에 초점을 맞추는 것이다. 하나의 변경 요구사항이 여러 클래스에 영향을 미치거나, 하나의 클래스가 서로 다른 이유로 자주 변경된다면 그 클래스는 여러 책임을 가지고 있을 가능성이 높다. 반대로, 변경의 빈도와 이유가 명확히 구분되지 않는 세부 사항들을 무리하게 분리하면, 클래스 간의 결합도는 오히려 높아지고 순환 의존성이 발생할 수 있다. 예를 들어, 단순히 데이터를 저장하는 DTO에 비즈니스 로직을 추가하거나, 하나의 도메인 객체를 표현(UI), 영속성(DB), 비즈니스 규칙 등으로 과도하게 분해하는 것은 관리 포인트만 늘릴 뿐이다.
복잡성을 관리하기 위해서는 추상화 수준과 모듈의 응집도를 함께 고려해야 한다. 동일한 추상화 수준에 있는 연관된 책임들은 하나의 모듈에 유지하는 것이 자연스럽다. 또한, 패키지나 네임스페이스를 활용하여 논리적으로 연관된 작은 클래스들을 그룹화하면, 과도한 분리로 인한 인지적 부하를 줄일 수 있다. 궁극적인 목표는 변경에 유연하면서도 이해하기 쉬운 구조를 만드는 것이며, 원칙의 이름에 매몰되어 '단일 책임'을 '단일 함수' 수준으로 해석하는 것을 경계해야 한다.