문서의 각 단락이 어느 리비전에서 마지막으로 수정되었는지 확인할 수 있습니다. 왼쪽의 정보 칩을 통해 작성자와 수정 시점을 파악하세요.

단일 책임 원칙 | |
이름 | 단일 책임 원칙 |
영문명 | Single Responsibility Principle |
약칭 | SRP |
분류 | 객체 지향 프로그래밍 원칙, SOLID 원칙 |
제안자 | |
핵심 개념 | 하나의 클래스는 하나의 책임만 가져야 한다 |
원칙 상세 | |
정의 | 하나의 모듈, 클래스, 또는 함수는 변경할 이유가 단 하나여야 하며, 오직 하나의 액터에 대해서만 책임져야 한다는 원칙 |
목적 | 코드의 응집도를 높이고, 결합도를 낮추며, 유지보수성과 재사용성을 향상시키기 위함 |
주요 특징 | 책임의 변경으로 인한 파급 효과를 최소화, 테스트 용이성 증가 |
위반 사례 | 하나의 클래스가 데이터 관리와 UI 렌더링을 동시에 담당하는 경우 |
적용 방법 | 책임을 명확히 분리하고, 관련 없는 기능은 별도의 클래스로 추출 |
관련 패턴 | |
장점 | 코드 이해도 향상, 변경에 강함, 재사용성 증가 |
단점 | 과도한 분할로 인한 클래스 수 증가, 설계 복잡도 상승 가능성 |
다른 SOLID 원칙과의 관계 | SOLID의 'S'에 해당하며, 다른 원칙(예: 개방-폐쇄 원칙)의 기초가 됨 |

단일 책임 원칙(Single Responsibility Principle, SRP)은 객체 지향 프로그래밍과 소프트웨어 설계에서 중요한 설계 원칙 중 하나이다. 이 원칙은 로버트 C. 마틴(Robert C. Martin)이 명확히 정립하여, SOLID 원칙의 첫 번째 글자를 차지하는 핵심 원리로 자리 잡았다.
원칙의 핵심은 "하나의 클래스나 모듈은 변경해야 할 이유가 단 하나만 가져야 한다"는 것이다. 즉, 하나의 구성 요소는 하나의 명확한 책임 또는 역할만을 수행해야 하며, 그 책임은 완전히 캡슐화되어야 한다. 이는 하나의 구성 요소가 여러 가지 일을 하게 되면, 그 중 하나의 기능을 수정할 때 다른 관련 없는 기능들까지 영향을 받을 수 있기 때문이다.
이 원칙을 따르면 시스템의 각 부분이 명확한 목적을 가지게 되어, 코드의 이해도와 유지보수성이 크게 향상된다. 또한, 변경이 발생했을 때 그 영향을 최소화하는 범위로 국한시키는 데 도움이 된다. 단일 책임 원칙은 단순히 클래스의 크기를 작게 만드는 것 이상으로, 변경의 축(axis of change)을 기준으로 책임을 분리하는 개념적 도구로 사용된다.

