1. 오래된 코드, 왜 이렇게 복잡해졌을까?
오래된 애플리케이션의 코드가 복잡해지는 데에는 단 하나의 원인만 있는 것은 아니다.
초기에는 잘 구조화되고 의도대로 동작하던 코드라 하더라도, 시간이 흐르면서 다양한 요인들이 누적되면서 점차 복잡도가 증가하는 경우가 많다.
예를 들어, 당장의 요구사항을 빠르게 반영해야 하는 상황에서 임시적인 방식으로 코드를 덧붙이기 시작하면, 어느새 전체 구조는 계획 없이 확장된다. 또한 인력의 잦은 교체나 설계 의도에 대한 공유 부족으로 인해 코드의 맥락을 이해하지 못한 채 기능이 누적되면서 점점 관리하기 어려운 형태가 되곤 한다.
이처럼 설계적 문제와 운영 현실이 맞물려 발생하는 코드의 복잡성은 다양한 원인에서 비롯되지만, 그 근본을 자세히 들여다보면 결국 공통된 구조적 문제로 수렴하는 경우가 많다. 바로 하나의 클래스가 너무 많은 일을 하고 있으며, 서로 다른 책임이 뒤섞여 있다는 점이다.

하나의 클래스나 모듈이 서로 무관한 여러 기능을 동시에 처리하기 시작하면, 그 구조는 점점 취약해진다. 하나의 수정이 다른 기능에 영향을 미치고, 테스트는 어려워지며 코드 변경은 위험한 작업이 되어 버린다. 결국 누구도 쉽게 손댈 수 없는 구조로 굳어진다.
따라서 우리는 클래스나 모듈이 하나의 책임에만 집중하도록, 설계 단계부터 이를 의식적으로 관리할 필요가 있다. 이 원칙을 구체화한 것이 바로 SOLID 원칙 중 하나인 단일 책임 원칙(SRP, Single Responsiblility Principle)이다.
이 글에서는 SRP가 무엇인지, 왜 중요한지, 어떻게 적용하고 오해하지 말아야 하는지를 살펴보고자 한다.
2. SRP란 무엇인가? 정의와 핵심 개념
단일 책임 원칙(SRP, Single Responsibility Principle)은 다음과 같이 정의된다.
"하나의 클래스는 오직 하나의 변경 이유만을 가져야 한다."
여기서 말하는 '변경 이유'란 해당 클래스를 수정하게 만드는 관심사(즉, 변경의 이유)를 의미한다. 만약 하나의 클래스가 한 개 이상의 이유로 수정될 가능성이 있다면, 그 클래스는 여러 책임을 동시에 지고 있으며, 이는 SRP 위반에 해당한다.
이 원칙을 보다 직관적으로 설명하기 위해 공연장의 무대 스태프를 예로 들어보자.
공연에서는 조명, 음향, 소품 등을 각각의 담당 스태프가 전담한다. 근데 예산 부족이나 인력 문제로 한 명이 이 모든 역할을 맡게 된다면 어떻게 될까?
- 조명에 집중하느라 음향 시작 타이밍을 놓칠 수 있다.
- 무대 전환 시 소품을 준비하느라 조명을 제때 끄지 못할 수 있다.
- 해당 스태프가 아무리 뛰어난 능력으로 1인 3역을 완벽히 수행하더라도, 추후 공연 방식이 바뀌거나 시스템이 변경되었을 때 이 구조를 유지한 채 수정하기는 매우 어렵다.

