정의
흔히 “클래스는 하나의 일만 해야 한다”로 외우지만, 실무에서 더 정확한 해석은 “변경 이유(Reason to Change)가 하나여야 한다” 입니다. 즉, 한 클래스가 서로 다른 이유로 자주 변경된다면 SRP가 깨지고 있을 가능성이 큽니다.
“SRP는 ‘기능을 쪼개는 기술’이 아니라,
변경의 원인을 한 곳에 가두는 기술이다.”
핵심 개념
책임을 “기능 목록”으로 나누면 과도하게 쪼개질 수 있습니다. 대신 실무에서는 누가(어떤 이해관계자) 이 코드를 바꾸게 만드는가를 기준으로 책임을 나누는 편이 더 정확합니다.
판단 기준
아래 질문에 “예”가 많을수록 SRP가 깨졌을 가능성이 큽니다.
- ✔️ “정책 변경”이 오면 수정하는 코드와 “기술 변경(DB/캐시/프레임워크)”이 오면 수정하는 코드가 한 클래스에 섞여 있나?
- ✔️ 한 메서드 수정이 다른 기능의 버그로 이어지는 일이 잦은가?
- ✔️ 테스트가 커지고(Mock/스텁이 과도), 변경 영향 범위가 넓게 퍼지는가?
- ✔️ 한 클래스가 “검증 + 계산 + 저장 + 포맷팅 + 로깅”까지 다 하고 있나?
예시
// 개념 예시 (BAD)
// - 할인 정책 변경
// - 재고 검증 규칙 변경
// - 결제 방식 변경
// - DB 저장 방식 변경
// - 로그/포맷 변경
// 서로 다른 이유로 계속 바뀜 → SRP 위반 신호
class OrderService {
public int checkout(Order order) {
validate(order); // 검증
int price = calcPrice(order); // 계산/정책
saveToDb(order, price); // 저장(인프라)
logAsJson(order, price); // 로깅/포맷
return price;
}
}
// 개념 예시 (GOOD)
interface OrderValidator { void validate(Order order); }
interface PricingPolicy { int priceOf(Order order); }
interface OrderRepository { void save(Order order, int price); }
interface OrderLogger { void log(Order order, int price); }
class CheckoutService {
private final OrderValidator validator;
private final PricingPolicy pricing;
private final OrderRepository repo;
private final OrderLogger logger;
CheckoutService(OrderValidator v, PricingPolicy p, OrderRepository r, OrderLogger l) {
this.validator = v; this.pricing = p; this.repo = r; this.logger = l;
}
public int checkout(Order order) {
validator.validate(order); // 검증 변경은 Validator에서
int price = pricing.priceOf(order); // 정책 변경은 Pricing에서
repo.save(order, price); // 저장 변경은 Repo에서
logger.log(order, price); // 로깅 변경은 Logger에서
return price;
}
}
리팩터링
주의점
SRP를 과하게 적용하면 파일/클래스가 너무 잘게 쪼개져 흐름 파악이 어려워질 수 있습니다. 그래서 “한 번에 이해 가능한 단위”를 유지하면서, 변경 이유가 섞이는 지점만 정확히 분리하는 것이 실무적으로 가장 효율적입니다.
핵심 요약
✅ 핵심 요약
- ✔️ SRP는 “하나의 기능”이 아니라 하나의 변경 이유(Reason to Change)를 갖게 하는 원칙이다.
- ✔️ 서로 다른 변경 이유가 한 클래스에 섞이면 수정 범위가 커지고 버그 확률이 올라간다.
- ✔️ 분리 후에는 상위 계층에서 조립(DI)하고, 테스트는 더 작고 명확해진다.