단일 책임 원칙의 핵심은 하나의 클래스나 모듈이 변경되어야 하는 이유는 단 하나뿐이어야 한다는 것이다. 이 원칙은 객체 지향 프로그래밍의 설계 원칙 중 가장 기본적이며, 다른 설계 원칙들의 기초를 형성한다.
원칙의 핵심을 이해하기 위해서는 '책임'과 '변경의 이유'라는 두 가지 개념을 명확히 정의해야 한다. 여기서 말하는 책임은 클래스가 수행하는 일련의 기능이나 역할을 의미한다. 변경의 이유는 해당 클래스를 수정하게 만드는 요인, 즉 비즈니스 규칙의 변경, 보고서 형식 변경, 데이터베이스 스키마 변경 등과 같은 외부적 요구사항을 가리킨다. 따라서 단일 책임 원칙은 하나의 클래스가 여러 개의 서로 다른 변경 이유를 가져서는 안 되며, 오직 하나의 변경 이유에 대해서만 책임져야 함을 강조한다.
이 원칙을 위반하는 대표적인 예는 갓 클래스이다. 갓 클래스는 사용자 인증, 데이터 검증, 보고서 생성, 데이터 저장 등 여러 가지 상이한 책임을 한꺼번에 떠안은 클래스를 말한다. 예를 들어, Order 클래스가 주문 정보를 관리하는 동시에 주문 데이터를 데이터베이스에 저장하고, 주문 내역을 PDF로 출력하는 기능까지 포함한다면, 이 클래스는 데이터 관리, 영속성, 보고서 생성이라는 세 가지 서로 다른 이유로 변경될 수 있다. 이는 단일 책임 원칙을 명백히 위반하는 사례이다.
단일 책임 원칙을 준수하면 각 클래스는 명확한 목적과 경계를 가지게 된다. 이는 시스템의 복잡성을 관리 가능한 수준으로 낮추고, 한 부분의 변경이 시스템의 다른 부분에 미치는 영향을 최소화하는 데 기여한다. 결과적으로 코드는 더 이해하기 쉽고, 수정하기 쉬우며, 재사용과 테스트가 용이해진다.
단일 책임 원칙에서 말하는 '책임'은 '변경의 이유'로 정의된다. 이는 로버트 C. 마틴이 제시한 개념으로, 하나의 클래스나 모듈이 변경되어야 하는 이유는 오직 하나뿐이어야 한다는 의미이다. 여기서 '변경의 이유'란 사용자 요구사항이나 비즈니스 규칙의 변화와 같이, 해당 코드를 수정하게 만드는 외부적 압력을 가리킨다.
예를 들어, 어떤 클래스가 영수증을 출력하는 로직과 데이터베이스에 저장하는 로직을 모두 포함한다면, 이 클래스는 출력 형식 변경과 저장 방식 변경이라는 두 가지 서로 다른 이유로 수정될 수 있다. 따라서 이 클래스는 두 가지 책임을 지고 있으며, 단일 책임 원칙을 위반한다고 볼 수 있다. 책임을 올바르게 분리하면, 출력 형식이 바뀔 때는 출력 담당 클래스만, 저장 방식이 바뀔 때는 저장 담당 클래스만 수정하면 된다.
이 원칙은 객체지향 설계의 응집도와 깊은 관련이 있다. 하나의 책임은 높은 응집도를 가진 기능의 집합으로 이어진다. 반면 여러 책임이 한데 모이면 결합도가 높아지고, 변경이 다른 부분에 미치는 영향이 커져 시스템이 취약해진다. 따라서 '책임의 정의'는 단순히 기능을 나누는 것을 넘어, 변경에 대한 영향을 최소화하는 설계의 단위를 규정한다.
단일 책임 원칙에서 '변경의 이유'는 '책임'을 정의하는 핵심 기준이다. 이 관점에서, 하나의 클래스나 모듈은 변경을 유발하는 단 하나의 '액터'[1]만을 위해 존재해야 한다. 여기서 '액터'는 해당 코드를 변경하도록 요구할 수 있는 사람이나 사용자 그룹을 의미한다.
이 원칙은 단순히 클래스가 한 가지 일만 해야 한다는 기술적 설명보다 더 깊은 의미를 지닌다. 서로 다른 액터가 동일한 클래스를 변경하도록 요구한다면, 그 클래스는 여러 책임을 지고 있는 것이다. 예를 들어, 하나의 클래스가 데이터베이스 연결 로직과 비즈니스 규칙 계산 로직을 모두 처리한다면, 이 클래스는 데이터베이스 관리자와 비즈니스 분석가라는 두 액터의 변경 요구에 모두 영향을 받게 된다. 한 액터의 요구로 인한 변경이 다른 액터와 무관한 코드에 영향을 미칠 수 있으며, 이는 예상치 못한 버그와 불필요한 재테스트를 초래한다.
따라서 변경의 이유를 분리하는 것은 설계의 안정성을 높인다. 아래 표는 단일 책임 원칙을 준수하는 경우와 위반하는 경우를 변경의 이유 측면에서 비교한다.
준수하는 경우 | 위반하는 경우 |
|---|---|
변경 요구가 발생하는 액터가 하나이다. | 서로 다른 액터들이 동일한 모듈을 변경하도록 요구한다. |
한 액터의 요구사항 변화가 시스템의 다른 부분에 미치는 영향이 최소화된다. | 한 액터를 위한 수정이 다른 액터와 관련된 기능에 부수 효과를 일으킬 위험이 크다. |
모듈의 수정 이유가 명확하고 단일하다. | 모듈을 수정해야 하는 이유가 복잡하고 다중적이다. |
결론적으로, 단일 책임 원칙은 '변경의 이유'를 기준으로 책임의 경계를 설정하도록 유도한다. 이는 궁극적으로 시스템의 각 구성 요소가 독립적으로 진화할 수 있는 기반을 마련해 준다.

