정보 시스템의 비즈니스 규칙은 지속적으로 진화하고 점점 더 복잡해지고 있습니다.
비즈니스 규칙을 빠르게 반영할 때 흔히 쓰이는 임시방편적인 방식은 기존 메서드에 코드를 덧붙이거나 클래스에 메서드를 추가하는 식으로 코드를 추가하는 것입니다.
그러다 보면 어느새 클래스가 길어지고, 이를 유지보수하는 데 엔지니어의 많은 에너지가 소모됩니다. 아무리 잘 디자인한 메서드나 클래스라도, 분량이 2,000줄이 되면 이해하기 어려운 건 마찬가지입니다.
복잡한 코드를 작은 단위로 나누는 것이 좋은 접근법이라는 사실을 설득하는 건 어렵지 않습니다.
작은 단위는 항상 큰 단위보다 낫고, 그 이유는 다음과 같습니다.
클래스나 코드 단위가 작으면 개발자가 읽어야 할 코드의 양이 줄어든다.
메서드가 다른 클래스를 호출하는 구조라면 정말 필요할 때만 다른 클래스의 코드를 열게 됩니다.
작은 코드 단위는 처음부터 확장 가능성을 제공한다.
모델링하고 나면 필요한 경우, 각 조각을 대체할 수 있습니다.
테스트 가능성이 향상된다.
클래스가 작으면 개발자가 비즈니스 로직의 특정 부분에 대해 독립적인 단위 테스트를 작성할지 여부를 쉽게 결정할 수 있습니다.
실제로는 작은 메서드나 클래스를 사용해 복잡한 동작을 구축해야 합니다.
지금부터는 언제 코드를 메서드와 클래스로 나눌지 정하는 몇 가지 휴리스틱을 살펴보겠습니다.
휴리스틱(Heuristics, 발견법)은 불충분한 시간이나 정보 속에서 빠르고 효율적인 결정을 내리기 위해 사용하는 간편한 추론 방법으로, 인간의 인지 과정에서 나타나는 특징입니다. 경험에 기반한 간단한 규칙이나 어림짐작을 통해 복잡한 문제를 해결하거나 의사결정을 하지만, 때로는 판단의 오류를 유발하기도 합니다.
큰 메서드를 작은 메서드 여럿으로 나누는 것은 복잡성을 줄이는 훌륭하고 쉬운 방법입니다.
응집도가 높은 컴포넌트(클래스나 메서드)는 시스템 안에서 하나의 명확한 책임을 가집니다.
응집도 높은 코드를 추구하면 자연스럽게 단순한 코드를 추구하게 됩니다.
비공개 메서드는 자신이 선언된 클래스 내부에서만 호출이 가능합니다.
코드 일부를 분리하고 싶지만, 클래스 밖에서 그 코드를 보거나 호출하기를 원하지 않을 때는 비공개 메서드가 완벽한 해답입니다.
새로운 비공개 메서드를 도입하는 게 적합한지, 코드 세그먼트가 독립된 단위가 될 수 있는지를 판단하는 가장 좋은 방법은 다음 5가지를 평가하는 것입니다.
새 비공개 메서드의 목적을 설명하는 명확한 이름을 부여할 수 있는가?
새 메서드가 응집력 있고 작으며, 공개 메서드가 쉽게 사용할 수 있는 동작을 수행하는가?
새 메서드가 많은 파라미터나 클래스의 의존성에 의존하는가? 개발자가 새 메서드의 요구사항을 빠르게 이해할 수 있을 만큼 간결한가?
메서드가 호출될 때 구현을 살펴보지 않아도 이름만으로 기능을 충분히 설명할 수 있는가?
이 비공개 메서드를 정적 메서드로 만들 수 있는가?
비공개 메서드가 추출한 코드를 두기에 이상적인 장소가 아닐 수도 있습니다. 특히 코드가 큰 단위의 주요 목표와 관련이 없다면 그렇습니다.
이 코드 조각이 클래스의 나머지 부분과는 다른 작업을 하는가?
이 코드가 도메인 안에서 별도의 이름과 클래스가 필요할 정도로 중요한 일을 하는가?
이 코드 조각을 독립적으로 테스트하고 싶은가?
이 코드 조각이 다른 코드들이 의존하기를 원하지 않는 클래스에 의존하는가?
모든 규칙에는 예외가 있습니다. 언제 코드를 유지해야 할까요?
둘 이상의 퍼즐 조각이 독립적으로 존재할 수 없을 때, 강제로 분리하면 메서드 시그니처가 복잡해질 수 있습니다.
퍼즐 조각이 교체될 가능성이 낮을 때
해당 조각만 완전히 따로 떼어 테스트할 만한 가치가 없을 때
퍼즐 조각의 개수가 몇 개 없을 때
이러한 때는 언제나 그렇듯 실용적인 접근이 중요하니 참고해두는 것이 좋습니다.
더 복잡한 리팩터링 작업에서는 리팩터링이 끝난 최종 코드가 어떤 모습일지 상상해 봅시다.
리팩터링 후 클래스는 어떤 모습이며, 서로 어떻게 연관되는가?
미래의 모습이 마음에 드는가?
디자인의 문제점이 보이는가?
피플그로우에서는 직원 데이터를 일괄적으로 가져옵니다.
관리자는 직원의 이름, 이메일, 역할, 입사일이 포함된 CSV 파일을 업로드 하는데요.
직원이 데이터베이스에 이미 있으면 피플그로우는 그 직원의 정보를 갱신합니다.
초기 구현은 코드 2-1의 코드와 비슷하며, 이 코드는 서드파티 라이브러리를 사용해 CSV를 파싱합니다.
그 후 시스템은 임포트한 데이터의 각 직원에 대해 데이터베이스에 새로운 직원을 생성하거나 데이터베이스에 있던 기존 직원을 갱신합니다.
코드는 복잡하지 않지만, 이는 단지 상황을 보여주기 위한 예시일뿐입니다.
실제 소프트웨어 시스템에서는 임포트 서비스가 수백 줄의 코드로 이뤄질 수도 있어요.
먼저 CSV 파싱 로직을 별도의 클래스로 옮겨보겠습니다. 이 코드 몇 줄은 실제 작업을 CsvParserLibrary 클래스에 위임할 뿐이지만, 책임이 다르므로 독립된 클래스에도 잘 어울립니다.
또한, EmployeeimportCSVParser 클래스는 EmployeeParsedData의 리스트를 반환하는 parse 메서드를 제공합니다.
ImportEmployeeService 클래스를 개선할 수 있는 방법은 아직 더 있습니다.
import 메서드가 작업 흐름만 제어하도록 만들고, 실제 동작은 다른 클래스나 메서드에 구현하면 됩니다.
예를 들어, if 문에서 새로운 직원을 생성하거나 갱신하는 두 코드 블록을 비공개 메서드로 추출 가능합니다.
이 두 메서드를 다른 클래스로 옮기게 될 것 같지는 않지만, 이 메서드들은 서로 관련이 있어 보이므로 현재로서는 ImportEmployeesService에 남겨두는 것이 적절해 보입니다.
상단의 코드를 보면 클래스는 훨씬 작아졌고, 메서드들의 응집도는 더 커진 것을 확인 가능합니다. import 메서드는 작업을 조정하는 역할만 하고 있습니다.
EmployeeImportCSVParser를 호출해 파싱 결과를 얻고, EmployeeRepository를 호출해 직원이 데이터베이스에 존재하는지 확인한 후 이에 따라 적절한 동작을 결정해야 합니다. 이때 각각의 동작은 별도의 비공개 메서드가 처리합니다.
이제 개발자가 이 클래스를 읽고 역할을 파악하는 데 걸리는 시간이 훨씬 줄어들었습니다.
각각의 작은 코드 블록을 이해하는 데 걸리는 시간이 줄기도 하였습니다.
이처럼 '무엇'을 담당하는 메서드와 '어떻게'를 구현하는 다른 메서드로 나누는 것은 좋은 프랙티스입니다.
ImportEmployeesService는 이제 EmployeeRepository와 EmployeeImportCSVParser를 생성자를 통해 받아야 합니다.
즉, 코드의 다른 부분에서 이 클래스를 인스턴스화해야 한다는 의미입니다. 의존성은 생성자를 통해 전달받는 것이 좋으니 참고해 주세요!