현실에서도 여러 책임이 한 사람에게 집중되면, 효율성과 안정성은 급격히 저하될 수밖에 없다. 애플리케이션 설계도 마찬가지이다. 하나의 클래스가 여러 역할을 담당하게 되면 변경 사유가 늘어나고, 이에 따라 복잡성과 취약성도 함께 증가한다.
따라서, 클래스는 항상 하나의 책임, 하나의 관심사만을 가져야 한다. 그래야 추후 변경이 발생하더라도 그 영향 범위가 최소화되며, 전체 구조 역시 유지보수가 용이한 형태로 정비될 수 있다.
3. SRP 위반 사례와 리팩토링 예제
앞서 언급한 무대 스태프의 역할을 코드로 표현해 보자. 앞서 언급한 스태프가 맡았던 작업은 다음과 같이 세 가지 책임으로 나뉜다.
- 🎶 음향 제어 작업
- 무대 시작 시 : 볼륨을 높이고 음악 재생
- 무대 종료 시 : 볼륨을 줄이고 음악 정지
- 💡 조명 제어 작업
- 무대 시작 시 : 조명 A 점등
- 무대 종료 시 : 조명 A 소등 후 B 점등
- 🎭 소품 제어 작업
- 무대 시작 시 : 커튼 개방 및 회전판 가동
- 무대 종료 시 : 커튼 닫기 및 회전판 정지
3-1. 초보 개발자의 잘못된 설계 예시
class StageController {
public void startStage() {
// [1] 음향 제어
increaseVolume();
playMusic();
// [2] 조명 제어
turnOnLightA();
// [3] 소품 제어
openCurtain();
startTurntable();
}
public void endStage() {
// [1] 음향 제어
decreaseVolume();
stopMusic();
// [2] 조명 제어
turnOffLightA();
turnOnLightB();
// [3] 소품 제어
closeCurtain();
stopTurntable();
}
// 음향 관련
private void increaseVolume() {
System.out.println("음향 볼륨을 높입니다.");
}
private void decreaseVolume() {
System.out.println("음향 볼륨을 줄입니다.");
}
private void playMusic() {
System.out.println("음악을 재생합니다.");
}
private void stopMusic() {
System.out.println("음악 재생을 종료합니다.");
}
// 조명 관련
private void turnOnLightA() {
System.out.println("조명 A를 켭니다.");
}
private void turnOffLightA() {
System.out.println("조명 A를 끕니다.");
}
private void turnOnLightB() {
System.out.println("조명 B를 켭니다.");
}
// 소품 관련
private void openCurtain() {
System.out.println("무대 커튼을 엽니다.");
}
private void closeCurtain() {
System.out.println("무대 커튼을 닫습니다.");
}
private void startTurntable() {
System.out.println("회전판을 회전시킵니다.");
}
private void stopTurntable() {
System.out.println("회전판을 멈춥니다.");
}
}
이 StageController 클래스는 🎶 음향, 💡 조명, 🎭 소품이라는 서로 다른 관심사를 한 클래스에서 동시에 다르고 있다. 이러한 구조는 SRP를 위반하며, 다음과 같은 문제를 야기한다.
- 하나의 기능(조명)을 변경하려고 해도 나머지 기능(음향, 소품)과 코드가 얽혀 있어 함께 영향받는다.
- 코드의 덩치가 매우 커서 단위 테스트가 어려워지며 이로 인해 예기치 않은 부작용이 발생할 가능성도 커진다.
- 결국 유지보수나 기능 확장이 극도로 어려운 구조로 굳어진다.
3-2. 능숙한 개발자의 SRP 적용 후 개선된 구조
SRP에 따라 관심사별로 클래스를 분리하면 다음과 같은 구조가 된다.
// 음향 제어 전담
class SoundController {
public void startSound() {
increaseVolume();
playMusic();
}
public void stopSound() {
decreaseVolume();
stopMusic();
}
private void increaseVolume() {
System.out.println("음향 볼륨을 높입니다.");
}
private void decreaseVolume() {
System.out.println("음향 볼륨을 줄입니다.");
}
private void playMusic() {
System.out.println("음악을 재생합니다.");
}
private void stopMusic() {
System.out.println("음악 재생을 종료합니다.");
}
}
// 조명 제어 전담
class LightingController {
public void startLighting() {
turnOnLightA();
}
public void stopLighting() {
turnOffLightA();
turnOnLightB();
}
private void turnOnLightA() {
System.out.println("조명 A를 켭니다.");
}
private void turnOffLightA() {
System.out.println("조명 A를 끕니다.");
}
private void turnOnLightB() {
System.out.println("조명 B를 켭니다.");
}
}
// 소품 제어 전담
class PropController {
public void startProps() {
openCurtain();
startTurntable();
}
public void stopProps() {
closeCurtain();
stopTurntable();
}
private void openCurtain() {
System.out.println("무대 커튼을 엽니다.");
}
private void closeCurtain() {
System.out.println("무대 커튼을 닫습니다.");
}
private void startTurntable() {
System.out.println("회전판을 회전시킵니다.");
}
private void stopTurntable() {
System.out.println("회전판을 멈춥니다.");
}
}
// 무대 전체를 조율하는 상위 조정자 클래스
class StageDirector {
private final SoundController soundController;
private final LightingController lightingController;
private final PropController propController;
public StageDirector() {
this.soundController = new SoundController();
this.lightingController = new LightingController();
this.propController = new PropController();
}
public void startStage() {
soundController.startSound();
lightingController.startLighting();
propController.startProps();
}
public void endStage() {
soundController.stopSound();
lightingController.stopLighting();
propController.stopProps();
}
}
4. SRP의 장점
SRP를 올바르게 적용한 클래스에서는 각 책임이 명확하게 분리되어 있으며, 각 클래스는 명확한 단일 책임을 중심으로 구성되어 있다. 음향, 조명, 소품 컨트롤러들은 상위의 조정자 역할 클래스를 통해 유기적으로 제어된다. 이 구조는 각 기능을 책임 단위로 나누고, 전체 흐름을 고수준의 추상화로 제어할 수 있게 해 준다.
이렇게 SRP를 적용한 구조는 다음과 같은 실질적인 이점을 제공한다.
- ✅ 수정 시 변경 이유가 명확해진다.
각 클래스는 하나의 책임만 가지게 되므로, 기능 하나가 바뀔 때 해당 클래스만 수정하면 된다. 예를 들어 조명 장치가 교체된다면 LightingController만 수정하게 되고 그 변경 사유는 '조명'이라는 관심사를 넘어서지 않는다. 즉, 변경의 이유가 하나라는 것은 수정 범위가 하나인 것으로 일치시킬 수 있다는 것이다.
- ✅ 유지보수와 디버깅이 쉬워진다.
SRP가 적용된 코드에서는 문제가 발생하였을 때 어느 책임에서 발생했는지 명확히 분별이 가능하다. 또한 클래스가 상대적으로 작고 역할이 명확하기 때문에 개발자는 코드를 읽고 파악하는 데 드는 시간이 줄어든다.
- ✅ 테스트가 용이해진다.
SRP가 적용된 클래스는 단위 테스트가 단순하고 명확하다. 예를 들어, 소품 제어 클래스 하나만 테스트하고 싶다면 클래스가 정확히 나뉘어 있기 때문에 음향이나 조명 제어와는 무관하게, 소품 제어만 독립적으로 테스트할 수 있다.
- ✅ 사용과 조합이 쉬워진다.
각 기능이 독립된 모듈이기 때문에 다른 시스템에서도 쉽게 재사용이 가능하다. 예를 들어, 다른 연극에서도 같은 소품 제어가 사용된다면 PropController를 복사, 붙여 넣기 하여 동일하게 사용할 수 있다.
- ✅ 협업과 분담이 명확해진다.
역할이 명확하게 구분이 가능하여 팀원 간에 분업이 깔끔하게 이루어진다. 또한 클래스 간 책임 관계도 자연스럽게 드러난다.
- ✅ 상위 로직을 전체 흐름의 의미 단위로 추상화할 수 있다.
각 책임이 잘 분리되어 있어서 전체 동작을 의미 단위로 캡슐화한 추상 메서드로 표현할 수 있다. 이는 고수준 정책과 저수준 세부 구현을 분리할 수 있게 하여 유지보수성을 향상한다.
5. SRP 적용을 판단하는 질문들
SRP는 객체 지향 프로그래밍(OOP)의 기본이자 핵심으로 여겨지지만 이 원칙을 기계적으로 해석하거나 지나치게 적용하면, 오히려 설계에 부작용을 초래할 수 있다. 대표적인 오해는 SRP를 다음과 같이 이해하는 것이다.
- 클래스는 작아야 한다.
- 클래스는 하나의 기능만 가져야 한다.
물론 지금까지 SRP는 하나의 클래스가 하나의 책임만을 가져야 한다고 설명해 왔다. 하지만 여기서 말하는 '책임'이란 단순히 기능의 수가 아니라 "이 클래스가 바뀌는 이유가 하나인가?"라는 관점에서 정의되는 관심사이다.
예를 들어, 클래스 안에 여러 기능이 존재하더라도 그 기능들이 동일한 목적과 맥락 아래서 동작하고 모두 동일한 변경 이유를 가진다면 그것은 단일 책임을 지닌 구조라고 할 수 있다. 반대로, 기능의 개수는 적지만 서로 다른 관심사(예컨대, 데이터 처리와 UI 표시)가 섞여 있다면 SRP는 이미 위반되고 있는 것이다.
SRP를 오해한 나머지, 불필요할 정도로 책임을 세분화한다면 어떤 일이 벌어질까? 클래스는 의미 없이 많아지고, 전체 구조는 흐릿해지며 응집력이 낮고 이해하기 어려운 코드가 될 것이다. 책임이 제대로 구분되지 않고 실질적 책임 없이 단순히 분리된 클래스로 구성된 골조는 SRP를 따르는 것이 아니라 오히려 설계자의 의도를 흐리게 할 것이다.
따라서 SRP를 적용할 때는 "왜 분리해야 하는가?"라는 질문을 중심에 두고 판단해야 한다. 다음과 같은 기준을 통해서 SRP 적용 여부를 판단할 수 있다.
- ❗️클래스가 변경되는 이유가 하나인가?
클래스를 변경할 때 그 이유가 하나인지 점검해야 한다. 일반적으로 우리는 변경 후 커밋 메시지를 작성하며 무엇이 바뀌었는지 기록한다. 이때 그 이유가 둘 이상이라면, 해당 클래스는 이미 서로 다른 책임이 섞여 있을 가능성이 높다.
- ❗️클래스의 기능들이 동일한 관심사 아래 있는가?
기능이 여러 개 있더라도, 그것들이 모두 같은 목적을 향해 동작하고, 서로 밀접하게 연관되어 있다면 굳이 분리할 필요가 없다. 예를 들어 파일 확장자를 변환하는 여러 메서드는 모두 같은 관심사(파일 포맷 처리)에 속한다. 그러나 그 안에 사용자 인터페이스 처리나 DB 저장 같은 기능이 섞여 있다면, 서로 다른 관심사가 혼합된 것이므로 분리가 필요하다.
- ❗️특정 기능만 수령하려는데, 다른 기능도 함께 손대야 하는가?
SRP가 잘 지켜진 클래스라면, 하나의 기능만 수정할 때 다른 기능과 충돌 없이 변경할 수 있어야 한다. 만약 관련 없는 기능 코드까지 영향을 받는다면, 그 클래스는 이미 여러 책임이 얽혀 있다는 증거이다.
- ❗️기능별로 독립적인 테스트가 가능한가?
테스트를 작성할 때, 하나의 기능만 검증하고자 해도 다른 기능의 세팅이나 우회가 필요하다면, SRP가 지켜지지 않았을 가능성이 높다. SRP를 잘 적용하면, 각 책임은 단독으로 테스트 가능한 단위로 유지된다.
- ❗️미래에 다른 방향으로 변경될 가능성이 있는가?
지금 당장은 하나의 책임처럼 보이지만 비즈니스 흐름상 해당 기능이 앞으로 분기되거나 확장될 가능성이 있다면, 미리 책임을 분리해 구조화해 두는 것이 유리할 수 있다. 단, 이 경우에도 실제 변경 가능성과 복잡도를 고려하여야 한다.
- ❗️책임을 분리했을 때 설계가 더 명확해지는가?
클래스를 나누었을 때 전체 구조가 더 깔끔해지고, 각 클래스의 역할이 명확해진다면 SRP 적용이 적절한 경우이다. 반대로 분리하고 나서 오히려 설계가 더 복잡해지고 기능 흐름 파악이 어렵다면, 응집이 더 필요한 구조일 수 있다.
6. 마무리 - 코드 복잡도를 줄이는 설계 원칙 SRP
지금까지의 내용을 정리하면 다음과 같다.
단일 책임 원칙(SRP, Single Responsibility Principle)은 "하나의 클래스는 오직 하나의 변경 이유만을 가져야 한다."는 설계 원칙이다. 이는 복잡한 소프트웨어에서 각 클래스의 책임 경계를 명확히 하여 변경을 쉽게 만들고 유지보수성을 향상하기 위한 핵심 지침이다.
SRP를 올바르게 적용하면 다음의 이점을 가진다.
- 수정 시 변경 이유가 명확해진다.
- 유지보수와 디버깅이 쉬워진다.
- 독립적인 테스트가 가능해진다.
- 각 기능의 재사용성이 높아지고 팀 협업 시 분업이 명확해진다.
SRP는 단순히 "클래스를 작게 만들어라." 또는 "클래스가 하나의 기능만 가져라."는 의미가 아니다. SRP 적용 시 언급되는 책임의 의미는 기능의 개수가 아니라 클래스가 바뀌는 이유를 의미한다. 여러 기능이 있어도 동일한 관심사 아래서 동일한 변경 이유를 가진다면 단일 책임을 지닌 구조라 할 수 있다.
SRP 적용 여부는 다음의 질문들로 판단할 수 있다.
- 클래스가 변경되는 이유가 하나인가?
- 클래스의 기능들이 동일한 관심사 아래 있는가?
- 특정 기능만 수정하려는데 다른 기능도 함께 손대야 하는가?
- 기능별로 독립적인 테스트가 가능한가?
- 미래에 다른 방향으로 변경될 가능성이 있는가?
- 책임을 분리했을 때 설계가 더 명확해지는가?
불필요한 세분화는 오히려 클래스를 의미 없이 많아지게 하고 전체 구조의 응집력을 떨어뜨릴 수 있다. 따라서 "왜 분리해야 하는가?"라는 질문을 중심에 두고, 클래스가 의미 있는 단위로 설계될 수 있도록 지속적인 고민을 해야 한다.
SRP는 복잡한 코드의 실타래를 풀어내는 데 있어 실질적인 방향성을 제공하는 나침반이다. 물론 이 나침반이 항상 정답은 아니며, 실무의 복잡한 요구사항이나 기존 시스템 구조에 따라 적절한 유연성도 필요하다. 원칙을 기준으로 삼되, 현실에 맞게 조율하는 균형 감각이 중요하다.
'설계 > 설계 원칙' 카테고리의 다른 글
| SOLID의 DIP : 변화에 강한 구조를 만드는 의존성 역전 원칙 (0) | 2025.05.12 |
|---|---|
| SOLID의 ISP : 역할 중심 설계를 도와주는 인터페이스 분리 원칙 (0) | 2025.05.07 |
| SOLID의 LSP : 왜 타입만 맞으면 안 되는가? 설계 오류를 막는 리스코프 치환 원칙 (0) | 2025.05.02 |
| SOLID의 OCP : 조건문 없이 유연하게 확장하는 개방-폐쇄 원칙 (0) | 2025.05.01 |