단일 책임 원칙을 준수하면 소프트웨어의 유지보수성이 크게 향상된다. 하나의 클래스가 하나의 변경 이유만을 가질 경우, 해당 기능과 관련된 요구사항이 변하더라도 수정해야 할 코드의 범위가 명확하게 제한된다. 이는 시스템의 한 부분을 변경했을 때 예상치 못한 다른 부분에 부작용이 발생하는 위험을 줄여준다. 결과적으로 코드베이스는 변경에 더 유연해지고, 개발자는 보다 자신 있게 리팩토링을 수행할 수 있다.
코드의 가독성과 재사용성 또한 증가한다. 각 클래스가 명확하고 단일한 목적을 가지므로, 그 클래스가 무엇을 하는지 이해하기 쉬워진다. 이는 새로 합류한 개발자의 학습 곡선을 낮추는 데 도움을 준다. 또한, 잘 정의되고 응집된 책임을 가진 모듈은 다른 맥락에서도 독립적으로 사용될 가능성이 높아진다. 반면 여러 책임이 뒤섞인 클래스는 특정 컨텍스트에 과도하게 결합되어 재사용하기 어려운 경우가 많다.
마지막으로, 테스트 용이성이 증대된다는 점이 주요 이점이다. 단일 책임을 가진 클래스는 일반적으로 의존성이 적고 입력과 출력이 명확하다. 따라서 단위 테스트를 작성하기 훨씬 수월해지며, 테스트 케이스의 수도 줄어들고 집중도가 높아진다. 복잡한 설정이나 많은 목 객체가 필요 없는 간결한 테스트는 버그를 조기에 발견하고 회귀를 방지하는 데 효과적이다.
이점 | 설명 | 결과 |
|---|---|---|
유지보수성 향상 | 변경 지점이 국소화됨 | 시스템 변경이 안전하고 예측 가능해짐 |
가독성 증가 | 클래스의 목적이 명확함 | 코드 이해 및 협업 효율성 향상 |
재사용성 증가 | 응집도 높은 독립 모듈 | 새로운 컨텍스트에서 쉽게 활용 가능 |
테스트 용이성 | 의존성 감소, 인터페이스 단순화 | 견고하고 빠른 단위 테스트 작성 가능 |
단일 책임 원칙을 준수하면 클래스나 모듈의 변경 이유가 하나로 제한되므로, 해당 컴포넌트를 수정해야 할 가능성이 줄어든다. 이는 시스템의 전반적인 유지보수 비용을 낮추는 직접적인 효과를 가져온다. 변경이 필요한 경우, 수정 범위가 명확하게 한정되어 관련 코드를 빠르게 찾고 안전하게 변경할 수 있다.
또한, 단일 책임 원칙을 적용하면 각 컴포넌트의 목적과 역할이 명확해진다. 이는 코드를 이해하고 파악하는 데 걸리는 시간을 단축시킨다. 유지보수 담당자는 복잡하게 얽힌 로직을 해석하기보다는, 잘 정의된 책임을 가진 작은 단위들을 조합하여 시스템을 이해할 수 있게 된다.
아래 표는 단일 책임 원칙 적용 전후의 유지보수 특성을 비교한 것이다.
특성 | SRP 미적용 (책임 과중) | SRP 적용 (책임 분리) |
|---|---|---|
변경 영향 범위 | 넓고 예측하기 어려움 | 좁고 명확함 |
코드 이해도 | 낮음, 파악에 시간이 많이 소요됨 | 높음, 목적이 명확함 |
수정 오류 가능성 | 높음, 의도치 않은 부작용 발생 가능 | 상대적으로 낮음 |
리팩토링 용이성 | 어려움, 결합도가 높아 분리 작업이 복잡함 | 용이함, 독립적인 단위로 재구성 가능 |
결과적으로, 이 원칙은 소프트웨어의 생명주기 전반에 걸쳐 변경에 대한 두려움을 줄이고, 보다 안정적이고 예측 가능한 방식으로 시스템을 진화시키는 토대를 제공한다. 이는 장기적인 프로젝트의 성공에 중요한 기여를 한다.
단일 책임 원칙을 준수하는 클래스는 하나의 명확한 책임만을 가지므로, 그 목적과 기능이 명료해집니다. 이는 코드의 가독성을 크게 향상시킵니다. 다른 개발자가 코드를 파악할 때, 클래스가 무엇을 하는지 쉽게 이해할 수 있고, 이는 협업과 유지보수 과정을 원활하게 만듭니다. 복잡한 로직이 여러 책임으로 뒤엉키지 않고 단일 모듈에 캡슐화되면, 시스템의 전반적인 구조를 이해하기도 훨씬 수월해집니다.
이러한 명확성은 자연스럽게 재사용성 증가로 이어집니다. 하나의 잘 정의된 책임을 가진 클래스는 다른 맥락에서도 독립적으로 사용될 가능성이 높습니다. 예를 들어, 데이터를 특정 형식으로 변환하는 책임만 가진 클래스는 보고서 생성, 파일 저장, 네트워크 전송 등 다양한 시나리오에서 재사용될 수 있습니다. 반면, 여러 책임이 결합된 클래스는 특정 컨텍스트에 너무 강하게 결합되어 있어, 다른 곳에서 사용하려면 불필요한 의존성을 함께 가져오거나 수정해야 하는 경우가 많습니다.
특성 | 단일 책임 원칙 적용 시 | 다중 책임 클래스 |
|---|---|---|
이해 용이성 | 클래스의 목적이 명확하여 빠르게 이해 가능 | 여러 관심사가 혼재되어 파악이 어려움 |
의존성 | 다른 모듈에 대한 의존성이 최소화됨 | 불필요한 의존성을 많이 가질 수 있음 |
재사용 맥락 | 다양한 컨텍스트에서 독립적으로 재사용 가능 | 특정 상황에 맞춰 설계되어 재사용이 제한적 |
결과적으로, 가독성과 재사용성은 서로 시너지 효과를 냅니다. 읽기 쉬운 코드는 재사용 가능한 구성 요소를 식별하기 쉽게 만들고, 재사용 가능한 컴포넌트는 시스템 전반에 걸쳐 일관된 패턴으로 사용되어 코드베이스의 일관성과 가독성을 더욱 높입니다. 이는 소프트웨어의 응집도를 높이고 결합도를 낮추는 핵심 메커니즘으로 작동합니다.
단일 책임 원칙을 준수하는 클래스는 하나의 명확한 책임만을 가지므로, 해당 책임과 관련된 동작만을 테스트하면 된다. 이는 테스트 대상의 범위가 명확히 제한됨을 의미하며, 결과적으로 테스트 케이스를 작성하고 유지보수하는 것이 훨씬 간단해진다.
테스트의 용이성은 주로 낮은 결합도에서 비롯된다. 하나의 클래스가 여러 책임을 가질 경우, 그 클래스를 테스트하려면 다양한 의존성들을 모두 설정해야 하며, 이는 테스트를 복잡하고 불안정하게 만든다. 반면 SRP를 적용하면 각 클래스가 독립적이므로, 특정 기능을 검증하기 위해 필요한 의존성의 수가 최소화된다. 이는 단위 테스트를 더 빠르고 격리된 상태에서 실행할 수 있게 한다.
또한, 책임이 분리되면 테스트의 코드 커버리지를 측정하고 분석하기도 쉬워진다. 하나의 거대한 클래스에 대한 높은 커버리지를 달성하는 것은 어렵지만, 여러 개의 작고 집중된 클래스들에 대한 커버리지는 각각 명확한 기준으로 측정할 수 있다. 이는 테스트되지 않은 코드 경로를 발견하고 테스트 전략을 수립하는 데 도움을 준다.

