SOLID 원칙을 정처기 공부하면서 처음 접해서였는지, 그저 외우기 바빴다. “무슨 무슨 원칙이 있었지?” 정도로만 기억할 뿐, 각 원칙이 실제로 어떤 의미를 가지는지, 그리고 어떤 맥락에서 적용되어야 하는지는 제대로 이해하지 못했다. 말 그대로 ‘이해’가 아니라 ‘암기’였다.
그래서 이걸 어떻게 실제 코드에 적용해야 하는 건지, 내가 제대로 적용하고 있는 건지 늘 자신이 없었다.
그 중에 SRP (Single Responsibility Principle)를 직역해서 외웠던 게 오히려 독이 됐다.
아래와 같은 단순 무식한 결론을 내리게 되는데...
단일 책임 원칙이니까, 하나의 함수는 하나의 동작만 해야 하는구나!
이게 나의 첫 번째 오해였고, 이후 코드에 적용하면서 약간 의문이 생기기 시작했다.
그렇게 SRP를 “하나의 함수 = 하나의 동작”으로 해석한 채 프로젝트를 진행했다. 나름 장점도 있었다. 하나의 동작만 하려면 대체로 함수가 짧았다. 함수가 짧아지니 가독성은 올라갔다.
하지만 기능이 많아지고 상태를 관리하는 Bloc이 늘어날수록 점점... 쪼개는 게 오히려 복잡하고 불편하게 느껴지기 시작했다.
굳이 이렇게까지 쪼개야 되나?
예를 들어, 다음과 같은 초기화 관련 함수들이 있었다.
void _fetchCategoryList() {
context.read<CategoryListBloc>().add(FetchCategoryList());
}
void _fetchTimerHistory() {
context.read<AllTimerBloc>().add(GetTimerHistoryByDate(date: _homeDate));
}
void _fetchTodayGoal() {
context.read<TodayGoalBloc>().add(GetTodayGoal(goalDate: _homeDate));
}
단순히 SRP를 적용한다는 이유만으로 각각의 로직을 별도의 함수로 분리했다. 하지만 이 세 함수 모두 결국은 초기 데이터 로딩을 위한 작업이었다.
어차피 이렇게 initState 내부에서 전부 호출할 건데... 꼭 함수를 나눠야 하나? 싶은 거다.
void initState() {
super.initState();
_fetchCategoryList();
_fetchTimerHistory();
_fetchTodayGoal();
}
사실 의문은 진작 들었지만 시간적 여유가 없어 지금에서야 돌아보게 되었다. 왜냐면 그땐 설계보다는 구현에 집중할 수밖에 없는 환경이었다. 빨리 출시해야한다는 목표에 마음이 급했기 때문이다.
난... 말하는 감자일 뿐인데... UI도 구현해야 하고, 처음 써보는 라이브러리도 익혀야 하고, API 연동도 해야 했으니까...
그렇게 정작 "어떤 코드가 좋은 코드인가?"에 대한 고민을 소홀히 한 채로 프로젝트가 흘러갔다.
그리고 며칠 전 SOLID 원칙에 대한 글을 보게 됐다. 그리고 "SRP를 지켜야 하니까"라는 강박으로 코드를 쪼개는 것이 아님을 깨달았다.
그 글을 통해 SRP가 "하나의 클래스가 하나의 책임만을 가져야 한다"는 원칙이라는 걸 다시 제대로 정리하게 됐다. 그리고 나의 오해를 깨달았다.
여기서 말하는 책임은 단순히 "작은 단위의 동작"이 아니라, 변경의 이유가 기준이 되어야 한다.
즉, 내가 했던 것처럼 “함수 단위로 작게 쪼개는 것”이 아니라, 그 클래스나 함수가 왜 변경되어야 하는지의 이유가 명확하게 하나인가? 를 기준으로 판단해야 하는 것이었다.
아까 위에서 봤던 코드를 다시 보자.
void _fetchCategoryList() {
context.read<CategoryListBloc>().add(FetchCategoryList());
}
void _fetchTimerHistory() {
context.read<AllTimerBloc>().add(GetTimerHistoryByDate(date: _homeDate));
}
void _fetchTodayGoal() {
context.read<TodayGoalBloc>().add(GetTodayGoal(goalDate: _homeDate));
}
각 함수로 쪼개면서 의문이 들었던 이유가 이제 납득이 간다.
SRP 관점에서 보면, 내가 작성했던 다음과 같은 초기화 함수들은 전부 동일한 변경 이유를 가진다. 즉 ‘홈 화면의 초기 상태 세팅’이라는 책임을 가진다.
각각 나눠서 작성했지만 사실상 세 개 모두 동일한 시점에 호출되며 동일한 목적을 수행하는 것이다.
이럴 경우, 아래와 같이 하나의 함수로 묶는 것이 오히려 SRP에 더 부합한다.
void _initData() {
context.read<CategoryListBloc>().add(FetchCategoryList());
context.read<AllTimerBloc>().add(GetTimerHistoryByDate(date: _homeDate));
context.read<TodayGoalBloc>().add(GetTodayGoal(goalDate: _homeDate));
}
과거의 나는 “SRP를 지켜야 하니까 코드를 무조건 작게 쪼개야 해”라는 강박에 사로잡혀 있었고, 그것이 오히려 코드의 전체적인 복잡도를 높이고 유지보수성을 떨어뜨리는 결과를 낳았다.
중요한 건 코드 단위의 크기가 아니라, 그 단위가 가진 책임의 명확성이다.
‘변경의 이유’가 같다면 묶는 것이 자연스럽고, 그 이유가 다르다면 과감히 나누는 것이 맞다.
UI나 초기화 로직처럼 명확한 목적을 가진 작업은 블럭을 여러 개 사용하더라도 하나의 책임으로 봐도 된다.
SRP를 오해한 채 코드 구조를 만드는 것이 결국엔 복잡도를 높이고 유지보수를 어렵게 만든다는 걸 뼈저리게 느꼈다.
앞으로는 원칙을 지킨다는 이유로 맹목적으로 쪼개는 게 아니라,
진짜로 그 코드의 책임이 뭔지 고민하면서 구조를 잡아가야겠다는 생각이 든다.