목차
한 클래스가 너무 많은 일을 한다
주문을 나타내는 Order 클래스가 이렇게 생겼다고 하자.
public class Order {
private List<Item> items;
public int total() { // ① 금액 계산 (할인·세금)
// 비즈니스 규칙
}
public void save(Connection conn) { // ② DB에 저장
// SQL로 orders 테이블에 insert
}
public void sendConfirmationEmail() { // ③ 확인 메일 발송
// SMTP로 메일 전송
}
}한 클래스가 계산도 하고, 저장도 하고, 메일도 보낸다. 편해 보인다. “주문에 관한 것”은 다 여기 있으니까. 그런데 이게 문제의 씨앗이다.
단일 책임 = 바뀌는 이유가 하나다
단일 책임 원칙(SRP)은 흔히 “클래스는 하나의 일만 해야 한다”로 알려져 있다. 그런데 더 정확한 정의는 이것이다.
클래스가 바뀌어야 하는 이유는 하나여야 한다.
Order를 보자. 이 클래스는 세 가지 이유로 바뀐다.
- 할인 규칙이 바뀌면 →
total()을 고친다 - DB 스키마가 바뀌면 →
save()를 고친다 - 메일 문구가 바뀌면 →
sendConfirmationEmail()을 고친다
서로 아무 상관없는 세 가지 변경이 한 파일에 모여 있다.
”일”이 아니라 “이유”다
여기서 흔한 오해를 짚자. SRP를 “메서드를 하나만 두라”거나 “클래스를 무조건 잘게 쪼개라”로 읽으면 틀린다.
total()을 계산 단계별로 다섯 메서드로 나눠도 그건 SRP와 상관없다. 그 다섯은 다 같은 이유(비즈니스 규칙)로 바뀌니까. 반대로 메서드가 셋뿐이어도, 셋이 서로 다른 이유로 바뀌면 그게 위반이다.
기준은 메서드 개수가 아니라 **“바뀌는 이유”**다. 같은 이유로 바뀌는 것끼리는 한데 있어도 되고, 다른 이유로 바뀌는 것이 섞이면 그게 문제다.
고치다 옆이 깨진다
세 이유가 한 클래스에 있으면 무슨 일이 나나.
메일 문구를 바꾸려고 Order를 열었다가, 실수로 total()이나 save()를 건드릴 수 있다. 메일만 손보려던 사람이 계산 로직이 든 파일을 통째로 마주하는 것이다. 하나를 고치려 열었는데 관계없는 것들이 같이 노출되고, 같이 위험해진다.
테스트가 딸려 온다
계산이 맞는지만 확인하고 싶어도, total()이 save()·sendConfirmationEmail()과 한 클래스에 묶여 있으면 그 클래스를 세우는 순간 DB 연결과 메일 설정까지 따라온다. 순수하게 계산만 보려던 테스트가 인프라에 발이 묶이는 것이다.
바뀌는 이유가 하나인 클래스는 그 이유 하나만 세워 두고 테스트하면 된다. 여러 이유가 얽히면 테스트도 딱 그만큼 얽힌다.
책임으로 쪼갠다
고침은 단순하다. 이유별로 클래스를 나눈다.
public class Order {
public int total() { /* 계산만 */ }
}
public class OrderRepository {
public void save(Order order) { /* 저장만 */ }
}
public class OrderNotifier {
public void sendConfirmation(Order order) { /* 메일만 */ }
}이제 각 클래스는 바뀌는 이유가 하나다. 메일 문구가 바뀌면 OrderNotifier만 열고, Order와 OrderRepository는 쳐다볼 일이 없다.
누가 바꾸라 하는가 - 액터
이 원칙을 더 날카롭게 보는 방법이 있다. “이유”를 사람으로 바꿔 생각하는 것이다. 변경을 요구하는 쪽을 액터라고 하면:
total()의 규칙은 회계·영업이 바꾼다save()의 스키마는 인프라·DBA가 바꾼다- 메일 문구는 마케팅이 바꾼다
세 액터가 한 클래스를 공유하면, 한 액터의 요구가 다른 액터의 코드를 흔든다. 마케팅이 메일을 바꿨는데 회계 계산이 깨지는 식이다. 그래서 SRP는 **“한 클래스는 한 액터에게만 답한다”**로 읽으면 가장 정확하다.
같은 이유는 모은다 - 응집
SRP를 ‘쪼개는 원칙’으로만 읽으면 절반만 본 것이다. 나머지 절반은 모으는 것이다. 이 원칙은 다른 이유를 가르라고만 하지 않는다. 같은 이유로 바뀌는 것은 한데 모으라고도 한다.
할인 규칙과 관련된 로직이 여러 파일에 흩어져 있으면, 규칙 하나 바꾸는 데 이곳저곳을 돌아다녀야 한다. 같은 이유로 바뀌는 것을 한 곳에 모아 두는 성질을 응집이라 한다. 한 클래스 안의 것들이 다 같은 이유로 움직이면 응집이 높고, 제각각 다른 이유로 움직이면 응집이 낮다. SRP는 결국 응집을 높이는 원칙이다.
너무 쪼개면 파편화된다
그래서 반대 함정도 있다. SRP를 “무조건 작게”로 밀면 클래스가 파편화돼 오히려 나빠진다. 한 줄짜리 클래스가 수십 개가 되면, 관련된 걸 찾아 여기저기 헤매게 된다 - 응집이 도로 낮아지는 것이다.
기준은 늘 같다. 무작정 잘게 쪼개는 게 아니라, 변경의 축을 따라 나눈다. 다른 이유는 가르고, 같은 이유는 붙여 둔다.
실무: 비대한 서비스가 신호다
실무에서 SRP 위반은 대개 비대한 서비스 클래스로 나타난다. UserService 하나가 회원가입·인증·정산·통계·메일을 다 들고 500줄이 되는 식이다.
신호는 이렇다.
- 클래스를 한 문장으로 설명하는데 “그리고”가 자꾸 들어간다 (사실은 여러 개다)
- 한 메서드를 고쳤는데 관계없는 테스트가 깨진다
- 서로 다른 팀이 같은 파일을 자주 만진다 (액터가 여럿이라는 증거)
이럴 때 “무엇이 이 클래스를 바꾸게 하는가”를 종이에 적어 보면, 자연스러운 분리선이 드러난다.
정리
| SRP란 | 클래스가 바뀌는 이유는 하나여야 한다 |
| 흔한 오해 | ”일 하나”가 아니라 “이유 하나”. 메서드 개수 문제가 아니다 |
| 왜 | 다른 이유가 섞이면, 하나를 고치다 관계없는 게 깨진다 |
| 액터로 보면 | 한 클래스는 한 액터(변경을 요구하는 쪽)에게만 답한다 |
| 양면 | 다른 이유는 가르고, 같은 이유는 모은다(응집). 무작정 쪼개기가 아니다 |
| 신호 | 비대한 서비스, 이름의 “그리고”, 여러 팀이 만지는 파일 |
한 클래스에 여러 이유를 담지 않는 것. 그래야 하나를 바꿀 때 나머지가 안전하다. 그게 단일 책임이 지키는 것이다.