단일 책임 원칙을 위반하는 가장 대표적인 사례는 갓 클래스 또는 갓 오브젝트라고 불리는 거대한 클래스이다. 이는 하나의 클래스가 너무 많은 책임을 지니고 있어, 데이터 관리, 비즈니스 로직 처리, 사용자 인터페이스 제어, 외부 시스템 통신 등 서로 연관성이 낮은 다양한 기능을 모두 수행하는 경우를 말한다. 이러한 클래스는 코드베이스에서 변경이 발생할 가능성이 높은 지점이 여러 군데 산재하게 되어, 작은 수정 사항이 예상치 못한 부작용을 초래할 위험이 크다.
위반의 주요 징후로는 클래스나 메서드의 이름이 모호하거나 포괄적인 경우가 있다. 예를 들어, UserManager, OrderProcessor, SystemController와 같은 이름은 해당 모듈이 사용자 정보 관리와 인증, 주문 생성과 결제 처리, 시스템 설정과 로깅 등 구체적이지 않은 광범위한 작업을 수행할 것임을 암시한다. 또한, 하나의 클래스가 서로 다른 이유로 자주 변경되어야 하거나, 클래스 내부 메서드들이 서로 다른 의존성을 많이 가지고 있는 경우도 위반 징후로 볼 수 있다.
과도한 결합도는 단일 책임 원칙 위반에서 비롯되는 직접적인 결과이다. 여러 책임이 한 클래스에 뭉쳐 있으면, 이 클래스는 각 책임과 관련된 다양한 외부 모듈에 강하게 의존하게 된다. 이는 시스템의 한 부분을 수정할 때 다른 많은 부분을 함께 수정해야 하는 취약한 설계를 초래한다. 예를 들어, 보고서를 생성하는 클래스가 데이터베이스에서 데이터를 가져오는 로직, 데이터를 가공하는 비즈니스 규칙, 그리고 PDF 형식으로 출력하는 로직을 모두 포함한다면, 출력 형식을 HTML로 변경하려는 요구사항 하나만으로도 데이터 접근 로직까지 영향을 받을 수 있다.
다른 징후로는 테스트 코드 작성의 어려움을 들 수 있다. 책임이 여러 개인 클래스는 테스트를 위해 많은 의존성을 설정해야 하며, 단위 테스트의 범위가 불분명해진다. 또한, 클라이언트 코드가 클래스의 모든 기능을 필요로 하지 않음에도 불구하고 불필요한 의존성을 강제로 가지게 되는 문제도 발생한다. 이는 인터페이스 분리 원칙 위반으로도 이어질 수 있다.
갓 클래스는 단일 책임 원칙을 심각하게 위반하는 대표적인 안티패턴이다. 이는 하나의 클래스가 너무 많은 책임을 지니고 있어, 시스템의 광범위한 부분을 과도하게 제어하거나 알고 있는 상태를 의미한다. 마치 모든 것을 아는 신과 같은 존재처럼 동작하기 때문에 '갓 클래스'라는 이름이 붙었다. 이러한 클래스는 보통 수백에서 수천 줄에 이르는 방대한 코드를 포함하며, 다양한 이유로 변경될 가능성이 매우 높다.
갓 클래스의 주요 징후는 다음과 같다. 클래스가 여러 개의 서로 다른 비즈니스 로직이나 도메인 개념을 처리한다. 수많은 인스턴스 변수와 메서드를 보유하고 있으며, 그 중 상당수는 서로 밀접하게 연관되어 있지 않다. 다른 많은 클래스에 대한 의존성이 높고, 반대로 시스템 내 많은 다른 클래스들이 이 갓 클래스에 의존한다. 결과적으로 결합도가 극도로 높아지고, 응집도는 매우 낮아진다.
이러한 클래스는 여러 가지 문제를 초래한다. 한 부분의 작은 변경이 예상치 못한 다른 부분에 영향을 미칠 수 있어 유지보수가 어렵다. 클래스의 규모가 크고 복잡하여 새로운 개발자가 코드를 이해하는 데 많은 시간이 소요된다. 단위 테스트 작성이 거의 불가능해지며, 테스트 케이스 자체가 복잡해진다. 코드 재사용이 사실상 불가능하고, 시스템의 다른 부분을 분리하여 개별적으로 발전시키는 것도 어렵게 만든다.
갓 클래스를 해결하기 위해서는 리팩토링을 통해 책임을 분리해야 한다. 관련된 데이터와 메서드를 식별하여 새로운 클래스로 추출하는 클래스 추출 기법이 핵심이다. 큰 메서드를 더 작고 명확한 책임을 가진 메서드들로 나누는 메서드 추출도 함께 진행된다. 이를 통해 하나의 거대한 클래스는 여러 개의 작고 응집된 클래스들로 분해되며, 각 클래스는 명확한 단일 책임을 가지게 된다.
단일 책임 원칙을 위반하는 클래스는 종종 결합도가 과도하게 높아지는 문제를 야기한다. 이는 하나의 클래스가 여러 책임을 지니게 되면서, 서로 다른 이유로 변경되어야 하는 요소들이 강하게 연결되어 있기 때문이다. 이러한 클래스는 시스템 내 다른 모듈들과 불필요하게 많은 의존 관계를 형성하게 되며, 이는 시스템의 한 부분을 수정했을 때 예상치 못한 다른 부분에 영향을 미치는 파급 효과를 초래한다.
과도한 결합도의 구체적인 징후로는 클래스가 다수의 다른 클래스에 대한 인스턴스를 직접 생성하거나 관리하는 경우, 또는 하나의 메서드 내에서 서로 관련 없는 여러 외부 시스템(예: 데이터베이스 접근, 네트워크 통신, 로깅, 비즈니스 로직 처리)을 동시에 조작하는 경우를 들 수 있다. 또한, 클래스의 인터페이스가 지나치게 복잡하고 다양한 목적의 메서드로 구성되어 있는 것도 결합도가 높아질 수 있는 신호이다.
이러한 높은 결합도는 다음과 같은 실질적인 문제를 발생시킨다.
변경의 어려움: 한 책임 영역의 요구사항 변경이 다른 책임 영역의 코드 수정을 강제한다.
재사용성 저하: 여러 책임이 뒤엉킨 클래스는 특정 기능만을 필요로 하는 다른 컨텍스트에서 사용하기 어렵다.
테스트 복잡성 증가: 단위 테스트를 작성할 때 테스트 대상 클래스와 결합된 모든 의존성을 설정해야 하므로 테스트가 번거로워진다.
따라서 단일 책임 원칙을 준수하여 각 클래스의 책임을 명확히 분리하는 것은 결합도를 낮추는 핵심적인 방법이다. 책임이 분리되면 각 클래스는 자신의 핵심 기능에만 집중하고, 다른 클래스와는 명확하게 정의된 인터페이스를 통해 최소한으로 소통하게 되어, 시스템 전체의 유연성과 견고함이 향상된다.

적용 방법은 단일 책임 원칙을 코드에 실천하기 위한 구체적인 접근법을 포함한다. 핵심은 하나의 클래스나 모듈이 담당하는 책임을 명확히 식별하고, 이를 분리하는 것이다.
책임 분리를 위한 기법으로는 크게 두 가지가 있다. 첫째는 추상화를 통해 책임의 경계를 정의하는 것이다. 인터페이스나 추상 클래스를 사용하여 특정 책임에 대한 계약을 명시하면, 구현체는 그 계약에만 집중할 수 있다. 둘째는 조합을 활용하는 것이다. 하나의 클래스가 여러 책임을 수행하는 경우, 각 책임을 별도의 클래스로 추출한 후, 원본 클래스가 이들을 조합하여 사용하도록 리팩토링한다. 이는 의존성 주입 패턴과 잘 결합된다.
리팩토링 패턴으로는 주로 추출 클래스와 이동 메서드가 빈번히 사용된다. 예를 들어, 사용자 데이터를 관리하면서 동시에 이메일 발송 로직을 처리하는 User 클래스가 있다면, 이메일 발송 책임을 EmailService라는 새로운 클래스로 추출한다. 이후 User 클래스는 EmailService의 인스턴스를 통해 이메일 발송을 위임한다. 변경의 이유가 다른 메서드들을 그룹화하여 새로운 클래스의 후보로 삼는 것이 효과적이다.
적용 과정에서 도움이 되는 실용적인 질문 목록은 다음과 같다.
질문 | 책임 분리 결정에 대한 도움 |
|---|---|
이 클래스를 한 문장으로 설명할 수 있는가? | 설명에 "그리고", "또는"이 들어간다면 여러 책임을 가질 가능성이 높다. |
클래스의 메서드들을 서로 다른 관심사로 그룹화할 수 있는가? | 그룹화가 가능하다면 각 그룹이 하나의 책임 후보이다. |
한 가지 변경 사유가 클래스 내의 여러 부분을 동시에 수정하게 만드는가? | 그렇다면 해당 변경 사유가 하나의 책임이며, 아직 분리되지 않았을 수 있다. |
책임 분리 기법은 단일 책임 원칙을 준수하기 위해 하나의 클래스나 모듈이 지닌 여러 책임을 식별하고, 각각을 독립적인 구성 요소로 분리하는 방법을 포괄한다. 가장 기본적인 접근법은 변경의 이유를 분석하는 것이다. 클래스를 수정해야 하는 이유가 둘 이상이라면, 이는 서로 다른 책임이 혼재되어 있을 가능성이 높다. 각 변경 이유에 해당하는 코드를 별도의 클래스로 추출하는 것이 일반적인 첫 단계이다.
흔히 사용되는 구체적인 기법으로는 추상화와 인터페이스 분리가 있다. 공통된 동작은 추상 클래스나 인터페이스로 정의하고, 구체적인 책임은 이를 구현하는 개별 클래스에 위임한다. 또한, 의존성 주입을 활용하면 강한 결합 없이 분리된 책임들을 유연하게 조합할 수 있다. 예를 들어, 데이터 저장과 보고서 생성이라는 두 책임이 한 클래스에 있다면, 저장 로직은 리포지토리 패턴 클래스로, 생성 로직은 별도의 서비스 클래스로 분리할 수 있다.
다음 표는 몇 가지 대표적인 책임 분리 기법과 그 설명을 정리한 것이다.
기법 | 설명 |
|---|---|
클래스 추출(Extract Class) | 하나의 클래스에서 특정 책임을 담당하는 필드와 메서드를 묶어 새로운 클래스를 생성한다. |
인터페이스 분리(Interface Segregation) | 범용적인 인터페이스를 클라이언트의 필요에 따라 더 작고 구체적인 인터페이스들로 분리한다. |
위임(Delegation) | 특정 작업을 수행하는 책임을 다른 헬퍼 클래스나 서비스 객체에 넘긴다. |
계층화(Layering) | 표현, 비즈니스 로직, 데이터 접근 등의 책임을 아키텍처 수준에서 명확한 계층으로 구분한다. |
이러한 기법들을 적용할 때는 분리의 적정 수준을 고려해야 한다. 지나치게 많은 작은 클래스는 시스템을 복잡하게 만들 수 있으며, 응집도를 유지하면서 자연스러운 경계를 찾는 것이 중요하다. 최종 목표는 각 모듈이 변경, 이해, 재사용되는 데 있어 명확한 이유 하나만을 가지도록 하는 것이다.
단일 책임 원칙을 위반한 코드를 개선하기 위해 적용할 수 있는 일반적인 리팩토링 패턴이 존재한다. 이러한 패턴들은 하나의 클래스가 지나치게 많은 책임을 지고 있을 때, 그 책임을 명확히 분리하여 새로운 클래스나 모듈로 이관하는 방법을 제시한다.
주요 패턴으로는 추출 클래스, 추출 메서드, 이동 메서드 등이 있다. 추출 클래스는 하나의 클래스에서 두 개 이상의 책임을 수행하는 부분을 찾아, 그 부분을 새로운 클래스로 분리하는 기법이다. 예를 들어, Order 클래스가 주문 정보 관리와 동시에 영수증 출력 및 이메일 발송 로직을 포함한다면, InvoicePrinter와 EmailService라는 별도의 클래스를 추출할 수 있다. 추출 메서드는 하나의 긴 메서드 내에서 명확히 구분되는 작업을 별도의 메서드로 분리하여 가독성을 높이고, 이후 클래스 분리의 후보가 될 수 있도록 한다. 이동 메서드는 한 클래스의 메서드가 다른 클래스의 데이터를 더 많이 사용한다면, 그 메서드를 데이터가 있는 클래스로 옮기는 패턴이다.
보다 고수준의 아키텍처 패턴도 적용될 수 있다. 전략 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하도록 만든다. 이는 하나의 클래스가 여러 방식으로 데이터를 처리하거나 출력하는 책임을 질 때, 각 방식을 별도의 전략 클래스로 분리하는 데 유용하다. 퍼사드 패턴은 복잡한 서브시스템에 대한 단순화된 인터페이스를 제공하는 패턴으로, 기존의 복잡하고 다중 책임을 가진 클래스들을 재구성한 후, 그들과 상호작용하는 단일 진입점을 제공할 때 사용된다. 이는 클래스를 분리한 후에도 사용 편의성을 유지하는 데 도움을 준다.
패턴 이름 | 핵심 목적 | 적용 시나리오 예시 |
|---|---|---|
관련 있는 필드와 메서드를 새 클래스로 묶기 | 사용자 정보 관리와 사용자 인증 로직이 한 클래스에 혼재 | |
메서드 내의 특정 작업을 독립된 메서드로 분리 | 긴 메서드 내에서 데이터 검증, 포맷팅, 저장 로직이 모두 포함 | |
메서드를 해당 메서드가 주로 사용하는 데이터가 있는 클래스로 이동 |
| |
변하는 알고리즘을 캡슐화하고 런타임에 교체 가능하게 함 | 다양한 파일 형식(JSON, XML)으로 데이터를 내보내는 하나의 클래스 | |
복잡한 서브시스템에 대한 간단한 통합 인터페이스 제공 | 분리된 여러 관리 클래스(주문, 결제, 배송)에 대한 통합 조정 계층 제공 |

단일 책임 원칙은 SOLID 원칙의 첫 번째 원칙으로, 다섯 가지 객체 지향 설계 원칙의 기초를 형성한다. 이 원칙은 다른 SOLID 원칙을 적용하기 위한 전제 조건 역할을 하며, 특히 개방-폐쇄 원칙과 밀접하게 연관되어 있다.
개방-폐쇄 원칙은 소프트웨어 개체(클래스, 모듈, 함수 등)가 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙이다. 단일 책임 원칙을 준수하지 않은 클래스는 여러 변경 이유를 가지게 되며, 이는 결국 하나의 변경이 여러 다른 책임에 영향을 미쳐 기존 코드를 수정해야 하는 상황을 초래한다. 따라서 단일 책임 원칙을 통해 각 클래스의 책임을 명확히 분리하는 것은, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있는 구조를 만드는 개방-폐쇄 원칙의 실현 가능성을 높인다.
SOLID 원칙 내에서 단일 책임 원칙은 다음과 같은 관계를 가진다.
원칙 | 단일 책임 원칙과의 관계 |
|---|---|
개방-폐쇄 원칙 (OCP) | 단일 책임 원칙이 지켜져야 변경 지점이 분리되어, 확장 시 수정을 최소화하는 OCP를 달성하기 쉬워진다. |
리스코프 치환 원칙 (LSP) | 명확한 단일 책임은 하위 타입이 상위 타입의 행동을 보장하는 계약을 더 명확하게 정의하는 데 기여한다. |
인터페이스 분리 원칙 (ISP) | 클라이언트가 필요로 하지 않는 메서드에 의존하지 않도록 하는 ISP는 SRP를 인터페이스 수준에서 적용한 것으로 볼 수 있다. |
의존관계 역전 원칙 (DIP) | 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 DIP는, 세부 사항이 아닌 추상화에 의존하는 구조를 통해 책임의 경계를 명확히 하는 데 도움을 준다. |
이처럼 단일 책임 원칙은 다른 원칙들의 효과적인 적용을 위한 토대를 제공한다. 단일 책임 원칙이 무너지면, 변경이 시스템 여러 부분에 파급되어 개방-폐쇄 원칙을 위반하게 되고, 과도한 결합으로 인해 리스코프 치환 원칙과 인터페이스 분리 원칙을 준수하기 어려워지며, 결국 유연한 의존관계 역전을 구현하는 데도 장애가 된다.
단일 책임 원칙은 SOLID 원칙의 첫 번째 글자 'S'를 차지하며, 이 다섯 가지 객체 지향 설계 원칙의 기초를 형성한다. 다른 원칙들이 효과적으로 작동하기 위한 전제 조건 역할을 한다. 만약 하나의 클래스나 모듈이 여러 책임을 지니면, 그 자체로 응집도가 낮아지고 변경이 빈번해져 다른 원칙들을 적용하기 어려운 환경이 조성된다.
다른 SOLID 원칙과의 구체적인 관계는 다음과 같다.
개방-폐쇄 원칙 (OCP): 단일 책임 원칙이 잘 지켜진 클래스는 하나의 변경 이유만을 가지므로, 그 변경을 수용하기 위한 확장(개방)이 명확해지고 기존 코드 수정(폐쇄)을 최소화할 수 있다.
리스코프 치환 원칙 (LSP): 단일하고 명확한 책임을 가진 클래스는 행위의 계약이 분명해져, 하위 클래스가 상위 클래스를 올바르게 대체할 수 있는 가능성이 높아진다.
인터페이스 분리 원칙 (ISP): 이 원칙은 인터페이스 수준에서의 단일 책임 원칙이라 볼 수 있다. 클라이언트가 필요로 하지 않은 메서드에 의존하지 않도록 큰 인터페이스를 구체적이고 작은 인터페이스로 분리하는 것이 핵심이다.
의존관계 역전 원칙 (DIP): 고수준 모듈이 저수준 모듈에 의존하지 않도록 하려면, 각 모듈의 책임이 명확히 분리되어 있어야 추상화를 통한 의존성 역전이 효과적으로 이루어진다.
따라서 단일 책임 원칙은 SOLID의 출발점이자 토대이다. 이 원칙을 무시하고 복합적인 책임을 가진 모듈을 만들면, 나머지 원칙들을 적용하려는 시도 자체가 복잡해지거나 무의미해질 수 있다.
단일 책임 원칙과 개방-폐쇄 원칙은 SOLID 원칙을 구성하는 두 가지 핵심 원칙으로, 서로 밀접하게 연관되어 있다. 단일 책임 원칙은 하나의 클래스가 변경되어야 할 이유를 하나만 가지도록 하는 데 초점을 맞춘다. 이는 결과적으로 각 클래스가 하나의 잘 정의된 책임만을 수행하게 함으로써, 시스템의 구성 요소를 더 작고 응집력 높은 단위로 분리한다.
이러한 분리는 개방-폐쇄 원칙을 효과적으로 적용하기 위한 필수적인 토대를 마련한다. 개방-폐쇄 원칙은 소프트웨어 개체(클래스, 모듈, 함수 등)가 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 것을 의미한다. 단일 책임 원칙을 준수하여 각 책임이 고립된 클래스로 분리되면, 새로운 기능을 추가할 때 기존 클래스를 수정하기보다는 새로운 클래스를 생성하거나 기존 클래스를 확장하는 방식으로 변경을 수용하기가 훨씬 쉬워진다. 예를 들어, 보고서를 생성하는 책임과 보고서를 출력하는 책임이 하나의 클래스에 섞여 있다면, 출력 형식(PDF, HTML 등)을 추가하려 할 때 보고서 생성 로직까지 수정할 위험이 있다. 단일 책임 원칙에 따라 이 두 책임을 분리하면, 새로운 출력 형식을 추가할 때는 출력 전용 클래스를 확장하기만 하면 되므로, 기존 보고서 생성 클래스는 수정하지 않아도 된다[2].
따라서 단일 책임 원칙은 개방-폐쇄 원칙을 실현 가능하게 하는 선행 조건 또는 수단으로 작용한다고 볼 수 있다. 책임이 명확히 분리되지 않은 모놀리식 클래스는 새로운 요구사항이 발생했을 때 해당 클래스 자체를 직접 수정할 가능성이 높으며, 이는 개방-폐쇄 원칙의 '수정에 닫혀 있다'는 조건을 위반하게 만든다. 반면, 잘 정의된 단일 책임을 가진 클래스들은 서로 느슨하게 결합되어 있어, 시스템의 다른 부분에 영향을 최소화하면서 기능을 확장하는 것이 용이해진다. 이 두 원칙의 조화는 변경에 유연하고 안정적인 소프트웨어 아키텍처를 구축하는 데 중요한 기여를 한다.

실무에서 단일 책임 원칙을 적용할 때는 이론적인 이상과 현실적인 제약 사이에서 균형을 찾는 것이 중요하다. 모든 메서드나 작은 클래스를 무조건적으로 분리하는 것은 오히려 클래스 폭발(Class Explosion)을 초래하여 시스템을 더 복잡하게 만들 수 있다. 따라서 책임의 크기와 범위를 적절히 조절해야 한다. 하나의 변경 이유를 가진다는 것은, 해당 클래스가 담당하는 비즈니스 로직이나 기술적 관심사가 동일한 주기와 동기로 변경된다는 의미이다. 예를 들어, 보고서를 생성하는 클래스가 보고서 형식(HTML, PDF)이 바뀔 때와 데이터를 조회하는 로직이 바뀔 때 모두 영향을 받는다면, 이는 두 개의 책임을 가지고 있다고 판단할 수 있다.
적용 수준을 결정하는 것은 또 다른 주요 고려사항이다. 단일 책임 원칙은 메서드, 클래스, 모듈, 심지어 아키텍처 수준에서도 적용될 수 있다. 실무에서는 보통 클래스 수준에서 가장 활발히 논의된다. 그러나 마이크로서비스 아키텍처에서는 각 서비스가 하나의 명확한 비즈니스 능력(Business Capability)을 갖도록 하는 것이 넓은 의미의 SRP 적용이라고 볼 수 있다. 너무 이른 시기에 과도하게 책임을 분리하면 추상화 비용이 증가하므로, 변경이 실제로 반복적으로 발생하거나 예상될 때 리팩토링을 통해 점진적으로 분리하는 접근법이 효과적이다.
팀 내에서 '책임'에 대한 공통된 이해를 형성하는 것도 실패를 줄이는 핵심 요소이다. 개발자마다 책임의 경계를 다르게 해석할 수 있기 때문이다. 이를 위해 도메인 주도 설계의 바운디드 컨텍스트나 유비쿼터스 언어 같은 개념을 활용하여 비즈니스 도메인 내에서 자연스러운 책임의 경계를 찾는 것이 도움이 된다. 또한, 응집도가 높고 결합도가 낮은 모듈을 설계하는 다른 원칙들과 함께 종합적으로 적용할 때 그 진가를 발휘한다.